Merge pull request #212 from voideditor/model-selection

Latest updates
This commit is contained in:
Andrew Pareles 2025-01-15 18:38:51 -08:00 committed by GitHub
commit 2d583b87dc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
63 changed files with 4477 additions and 1660 deletions

33
CHANGELOG.md Normal file
View file

@ -0,0 +1,33 @@
## Jan. 13, 2025 - Entering beta
- Added quick edits! Void handles FIM-prompting, output parsing, and history management for inline UI.
- Migrated away from VS Code extension API - Void now lives and interacts entirely within the VS Code codebase.
- New settings page with model configuration, one-click switch, and user settings.
- Added auto-detection (via polling) of local models by default.
- LLM requests originate from `node/`, which fixes common CORS and CSP issues when running some models locally.
- Misc improvements like UI and history for Accept | Reject in the sidebar and editor, streaming interruptions, and past chat history.
- Automatic file selection on tab switches.
- Lots of new UI, misc bug fixes, and performance improvements.
- Switched from the MIT License to the Apache 2.0 License. Apache's attribution clause provides a small amount of protection to our source initiative.
A huge shoutout to our many contributors. If you'd like to help build Void,
## Sept/Oct. 2024 - Early launch
- Initialized Void's website and GitHub repo.
- Started a waitlist.

View file

@ -1,3 +1,5 @@
# Void - this looks like the relevant file for us (product-build-darwin.yml is independent and maybe just used for testing)
steps:
- task: NodeTool@0
inputs:
@ -59,6 +61,8 @@ steps:
- script: node build/azure-pipelines/distro/mixin-quality
displayName: Mixin distro quality
## Void - IMPORTANT
- script: |
set -e
unzip $(Pipeline.Workspace)/unsigned_vscode_client_darwin_x64_archive/VSCode-darwin-x64.zip -d $(agent.builddirectory)/VSCode-darwin-x64
@ -66,6 +70,7 @@ steps:
DEBUG=* node build/darwin/create-universal-app.js $(agent.builddirectory)
displayName: Create Universal App
## Void - IMPORTANT
- script: |
set -e
security create-keychain -p pwd $(agent.tempdirectory)/buildagent.keychain

View file

@ -3,6 +3,8 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// Void explanation - product-build-darwin-universal.yml runs this (create-universal-app.ts), then sign.ts
import * as path from 'path';
import * as fs from 'fs';
import * as minimatch from 'minimatch';

View file

@ -92,21 +92,21 @@ async function main(buildDir?: string): Promise<void> {
'-insert',
'NSAppleEventsUsageDescription',
'-string',
'An application in Visual Studio Code wants to use AppleScript.',
'An application in Void wants to use AppleScript.',
`${infoPlistPath}`
]);
await spawn('plutil', [
'-replace',
'NSMicrophoneUsageDescription',
'-string',
'An application in Visual Studio Code wants to use the Microphone.',
'An application in Void wants to use the Microphone.',
`${infoPlistPath}`
]);
await spawn('plutil', [
'-replace',
'NSCameraUsageDescription',
'-string',
'An application in Visual Studio Code wants to use the Camera.',
'An application in Void wants to use the Camera.',
`${infoPlistPath}`
]);
}

View file

@ -341,6 +341,8 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op
this.emit('data', file);
}));
// Void - this is important, creates the product.json in .app
let productJsonContents;
const productJsonStream = gulp.src(['product.json'], { base: '.' })
.pipe(json({ commit, date: readISODate('out-build'), checksums, version }))

775
package-lock.json generated
View file

@ -156,6 +156,7 @@
"mocha": "^10.2.0",
"mocha-junit-reporter": "^2.2.1",
"mocha-multi-reporters": "^1.5.1",
"next": "^15.1.4",
"nodemon": "^3.1.9",
"npm-run-all": "^4.1.5",
"opn": "^6.0.0",
@ -1056,6 +1057,17 @@
"node": ">= 4.0.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz",
"integrity": "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@es-joy/jsdoccomment": {
"version": "0.41.0",
"resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.41.0.tgz",
@ -1702,6 +1714,386 @@
"deprecated": "Use @eslint/object-schema instead",
"dev": true
},
"node_modules/@img/sharp-darwin-arm64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz",
"integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-arm64": "1.0.4"
}
},
"node_modules/@img/sharp-darwin-x64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz",
"integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-x64": "1.0.4"
}
},
"node_modules/@img/sharp-libvips-darwin-arm64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz",
"integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-darwin-x64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz",
"integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz",
"integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==",
"cpu": [
"arm"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz",
"integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-s390x": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz",
"integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==",
"cpu": [
"s390x"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-x64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz",
"integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==",
"cpu": [
"x64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz",
"integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz",
"integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==",
"cpu": [
"x64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-linux-arm": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz",
"integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm": "1.0.5"
}
},
"node_modules/@img/sharp-linux-arm64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz",
"integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm64": "1.0.4"
}
},
"node_modules/@img/sharp-linux-s390x": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz",
"integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==",
"cpu": [
"s390x"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-s390x": "1.0.4"
}
},
"node_modules/@img/sharp-linux-x64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz",
"integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-x64": "1.0.4"
}
},
"node_modules/@img/sharp-linuxmusl-arm64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz",
"integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-arm64": "1.0.4"
}
},
"node_modules/@img/sharp-linuxmusl-x64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz",
"integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-x64": "1.0.4"
}
},
"node_modules/@img/sharp-wasm32": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz",
"integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==",
"cpu": [
"wasm32"
],
"dev": true,
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
"optional": true,
"dependencies": {
"@emnapi/runtime": "^1.2.0"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-ia32": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz",
"integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-x64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz",
"integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==",
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@ -1974,6 +2366,149 @@
"exenv-es6": "^1.1.1"
}
},
"node_modules/@next/env": {
"version": "15.1.4",
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.1.4.tgz",
"integrity": "sha512-2fZ5YZjedi5AGaeoaC0B20zGntEHRhi2SdWcu61i48BllODcAmmtj8n7YarSPt4DaTsJaBFdxQAVEVzgmx2Zpw==",
"dev": true,
"license": "MIT"
},
"node_modules/@next/swc-darwin-arm64": {
"version": "15.1.4",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.1.4.tgz",
"integrity": "sha512-wBEMBs+np+R5ozN1F8Y8d/Dycns2COhRnkxRc+rvnbXke5uZBHkUGFgWxfTXn5rx7OLijuUhyfB+gC/ap58dDw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "15.1.4",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.1.4.tgz",
"integrity": "sha512-7sgf5rM7Z81V9w48F02Zz6DgEJulavC0jadab4ZsJ+K2sxMNK0/BtF8J8J3CxnsJN3DGcIdC260wEKssKTukUw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "15.1.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.1.4.tgz",
"integrity": "sha512-JaZlIMNaJenfd55kjaLWMfok+vWBlcRxqnRoZrhFQrhM1uAehP3R0+Aoe+bZOogqlZvAz53nY/k3ZyuKDtT2zQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "15.1.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.1.4.tgz",
"integrity": "sha512-7EBBjNoyTO2ipMDgCiORpwwOf5tIueFntKjcN3NK+GAQD7OzFJe84p7a2eQUeWdpzZvhVXuAtIen8QcH71ZCOQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "15.1.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.1.4.tgz",
"integrity": "sha512-9TGEgOycqZFuADyFqwmK/9g6S0FYZ3tphR4ebcmCwhL8Y12FW8pIBKJvSwV+UBjMkokstGNH+9F8F031JZKpHw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "15.1.4",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.1.4.tgz",
"integrity": "sha512-0578bLRVDJOh+LdIoKvgNDz77+Bd85c5JrFgnlbI1SM3WmEQvsjxTA8ATu9Z9FCiIS/AliVAW2DV/BDwpXbtiQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "15.1.4",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.1.4.tgz",
"integrity": "sha512-JgFCiV4libQavwII+kncMCl30st0JVxpPOtzWcAI2jtum4HjYaclobKhj+JsRu5tFqMtA5CJIa0MvYyuu9xjjQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "15.1.4",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.1.4.tgz",
"integrity": "sha512-xxsJy9wzq7FR5SqPCUqdgSXiNXrMuidgckBa8nH9HtjjxsilgcN6VgXF6tZ3uEWuVEadotQJI8/9EQ6guTC4Yw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -2711,6 +3246,23 @@
"node": ">=10"
}
},
"node_modules/@swc/counter": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
"integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/@swc/helpers": {
"version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
"integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.8.0"
}
},
"node_modules/@szmarczak/http-timer": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
@ -5482,6 +6034,18 @@
"esbuild": ">=0.18"
}
},
"node_modules/busboy": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
"dev": true,
"dependencies": {
"streamsearch": "^1.1.0"
},
"engines": {
"node": ">=10.16.0"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@ -5907,6 +6471,13 @@
"integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==",
"dev": true
},
"node_modules/client-only": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
"dev": true,
"license": "MIT"
},
"node_modules/cliui": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
@ -6111,6 +6682,21 @@
"node": ">=0.10.0"
}
},
"node_modules/color": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"color-convert": "^2.0.1",
"color-string": "^1.9.0"
},
"engines": {
"node": ">=12.5.0"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@ -6129,6 +6715,18 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"node_modules/color-string": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"color-name": "^1.0.0",
"simple-swizzle": "^0.2.2"
}
},
"node_modules/color-support": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
@ -15620,12 +16218,96 @@
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
"dev": true
},
"node_modules/next": {
"version": "15.1.4",
"resolved": "https://registry.npmjs.org/next/-/next-15.1.4.tgz",
"integrity": "sha512-mTaq9dwaSuwwOrcu3ebjDYObekkxRnXpuVL21zotM8qE2W0HBOdVIdg2Li9QjMEZrj73LN96LcWcz62V19FjAg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@next/env": "15.1.4",
"@swc/counter": "0.1.3",
"@swc/helpers": "0.5.15",
"busboy": "1.6.0",
"caniuse-lite": "^1.0.30001579",
"postcss": "8.4.31",
"styled-jsx": "5.1.6"
},
"bin": {
"next": "dist/bin/next"
},
"engines": {
"node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
},
"optionalDependencies": {
"@next/swc-darwin-arm64": "15.1.4",
"@next/swc-darwin-x64": "15.1.4",
"@next/swc-linux-arm64-gnu": "15.1.4",
"@next/swc-linux-arm64-musl": "15.1.4",
"@next/swc-linux-x64-gnu": "15.1.4",
"@next/swc-linux-x64-musl": "15.1.4",
"@next/swc-win32-arm64-msvc": "15.1.4",
"@next/swc-win32-x64-msvc": "15.1.4",
"sharp": "^0.33.5"
},
"peerDependencies": {
"@opentelemetry/api": "^1.1.0",
"@playwright/test": "^1.41.2",
"babel-plugin-react-compiler": "*",
"react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
"react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
"sass": "^1.3.0"
},
"peerDependenciesMeta": {
"@opentelemetry/api": {
"optional": true
},
"@playwright/test": {
"optional": true
},
"babel-plugin-react-compiler": {
"optional": true
},
"sass": {
"optional": true
}
}
},
"node_modules/next-tick": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz",
"integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==",
"dev": true
},
"node_modules/next/node_modules/postcss": {
"version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
"integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.6",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/nice-try": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
@ -19444,6 +20126,47 @@
"node": ">=0.10.0"
}
},
"node_modules/sharp": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz",
"integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==",
"dev": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"color": "^4.2.3",
"detect-libc": "^2.0.3",
"semver": "^7.6.3"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-darwin-arm64": "0.33.5",
"@img/sharp-darwin-x64": "0.33.5",
"@img/sharp-libvips-darwin-arm64": "1.0.4",
"@img/sharp-libvips-darwin-x64": "1.0.4",
"@img/sharp-libvips-linux-arm": "1.0.5",
"@img/sharp-libvips-linux-arm64": "1.0.4",
"@img/sharp-libvips-linux-s390x": "1.0.4",
"@img/sharp-libvips-linux-x64": "1.0.4",
"@img/sharp-libvips-linuxmusl-arm64": "1.0.4",
"@img/sharp-libvips-linuxmusl-x64": "1.0.4",
"@img/sharp-linux-arm": "0.33.5",
"@img/sharp-linux-arm64": "0.33.5",
"@img/sharp-linux-s390x": "0.33.5",
"@img/sharp-linux-x64": "0.33.5",
"@img/sharp-linuxmusl-arm64": "0.33.5",
"@img/sharp-linuxmusl-x64": "0.33.5",
"@img/sharp-wasm32": "0.33.5",
"@img/sharp-win32-ia32": "0.33.5",
"@img/sharp-win32-x64": "0.33.5"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@ -19604,6 +20327,25 @@
"simple-concat": "^1.0.0"
}
},
"node_modules/simple-swizzle": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
"integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"is-arrayish": "^0.3.1"
}
},
"node_modules/simple-swizzle/node_modules/is-arrayish": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/simple-update-notifier": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
@ -20254,6 +20996,15 @@
"node": ">=0.10"
}
},
"node_modules/streamsearch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
"dev": true,
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/streamx": {
"version": "2.21.1",
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.21.1.tgz",
@ -20492,6 +21243,30 @@
"webpack": "^5.0.0"
}
},
"node_modules/styled-jsx": {
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
"integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==",
"dev": true,
"license": "MIT",
"dependencies": {
"client-only": "0.0.1"
},
"engines": {
"node": ">= 12.0.0"
},
"peerDependencies": {
"react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0"
},
"peerDependenciesMeta": {
"@babel/core": {
"optional": true
},
"babel-plugin-macros": {
"optional": true
}
}
},
"node_modules/stylehacks": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-6.1.1.tgz",

View file

@ -223,6 +223,7 @@
"mocha": "^10.2.0",
"mocha-junit-reporter": "^2.2.1",
"mocha-multi-reporters": "^1.5.1",
"next": "^15.1.4",
"nodemon": "^3.1.9",
"npm-run-all": "^4.1.5",
"opn": "^6.0.0",

View file

@ -223,6 +223,11 @@ export class BaseEditorSimpleWorker implements IDisposable, IWorkerTextModelSync
private static readonly _diffLimit = 100000;
public async $computeMoreMinimalEdits(modelUrl: string, edits: TextEdit[], pretty: boolean): Promise<TextEdit[]> {
return this.$Void_computeMoreMinimalEdits(modelUrl, edits, pretty)
}
// Void added this as non async
public $Void_computeMoreMinimalEdits(modelUrl: string, edits: TextEdit[], pretty: boolean): TextEdit[] {
const model = this._getModel(modelUrl);
if (!model) {
return edits;

View file

@ -403,8 +403,9 @@ function isVersionValid(currentVersion: string, date: ProductDate, requestedVers
}
if (!isValidVersion(currentVersion, date, desiredVersion)) {
notices.push(nls.localize('versionMismatch', "Extension is not compatible with Code {0}. Extension requires: {1}.", currentVersion, requestedVersion));
return false;
// Void - ignore not compatible
// notices.push(nls.localize('versionMismatch', "Extension is not compatible with Code {0}. Extension requires: {1}.", currentVersion, requestedVersion));
// return false;
}
return true;

View file

@ -31,25 +31,29 @@ export class ServerTelemetryService extends TelemetryService implements IServerT
}
override publicLog(eventName: string, data?: ITelemetryData) {
if (this._injectedTelemetryLevel < TelemetryLevel.USAGE) {
return;
}
return super.publicLog(eventName, data);
// Void commented this out
// if (this._injectedTelemetryLevel < TelemetryLevel.USAGE) {
// return;
// }
// return super.publicLog(eventName, data);
}
override publicLog2<E extends ClassifiedEvent<OmitMetadata<T>> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck<T, E>) {
return this.publicLog(eventName, data as ITelemetryData | undefined);
// Void commented this out
// return this.publicLog(eventName, data as ITelemetryData | undefined);
}
override publicLogError(errorEventName: string, data?: ITelemetryData) {
if (this._injectedTelemetryLevel < TelemetryLevel.ERROR) {
return Promise.resolve(undefined);
}
return super.publicLogError(errorEventName, data);
// Void commented this out
// if (this._injectedTelemetryLevel < TelemetryLevel.ERROR) {
// return Promise.resolve(undefined);
// }
// return super.publicLogError(errorEventName, data);
}
override publicLogError2<E extends ClassifiedEvent<OmitMetadata<T>> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck<T, E>) {
return this.publicLogError(eventName, data as ITelemetryData | undefined);
// Void commented this out
// return this.publicLogError(eventName, data as ITelemetryData | undefined);
}
async updateInjectedTelemetryLevel(telemetryLevel: TelemetryLevel): Promise<void> {

View file

@ -4,7 +4,6 @@
*--------------------------------------------------------------------------------------------*/
import { DisposableStore } from '../../../base/common/lifecycle.js';
import { mixin } from '../../../base/common/objects.js';
import { isWeb } from '../../../base/common/platform.js';
import { escapeRegExpCharacters } from '../../../base/common/strings.js';
import { localize } from '../../../nls.js';
@ -15,7 +14,7 @@ import { IProductService } from '../../product/common/productService.js';
import { Registry } from '../../registry/common/platform.js';
import { ClassifiedEvent, IGDPRProperty, OmitMetadata, StrictPropertyCheck } from './gdprTypings.js';
import { ITelemetryData, ITelemetryService, TelemetryConfiguration, TelemetryLevel, TELEMETRY_CRASH_REPORTER_SETTING_ID, TELEMETRY_OLD_SETTING_ID, TELEMETRY_SECTION_ID, TELEMETRY_SETTING_ID, ICommonProperties } from './telemetry.js';
import { cleanData, getTelemetryLevel, ITelemetryAppender } from './telemetryUtils.js';
import { getTelemetryLevel, ITelemetryAppender } from './telemetryUtils.js';
export interface ITelemetryServiceConfig {
appenders: ITelemetryAppender[];
@ -38,7 +37,7 @@ export class TelemetryService implements ITelemetryService {
readonly firstSessionDate: string;
readonly msftInternal: boolean | undefined;
private _appenders: ITelemetryAppender[];
// private _appenders: ITelemetryAppender[];
private _commonProperties: ICommonProperties;
private _experimentProperties: { [name: string]: string } = {};
private _piiPaths: string[];
@ -53,7 +52,7 @@ export class TelemetryService implements ITelemetryService {
@IConfigurationService private _configurationService: IConfigurationService,
@IProductService private _productService: IProductService
) {
this._appenders = config.appenders;
// this._appenders = config.appenders;
this._commonProperties = config.commonProperties ?? Object.create(null);
this.sessionId = this._commonProperties['sessionID'] as string;
@ -121,44 +120,47 @@ export class TelemetryService implements ITelemetryService {
this._disposables.dispose();
}
private _log(eventName: string, eventLevel: TelemetryLevel, data?: ITelemetryData) {
// don't send events when the user is optout
if (this._telemetryLevel < eventLevel) {
return;
}
// Void commented this out
// private _log(eventName: string, eventLevel: TelemetryLevel, data?: ITelemetryData) {
// // don't send events when the user is optout
// if (this._telemetryLevel < eventLevel) {
// return;
// }
// add experiment properties
data = mixin(data, this._experimentProperties);
// // add experiment properties
// data = mixin(data, this._experimentProperties);
// remove all PII from data
data = cleanData(data as Record<string, any>, this._cleanupPatterns);
// // remove all PII from data
// data = cleanData(data as Record<string, any>, this._cleanupPatterns);
// add common properties
data = mixin(data, this._commonProperties);
// // add common properties
// data = mixin(data, this._commonProperties);
// Log to the appenders of sufficient level
this._appenders.forEach(a => a.log(eventName, data));
}
// // Log to the appenders of sufficient level
// this._appenders.forEach(a => a.log(eventName, data));
// }
publicLog(eventName: string, data?: ITelemetryData) {
this._log(eventName, TelemetryLevel.USAGE, data);
// this._log(eventName, TelemetryLevel.USAGE, data);
}
publicLog2<E extends ClassifiedEvent<OmitMetadata<T>> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck<T, E>) {
this.publicLog(eventName, data as ITelemetryData);
// this.publicLog(eventName, data as ITelemetryData);
}
publicLogError(errorEventName: string, data?: ITelemetryData) {
if (!this._sendErrorTelemetry) {
return;
}
// Void commented this out
// if (!this._sendErrorTelemetry) {
// return;
// }
// Send error event and anonymize paths
this._log(errorEventName, TelemetryLevel.ERROR, data);
// // Send error event and anonymize paths
// this._log(errorEventName, TelemetryLevel.ERROR, data);
}
publicLogError2<E extends ClassifiedEvent<OmitMetadata<T>> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck<T, E>) {
this.publicLogError(eventName, data as ITelemetryData);
// Void commented this out
// this.publicLogError(eventName, data as ITelemetryData);
}
}

View file

@ -15,7 +15,10 @@ import { IRequestService } from '../../request/common/request.js';
import { AvailableForDownload, DisablementReason, IUpdateService, State, StateType, UpdateType } from '../common/update.js';
export function createUpdateURL(platform: string, quality: string, productService: IProductService): string {
return `${productService.updateUrl}/api/update/${platform}/${quality}/${productService.commit}`;
// return `https://voideditor.dev/api/update/${platform}/stable`;
// return `${productService.updateUrl}/api/update/${platform}/${quality}/${productService.commit}`;
// https://github.com/VSCodium/update-api
return `https://updates.voideditor.dev/api/update/${platform}/${quality}/${productService.commit}`;
}
export type UpdateNotAvailableClassification = {
@ -70,32 +73,38 @@ export abstract class AbstractUpdateService implements IUpdateService {
*/
protected async initialize(): Promise<void> {
if (!this.environmentMainService.isBuilt) {
console.log('is NOT built, canceling update service')
this.setState(State.Disabled(DisablementReason.NotBuilt));
return; // updates are never enabled when running out of sources
}
console.log('is built, continuing with update service')
if (this.environmentMainService.disableUpdates) {
this.setState(State.Disabled(DisablementReason.DisabledByEnvironment));
this.logService.info('update#ctor - updates are disabled by the environment');
return;
}
// Void commented this
// if (this.environmentMainService.disableUpdates) {
// this.setState(State.Disabled(DisablementReason.DisabledByEnvironment));
// this.logService.info('update#ctor - updates are disabled by the environment');
// return;
// }
if (!this.productService.updateUrl || !this.productService.commit) {
this.setState(State.Disabled(DisablementReason.MissingConfiguration));
this.logService.info('update#ctor - updates are disabled as there is no update URL');
return;
}
// if (!this.productService.updateUrl || !this.productService.commit) {
// this.setState(State.Disabled(DisablementReason.MissingConfiguration));
// this.logService.info('update#ctor - updates are disabled as there is no update URL');
// return;
// }
// Void - for now, always update
const updateMode = 'default' //this.configurationService.getValue<'none' | 'manual' | 'start' | 'default'>('update.mode');
const updateMode = this.configurationService.getValue<'none' | 'manual' | 'start' | 'default'>('update.mode');
const quality = this.getProductQuality(updateMode);
if (!quality) {
this.setState(State.Disabled(DisablementReason.ManuallyDisabled));
this.logService.info('update#ctor - updates are disabled by user preference');
return;
}
this.url = this.buildUpdateFeedUrl(quality);
// const quality = 'stable'
this.url = this.doBuildUpdateFeedUrl(quality);
if (!this.url) {
this.setState(State.Disabled(DisablementReason.InvalidConfiguration));
this.logService.info('update#ctor - updates are disabled as the update URL is badly formed');
@ -111,33 +120,30 @@ export abstract class AbstractUpdateService implements IUpdateService {
this.setState(State.Idle(this.getUpdateType()));
if (updateMode === 'manual') {
this.logService.info('update#ctor - manual checks only; automatic updates are disabled by user preference');
return;
}
// if (updateMode === 'manual') {
// this.logService.info('update#ctor - manual checks only; automatic updates are disabled by user preference');
// return;
// }
if (updateMode === 'start') {
this.logService.info('update#ctor - startup checks only; automatic updates are disabled by user preference');
// if (updateMode === 'start') {
// this.logService.info('update#ctor - startup checks only; automatic updates are disabled by user preference');
// Check for updates only once after 30 seconds
setTimeout(() => this.checkForUpdates(false), 30 * 1000);
} else {
// Start checking for updates after 30 seconds
this.scheduleCheckForUpdates(30 * 1000).then(undefined, err => this.logService.error(err));
}
// // Check for updates only once after 30 seconds
// setTimeout(() => this.checkForUpdates(false), 30 * 1000);
// } else {
// Start checking for updates after 30 seconds
this.scheduleCheckForUpdates(30 * 1000).then(undefined, err => this.logService.error(err));
// }
}
private getProductQuality(updateMode: string): string | undefined {
return updateMode === 'none' ? undefined : this.productService.quality;
}
private scheduleCheckForUpdates(delay = 60 * 60 * 1000): Promise<void> {
return timeout(delay)
.then(() => this.checkForUpdates(false))
.then(() => {
// Check again after 1 hour
return this.scheduleCheckForUpdates(60 * 60 * 1000);
});
private async scheduleCheckForUpdates(delay = 60 * 60 * 1000): Promise<void> {
await timeout(delay);
await this.checkForUpdates(false);
return await this.scheduleCheckForUpdates(60 * 60 * 1000);
}
async checkForUpdates(explicit: boolean): Promise<void> {
@ -160,6 +166,7 @@ export abstract class AbstractUpdateService implements IUpdateService {
await this.doDownloadUpdate(this.state);
}
// override implemented by windows and linux
protected async doDownloadUpdate(state: AvailableForDownload): Promise<void> {
// noop
}
@ -174,6 +181,7 @@ export abstract class AbstractUpdateService implements IUpdateService {
await this.doApplyUpdate();
}
// windows overrides this
protected async doApplyUpdate(): Promise<void> {
// noop
}
@ -236,6 +244,6 @@ export abstract class AbstractUpdateService implements IUpdateService {
// noop
}
protected abstract buildUpdateFeedUrl(quality: string): string | undefined;
protected abstract doBuildUpdateFeedUrl(quality: string): string | undefined;
protected abstract doCheckForUpdates(context: any): void;
}

View file

@ -73,7 +73,7 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau
this.setState(State.Idle(UpdateType.Archive, message));
}
protected buildUpdateFeedUrl(quality: string): string | undefined {
protected doBuildUpdateFeedUrl(quality: string): string | undefined {
let assetID: string;
if (!this.productService.darwinUniversalAssetId) {
assetID = process.arch === 'x64' ? 'darwin' : 'darwin-arm64';

View file

@ -30,7 +30,7 @@ export class LinuxUpdateService extends AbstractUpdateService {
super(lifecycleMainService, configurationService, environmentMainService, requestService, logService, productService);
}
protected buildUpdateFeedUrl(quality: string): string {
protected doBuildUpdateFeedUrl(quality: string): string {
return createUpdateURL(`linux-${process.arch}`, quality, this.productService);
}

View file

@ -99,7 +99,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
await super.initialize();
}
protected buildUpdateFeedUrl(quality: string): string | undefined {
protected doBuildUpdateFeedUrl(quality: string): string | undefined {
let platform = `win32-${process.arch}`;
if (getUpdateType() === UpdateType.Archive) {

View file

@ -52,6 +52,7 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
super()
// const service = ProxyChannel.toService<LLMMessageChannel>(mainProcessService.getChannel('void-channel-sendLLMMessage')); // lets you call it like a service
// see llmMessageChannel.ts
this.channel = this.mainProcessService.getChannel('void-channel-llmMessageService')
// .listen sets up an IPC channel and takes a few ms, so we set up listeners immediately and add hooks to them instead
@ -68,14 +69,14 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
this.onErrorHooks_llm[e.requestId]?.(e)
this._onRequestIdDone(e.requestId)
}))
// ollama
// ollama .list()
this._register((this.channel.listen('onSuccess_ollama') satisfies Event<EventModelListOnSuccessParams<OllamaModelResponse>>)(e => {
this.onSuccess_ollama[e.requestId]?.(e)
}))
this._register((this.channel.listen('onError_ollama') satisfies Event<EventModelListOnErrorParams<OllamaModelResponse>>)(e => {
this.onError_ollama[e.requestId]?.(e)
}))
// openaiCompatible
// openaiCompatible .list()
this._register((this.channel.listen('onSuccess_openAICompatible') satisfies Event<EventModelListOnSuccessParams<OpenaiCompatibleModelResponse>>)(e => {
this.onSuccess_openAICompatible[e.requestId]?.(e)
}))
@ -87,7 +88,7 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
sendLLMMessage(params: ServiceSendLLMMessageParams) {
const { onText, onFinalMessage, onError, ...proxyParams } = params;
const { featureName } = proxyParams
const { useProviderFor: featureName } = proxyParams
// end early if no provider
const modelSelection = this.voidSettingsService.state.modelSelectionOfFeature[featureName]
@ -97,6 +98,10 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
}
const { providerName, modelName } = modelSelection
const aiInstructions = this.voidSettingsService.state.globalSettings.aiInstructions
if (aiInstructions)
proxyParams.messages.unshift({ role: 'system', content: aiInstructions })
// add state for request id
const requestId_ = generateUuid();
this.onTextHooks_llm[requestId_] = onText

View file

@ -7,6 +7,19 @@ import { IRange } from '../../../editor/common/core/range'
import { ProviderName, SettingsOfProvider } from './voidSettingsTypes.js'
export const errorDetails = (fullError: Error | null): string | null => {
if (fullError === null) {
return null
}
else if (typeof fullError === 'object') {
return JSON.stringify(fullError, null, 2)
}
else if (typeof fullError === 'string') {
return null
}
return null
}
export type OnText = (p: { newText: string, fullText: string }) => void
export type OnFinalMessage = (p: { fullText: string }) => void
export type OnError = (p: { message: string, fullError: Error | null }) => void
@ -18,12 +31,12 @@ export type LLMMessage = {
}
export type ServiceSendLLMFeatureParams = {
featureName: 'Ctrl+K';
useProviderFor: 'Ctrl+K';
range: IRange;
} | {
featureName: 'Ctrl+L';
useProviderFor: 'Ctrl+L';
} | {
featureName: 'Autocomplete';
useProviderFor: 'Autocomplete';
range: IRange;
}

View file

@ -25,6 +25,7 @@ export class MetricsService implements IMetricsService {
constructor(
@IMainProcessService mainProcessService: IMainProcessService // (only usable on client side)
) {
// creates an IPC proxy to use metricsMainService.ts
this.metricsService = ProxyChannel.toService<IMetricsService>(mainProcessService.getChannel('void-channel-metrics'));
}

View file

@ -25,22 +25,33 @@ type RefreshableState = ({
state: 'finished',
timeoutId: null,
} | {
state: 'finished_invisible',
state: 'error',
timeoutId: null,
})
/*
user click -> error -> fire(error)
\> success -> fire(success)
finally: keep polling
poll -> do not fire
*/
export type RefreshModelStateOfProvider = Record<RefreshableProviderName, RefreshableState>
const refreshBasedOn: { [k in RefreshableProviderName]: (keyof SettingsOfProvider[k])[] } = {
ollama: ['_enabled', 'endpoint'],
openAICompatible: ['_enabled', 'endpoint', 'apiKey'],
// openAICompatible: ['_enabled', 'endpoint', 'apiKey'],
}
const REFRESH_INTERVAL = 5_000
// const COOLDOWN_TIMEOUT = 300
const autoOptions = { enableProviderOnSuccess: true, doNotFire: true }
// element-wise equals
function eq<T>(a: T[], b: T[]): boolean {
if (a.length !== b.length) return false
@ -51,7 +62,7 @@ function eq<T>(a: T[], b: T[]): boolean {
}
export interface IRefreshModelService {
readonly _serviceBrand: undefined;
refreshModels: (providerName: RefreshableProviderName) => Promise<void>;
startRefreshingModels: (providerName: RefreshableProviderName, options: { enableProviderOnSuccess: boolean, doNotFire: boolean }) => void;
onDidChangeState: Event<RefreshableProviderName>;
state: RefreshModelStateOfProvider;
}
@ -75,23 +86,23 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
const disposables: Set<IDisposable> = new Set()
const initializePollingAndOnChange = () => {
const initializeAutoPollingAndOnChange = () => {
this._clearAllTimeouts()
disposables.forEach(d => d.dispose())
disposables.clear()
if (!voidSettingsService.state.featureFlagSettings.autoRefreshModels) return
if (!voidSettingsService.state.globalSettings.autoRefreshModels) return
for (const providerName of refreshableProviderNames) {
const { _enabled: enabled } = this.voidSettingsService.state.settingsOfProvider[providerName]
this.refreshModels(providerName, !enabled, { isPolling: true, isInternal: true })
// const { _enabled: enabled } = this.voidSettingsService.state.settingsOfProvider[providerName]
this.startRefreshingModels(providerName, autoOptions)
// every time providerName.enabled changes, refresh models too, like a useEffect
let relevantVals = () => refreshBasedOn[providerName].map(settingName => this.voidSettingsService.state.settingsOfProvider[providerName][settingName])
let relevantVals = () => refreshBasedOn[providerName].map(settingName => voidSettingsService.state.settingsOfProvider[providerName][settingName])
let prevVals = relevantVals() // each iteration of a for loop has its own context and vars, so this is ok
disposables.add(
this.voidSettingsService.onDidChangeState(() => { // we might want to debounce this
voidSettingsService.onDidChangeState(() => { // we might want to debounce this
const newVals = relevantVals()
if (!eq(prevVals, newVals)) {
@ -101,7 +112,7 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
// if it was just enabled, or there was a change and it wasn't to the enabled state, refresh
if ((enabled && !prevEnabled) || (!enabled && !prevEnabled)) {
// if user just clicked enable, refresh
this.refreshModels(providerName, !enabled, { isPolling: false, isInternal: true })
this.startRefreshingModels(providerName, autoOptions)
}
else {
// else if user just clicked disable, don't refresh
@ -117,11 +128,11 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
}
}
// on mount (when get init settings state), and if a relevant feature flag changes (detected natively right now by refreshing if any flag changes), start refreshing models
// on mount (when get init settings state), and if a relevant feature flag changes, start refreshing models
voidSettingsService.waitForInitState.then(() => {
initializePollingAndOnChange()
initializeAutoPollingAndOnChange()
this._register(
voidSettingsService.onDidChangeState((type) => { if (type === 'featureFlagSettings') initializePollingAndOnChange() })
voidSettingsService.onDidChangeState((type) => { if (typeof type === 'object' && type[1] === 'autoRefreshModels') initializeAutoPollingAndOnChange() })
)
})
@ -129,54 +140,53 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
state: RefreshModelStateOfProvider = {
ollama: { state: 'init', timeoutId: null },
openAICompatible: { state: 'init', timeoutId: null },
}
// start listening for models (and don't stop until success)
async refreshModels(providerName: RefreshableProviderName, enableProviderOnSuccess?: boolean, options?: { isPolling?: boolean, isInternal?: boolean }) {
const { isPolling, isInternal } = options ?? {}
console.log(`refreshModels, isInternal ${isInternal} isPolling ${isPolling}`)
startRefreshingModels: IRefreshModelService['startRefreshingModels'] = (providerName, options) => {
this._clearProviderTimeout(providerName)
// start loading models
if (!isInternal) this._setRefreshState(providerName, 'refreshing')
this._setRefreshState(providerName, 'refreshing', options)
const fn = providerName === 'ollama' ? this.llmMessageService.ollamaList
const autoPoll = () => {
if (this.voidSettingsService.state.globalSettings.autoRefreshModels) {
// resume auto-polling
const timeoutId = setTimeout(() => this.startRefreshingModels(providerName, autoOptions), REFRESH_INTERVAL)
this._setTimeoutId(providerName, timeoutId)
}
}
const listFn = providerName === 'ollama' ? this.llmMessageService.ollamaList
: providerName === 'openAICompatible' ? this.llmMessageService.openAICompatibleList
: () => { }
fn({
listFn({
onSuccess: ({ models }) => {
this.voidSettingsService.setDefaultModels(providerName, models.map(model => {
if (providerName === 'ollama') return (model as OllamaModelResponse).name
else if (providerName === 'openAICompatible') return (model as OpenaiCompatibleModelResponse).id
else throw new Error('refreshMode fn: unknown provider', providerName)
}))
if (enableProviderOnSuccess) {
this.voidSettingsService.setSettingOfProvider(providerName, '_enabled', true)
}
// set the models to the detected models
this.voidSettingsService.setAutodetectedModels(
providerName,
models.map(model => {
if (providerName === 'ollama') return (model as OllamaModelResponse).name;
else if (providerName === 'openAICompatible') return (model as OpenaiCompatibleModelResponse).id;
else throw new Error('refreshMode fn: unknown provider', providerName);
}),
{ enableProviderOnSuccess: options.enableProviderOnSuccess, hideRefresh: options.doNotFire }
)
if (!isInternal) this._setRefreshState(providerName, 'finished')
if (options.enableProviderOnSuccess) this.voidSettingsService.setSettingOfProvider(providerName, '_enabled', true)
this._setRefreshState(providerName, 'finished', options)
autoPoll()
},
onError: ({ error }) => {
console.log('retrying list models:', providerName, error)
this._setRefreshState(providerName, 'error', options)
autoPoll()
}
})
if (isInternal) this._setRefreshState(providerName, 'finished_invisible')
// check if we should poll
// if it was originally called as `isPolling` and if the `autoRefreshModels` flag is enabled
if (isPolling && this.voidSettingsService.state.featureFlagSettings.autoRefreshModels) {
const timeoutId = setTimeout(() => this.refreshModels(providerName, enableProviderOnSuccess, options), REFRESH_INTERVAL)
this._setTimeoutId(providerName, timeoutId)
}
}
_clearAllTimeouts() {
@ -197,7 +207,8 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
this.state[providerName].timeoutId = timeoutId
}
private _setRefreshState(providerName: RefreshableProviderName, state: RefreshableState['state']) {
private _setRefreshState(providerName: RefreshableProviderName, state: RefreshableState['state'], options?: { doNotFire: boolean }) {
if (options?.doNotFire) return
this.state[providerName].state = state
this._onDidChangeState.fire(providerName)
}

View file

@ -10,10 +10,11 @@ import { IEncryptionService } from '../../encryption/common/encryptionService.js
import { registerSingleton, InstantiationType } from '../../instantiation/common/extensions.js';
import { createDecorator } from '../../instantiation/common/instantiation.js';
import { IStorageService, StorageScope, StorageTarget } from '../../storage/common/storage.js';
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, modelInfoOfDefaultNames, VoidModelInfo, FeatureFlagSettings, FeatureFlagName, defaultFeatureFlagSettings } from './voidSettingsTypes.js';
import { IMetricsService } from './metricsService.js';
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, modelInfoOfDefaultNames, VoidModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings } from './voidSettingsTypes.js';
const STORAGE_KEY = 'void.voidSettingsStorage'
const STORAGE_KEY = 'void.settingsServiceStorage'
type SetSettingOfProviderFn = <S extends SettingName>(
providerName: ProviderName,
@ -27,21 +28,22 @@ type SetModelSelectionOfFeatureFn = <K extends FeatureName>(
options?: { doNotApplyEffects?: true }
) => Promise<void>;
type SetFeatureFlagFn = (flagName: FeatureFlagName, newVal: boolean) => void;
type SetGlobalSettingFn = <T extends GlobalSettingName, >(settingName: T, newVal: GlobalSettings[T]) => void;
export type ModelOption = { text: string, value: ModelSelection }
export type ModelOption = { name: string, selection: ModelSelection }
export type VoidSettingsState = {
readonly settingsOfProvider: SettingsOfProvider; // optionsOfProvider
readonly modelSelectionOfFeature: ModelSelectionOfFeature; // stateOfFeature
readonly featureFlagSettings: FeatureFlagSettings;
readonly globalSettings: GlobalSettings;
readonly _modelOptions: ModelOption[] // computed based on the two above items
}
type EventProp = Exclude<keyof VoidSettingsState, '_modelOptions'> | 'all'
type RealVoidSettings = Exclude<keyof VoidSettingsState, '_modelOptions'>
type EventProp<T extends RealVoidSettings = RealVoidSettings> = T extends 'globalSettings' ? [T, keyof VoidSettingsState[T]] : T | 'all'
export interface IVoidSettingsService {
@ -53,9 +55,9 @@ export interface IVoidSettingsService {
setSettingOfProvider: SetSettingOfProviderFn;
setModelSelectionOfFeature: SetModelSelectionOfFeatureFn;
setFeatureFlag: SetFeatureFlagFn;
setGlobalSetting: SetGlobalSettingFn;
setDefaultModels(providerName: ProviderName, modelNames: string[]): void;
setAutodetectedModels(providerName: ProviderName, modelNames: string[], logging: object): void;
toggleModelHidden(providerName: ProviderName, modelName: string): void;
addModel(providerName: ProviderName, modelName: string): void;
deleteModel(providerName: ProviderName, modelName: string): boolean;
@ -69,7 +71,7 @@ let _computeModelOptions = (settingsOfProvider: SettingsOfProvider) => {
if (!providerConfig._enabled) continue // if disabled, don't display model options
for (const { modelName, isHidden } of providerConfig.models) {
if (isHidden) continue
modelOptions.push({ text: `${modelName} (${providerName})`, value: { providerName, modelName } })
modelOptions.push({ name: `${modelName} (${providerName})`, selection: { providerName, modelName } })
}
}
return modelOptions
@ -80,7 +82,7 @@ const defaultState = () => {
const d: VoidSettingsState = {
settingsOfProvider: deepClone(defaultSettingsOfProvider),
modelSelectionOfFeature: { 'Ctrl+L': null, 'Ctrl+K': null, 'Autocomplete': null },
featureFlagSettings: deepClone(defaultFeatureFlagSettings),
globalSettings: deepClone(defaultGlobalSettings),
_modelOptions: _computeModelOptions(defaultSettingsOfProvider), // computed
}
return d
@ -100,6 +102,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
constructor(
@IStorageService private readonly _storageService: IStorageService,
@IEncryptionService private readonly _encryptionService: IEncryptionService,
@IMetricsService private readonly _metricsService: IMetricsService,
// could have used this, but it's clearer the way it is (+ slightly different eg StorageTarget.USER)
// @ISecretStorageService private readonly _secretStorageService: ISecretStorageService,
) {
@ -148,7 +151,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
}
}
const newFeatureFlags = this.state.featureFlagSettings
const newGlobalSettings = this.state.globalSettings
// if changed models or enabled a provider, recompute models list
const modelsListChanged = settingName === 'models' || settingName === '_enabled'
@ -157,7 +160,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
const newState: VoidSettingsState = {
modelSelectionOfFeature: newModelSelectionOfFeature,
settingsOfProvider: newSettingsOfProvider,
featureFlagSettings: newFeatureFlags,
globalSettings: newGlobalSettings,
_modelOptions: newModelsList,
}
@ -169,11 +172,11 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
for (const featureName of featureNames) {
const currentSelection = newModelSelectionOfFeature[featureName]
const selnIdx = currentSelection === null ? -1 : newModelsList.findIndex(m => modelSelectionsEqual(m.value, currentSelection))
const selnIdx = currentSelection === null ? -1 : newModelsList.findIndex(m => modelSelectionsEqual(m.selection, currentSelection))
if (selnIdx === -1) {
if (newModelsList.length !== 0)
this.setModelSelectionOfFeature(featureName, newModelsList[0].value, { doNotApplyEffects: true })
this.setModelSelectionOfFeature(featureName, newModelsList[0].selection, { doNotApplyEffects: true })
else
this.setModelSelectionOfFeature(featureName, null, { doNotApplyEffects: true })
}
@ -185,17 +188,17 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
}
setFeatureFlag: SetFeatureFlagFn = async (flagName, newVal) => {
const newState = {
setGlobalSetting: SetGlobalSettingFn = async (settingName, newVal) => {
const newState: VoidSettingsState = {
...this.state,
featureFlagSettings: {
...this.state.featureFlagSettings,
[flagName]: newVal
globalSettings: {
...this.state.globalSettings,
[settingName]: newVal
}
}
this.state = newState
await this._storeState()
this._onDidChangeState.fire('featureFlagSettings')
this._onDidChangeState.fire(['globalSettings', settingName])
}
@ -220,25 +223,45 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
setDefaultModels(providerName: ProviderName, newDefaultModelNames: string[]) {
setAutodetectedModels(providerName: ProviderName, newDefaultModelNames: string[], logging: object) {
const { models } = this.state.settingsOfProvider[providerName]
const old_names = models.map(m => m.modelName)
const newDefaultModels = modelInfoOfDefaultNames(newDefaultModelNames, { isAutodetected: true, existingModels: models })
const newModels = [
...newDefaultModels,
...models.filter(m => !m.isDefault), // keep any non-default models
]
this.setSettingOfProvider(providerName, 'models', newModels)
// if the models changed, log it
const new_names = newModels.map(m => m.modelName)
if (!(old_names.length === new_names.length
&& old_names.every((_, i) => old_names[i] === new_names[i])
)) {
this._metricsService.capture('Autodetect Models', { providerName, newModels, ...logging })
}
}
toggleModelHidden(providerName: ProviderName, modelName: string) {
const { models } = this.state.settingsOfProvider[providerName]
const modelIdx = models.findIndex(m => m.modelName === modelName)
if (modelIdx === -1) return
const newIsHidden = !models[modelIdx].isHidden
const newModels: VoidModelInfo[] = [
...models.slice(0, modelIdx),
{ ...models[modelIdx], isHidden: !models[modelIdx].isHidden },
{ ...models[modelIdx], isHidden: newIsHidden },
...models.slice(modelIdx + 1, Infinity)
]
this.setSettingOfProvider(providerName, 'models', newModels)
this._metricsService.capture('Toggle Model Hidden', { providerName, modelName, newIsHidden })
}
addModel(providerName: ProviderName, modelName: string) {
const { models } = this.state.settingsOfProvider[providerName]
@ -249,6 +272,9 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
{ modelName, isDefault: false, isHidden: false }
]
this.setSettingOfProvider(providerName, 'models', newModels)
this._metricsService.capture('Add Model', { providerName, modelName })
}
deleteModel(providerName: ProviderName, modelName: string): boolean {
const { models } = this.state.settingsOfProvider[providerName]
@ -259,6 +285,9 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
...models.slice(delIdx + 1, Infinity)
]
this.setSettingOfProvider(providerName, 'models', newModels)
this._metricsService.capture('Delete Model', { providerName, modelName })
return true
}

View file

@ -14,26 +14,33 @@ export type VoidModelInfo = {
isAutodetected?: boolean, // whether the model was autodetected by polling
}
type ModelInfoOfDefaultNamesOptions = { isAutodetected: true, existingModels: VoidModelInfo[] } // | { isOtherOption: true, ...otherOptions }
export const modelInfoOfDefaultNames = (modelNames: string[], options?: ModelInfoOfDefaultNamesOptions): VoidModelInfo[] => {
// creates `modelInfo` from `modelNames`
export const modelInfoOfDefaultNames = (modelNames: string[], options?: { isAutodetected: true, existingModels: VoidModelInfo[] }): VoidModelInfo[] => {
const { isAutodetected, existingModels } = options ?? {}
const isDefault = true
const isHidden = modelNames.length >= 10 // hide all models if there are a ton of them, and make user enable them individually
if (!existingModels) {
if (!existingModels) { // default settings
return modelNames.map((modelName, i) => ({ modelName, isDefault, isAutodetected, isHidden, }))
return modelNames.map((modelName, i) => ({
modelName,
isDefault: true,
isAutodetected: isAutodetected,
isHidden: modelNames.length >= 10 // hide all models if there are a ton of them, and make user enable them individually
}))
} else {
// keep existing `isHidden` property
} else { // settings if there are existing models (keep existing `isHidden` property)
const existingModelsMap: Record<string, VoidModelInfo> = {}
for (const em of existingModels) {
existingModelsMap[em.modelName] = em
for (const existingModel of existingModels) {
existingModelsMap[existingModel.modelName] = existingModel
}
return modelNames.map((modelName, i) => ({ modelName, isDefault, isAutodetected, isHidden: !!existingModelsMap[modelName]?.isHidden, }))
return modelNames.map((modelName, i) => ({
modelName,
isDefault: true,
isAutodetected: isAutodetected,
isHidden: !!existingModelsMap[modelName]?.isHidden,
}))
}
@ -144,7 +151,7 @@ export const defaultProviderSettings = {
export type ProviderName = keyof typeof defaultProviderSettings
export const providerNames = Object.keys(defaultProviderSettings) as ProviderName[]
export const localProviderNames = ['ollama', 'openAICompatible'] satisfies ProviderName[] // all local names
export const localProviderNames = ['ollama'] satisfies ProviderName[] // all local names
export const nonlocalProviderNames = providerNames.filter((name) => !(localProviderNames as string[]).includes(name)) // all non-local names
type CustomSettingName = UnionOfKeys<typeof defaultProviderSettings[ProviderName]>
@ -177,6 +184,7 @@ export type SettingName = keyof SettingsForProvider<ProviderName>
type DisplayInfoForProviderName = {
title: string,
desc?: string,
}
export const displayInfoOfProviderName = (providerName: ProviderName): DisplayInfoForProviderName => {
@ -203,7 +211,7 @@ export const displayInfoOfProviderName = (providerName: ProviderName): DisplayIn
}
else if (providerName === 'openAICompatible') {
return {
title: 'Other',
title: 'OpenAI-Compatible',
}
}
else if (providerName === 'gemini') {
@ -256,7 +264,7 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
: providerName === 'openAICompatible' ? 'https://my-website.com/v1'
: '(never)',
subTextMd: providerName === 'ollama' ? 'Read about advanced [Endpoints here](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-can-i-expose-ollama-on-my-network).' :
subTextMd: providerName === 'ollama' ? 'If you would like to change this endpoint, please read more about [Endpoints here](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-can-i-expose-ollama-on-my-network).' :
undefined,
}
}
@ -389,26 +397,16 @@ export type RefreshableProviderName = typeof refreshableProviderNames[number]
export type FeatureFlagSettings = {
export type GlobalSettings = {
autoRefreshModels: boolean;
aiInstructions: string;
}
export const defaultFeatureFlagSettings: FeatureFlagSettings = {
export const defaultGlobalSettings: GlobalSettings = {
autoRefreshModels: true,
aiInstructions: '',
}
export type FeatureFlagName = keyof FeatureFlagSettings
export const featureFlagNames = Object.keys(defaultFeatureFlagSettings) as FeatureFlagName[]
type FeatureFlagDisplayInfo = {
description: string,
}
export const displayInfoOfFeatureFlag = (featureFlag: FeatureFlagName): FeatureFlagDisplayInfo => {
if (featureFlag === 'autoRefreshModels') {
return {
description: `Automatically detect local providers and models (${refreshableProviderNames.map(providerName => displayInfoOfProviderName(providerName).title).join(', ')}).`,
}
}
throw new Error(`featureFlagInfo: Unknown feature flag: "${featureFlag}"`)
}
export type GlobalSettingName = keyof GlobalSettings
export const globalSettingNames = Object.keys(defaultGlobalSettings) as GlobalSettingName[]

View file

@ -34,9 +34,9 @@ export const sendLLMMessage = ({
const captureChatEvent = (eventId: string, extras?: object) => {
metricsService.capture(eventId, {
providerName,
modelName,
numMessages: messages?.length,
messagesShape: messages?.map(msg => ({ role: msg.role, length: msg.content.length })),
version: '2024-11-14',
...extras,
})
}
@ -61,7 +61,6 @@ export const sendLLMMessage = ({
const onError: OnError = ({ message: error, fullError }) => {
if (_didAbort) return
console.log("ERROR!!!!!", error)
console.error('sendLLMMessage onError:', error)
captureChatEvent(`${loggingName} - Error`, { error })
onError_({ message: error, fullError })

View file

@ -4,6 +4,8 @@
*--------------------------------------------------------------------------------------*/
import { Disposable } from '../../../base/common/lifecycle.js';
import { isLinux, isMacintosh, isWindows } from '../../../base/common/platform.js';
import { IProductService } from '../../product/common/productService.js';
import { ITelemetryService } from '../../telemetry/common/telemetry.js';
import { IMetricsService } from '../common/metricsService.js';
@ -24,14 +26,20 @@ export class MetricsMainService extends Disposable implements IMetricsService {
readonly client: PostHog
constructor(
@ITelemetryService private readonly _telemetryService: ITelemetryService
@ITelemetryService private readonly _telemetryService: ITelemetryService,
@IProductService private readonly _productService: IProductService
) {
super()
this.client = new PostHog('phc_UanIdujHiLp55BkUTjB1AuBXcasVkdqRwgnwRlWESH2', { host: 'https://us.i.posthog.com', })
const { devDeviceId, firstSessionDate, machineId } = this._telemetryService
this._distinctId = devDeviceId
this.client.identify({ distinctId: devDeviceId, properties: { firstSessionDate, machineId } })
const { commit, version } = this._productService
const os = isWindows ? 'windows' : isMacintosh ? 'mac' : isLinux ? 'linux' : null
this.client.identify({ distinctId: this._distinctId, properties: { firstSessionDate, machineId, commit, version, os } })
console.log('Void posthog metrics info:', JSON.stringify({ devDeviceId, firstSessionDate, machineId }))
}

View file

@ -2534,7 +2534,7 @@ const LayoutStateKeys = {
// Part Sizing
GRID_SIZE: new InitializationStateKey('grid.size', StorageScope.PROFILE, StorageTarget.MACHINE, { width: 800, height: 600 }),
SIDEBAR_SIZE: new InitializationStateKey<number>('sideBar.size', StorageScope.PROFILE, StorageTarget.MACHINE, 200),
AUXILIARYBAR_SIZE: new InitializationStateKey<number>('auxiliaryBar.size', StorageScope.PROFILE, StorageTarget.MACHINE, 200),
AUXILIARYBAR_SIZE: new InitializationStateKey<number>('auxiliaryBar.size', StorageScope.PROFILE, StorageTarget.MACHINE, 800), // Void changed this from 200 to 800
PANEL_SIZE: new InitializationStateKey<number>('panel.size', StorageScope.PROFILE, StorageTarget.MACHINE, 300),
PANEL_LAST_NON_MAXIMIZED_HEIGHT: new RuntimeStateKey<number>('panel.lastNonMaximizedHeight', StorageScope.PROFILE, StorageTarget.MACHINE, 300),

View file

@ -45,7 +45,7 @@ export class AuxiliaryBarPart extends AbstractPaneCompositePart {
static readonly viewContainersWorkspaceStateKey = 'workbench.auxiliarybar.viewContainersWorkspaceState';
// Use the side bar dimensions
override readonly minimumWidth: number = 170;
override readonly minimumWidth: number = 230; // Void changed this (was 170)
override readonly maximumWidth: number = Number.POSITIVE_INFINITY;
override readonly minimumHeight: number = 0;
override readonly maximumHeight: number = Number.POSITIVE_INFINITY;

View file

@ -18,14 +18,13 @@ import { isRecentFolder, IWorkspacesService } from '../../../../platform/workspa
import { ICommandService } from '../../../../platform/commands/common/commands.js';
import { OpenFileFolderAction, OpenFolderAction } from '../../actions/workspaceActions.js';
import { isMacintosh, isNative, OS } from '../../../../base/common/platform.js';
import { VOID_CTRL_L_ACTION_ID } from '../../../contrib/void/browser/sidebarActions.js';
import { VOID_CTRL_K_ACTION_ID } from '../../../contrib/void/browser/quickEditActions.js';
import { defaultKeybindingLabelStyles } from '../../../../platform/theme/browser/defaultStyles.js';
import { IWindowOpenable } from '../../../../platform/window/common/window.js';
import { ILabelService, Verbosity } from '../../../../platform/label/common/label.js';
import { splitRecentLabel } from '../../../../base/common/labels.js';
import { IHostService } from '../../../services/host/browser/host.js';
import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../contrib/void/browser/voidSettingsPane.js';
import { VOID_CTRL_K_ACTION_ID, VOID_CTRL_L_ACTION_ID } from '../../../contrib/void/browser/actionIDs.js';
// import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
registerColor('editorWatermark.foreground', { dark: transparent(editorForeground, 0.6), light: transparent(editorForeground, 0.68), hcDark: editorForeground, hcLight: editorForeground }, localize('editorLineHighlight', 'Foreground color for the labels in the editor watermark.'));
@ -168,17 +167,17 @@ export class EditorGroupWatermark extends Disposable {
// .filter(entry => !!this.keybindingService.lookupKeybinding(entry.id));
this.clear();
const box = append(this.shortcuts, $('.watermark-box'));
const boxBelow = append(this.shortcuts, $(''))
boxBelow.style.display = 'flex'
boxBelow.style.flex = 'row'
boxBelow.style.justifyContent = 'center'
const voidIconBox = append(this.shortcuts, $('.watermark-box'));
const recentsBox = append(this.shortcuts, $('div'));
recentsBox.style.display = 'flex'
recentsBox.style.flex = 'row'
recentsBox.style.justifyContent = 'center'
const update = async () => {
clearNode(box);
clearNode(boxBelow);
clearNode(voidIconBox);
clearNode(recentsBox);
this.currentDisposables.forEach(label => label.dispose());
this.currentDisposables.clear();
@ -188,13 +187,14 @@ export class EditorGroupWatermark extends Disposable {
if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) {
// Open a folder
const button = h('button')
button.root.classList.add('void-watermark-button')
button.root.style.display = 'block'
button.root.style.marginLeft = 'auto'
button.root.style.marginRight = 'auto'
button.root.textContent = 'Open a folder'
button.root.onclick = () => {
const openFolderButton = h('button')
openFolderButton.root.classList.add('void-watermark-button')
openFolderButton.root.style.display = 'block'
openFolderButton.root.style.marginLeft = 'auto'
openFolderButton.root.style.marginRight = 'auto'
openFolderButton.root.style.marginBottom = '16px'
openFolderButton.root.textContent = 'Open a folder'
openFolderButton.root.onclick = () => {
this.commandService.executeCommand(isMacintosh && isNative ? OpenFileFolderAction.ID : OpenFolderAction.ID)
// if (this.contextKeyService.contextMatchesRules(ContextKeyExpr.and(WorkbenchStateContext.isEqualTo('workspace')))) {
// this.commandService.executeCommand(OpenFolderViaWorkspaceAction.ID);
@ -202,7 +202,7 @@ export class EditorGroupWatermark extends Disposable {
// this.commandService.executeCommand(isMacintosh ? 'workbench.action.files.openFileFolder' : 'workbench.action.files.openFolder');
// }
}
box.appendChild(button.root);
voidIconBox.appendChild(openFolderButton.root);
// Recents
@ -212,13 +212,8 @@ export class EditorGroupWatermark extends Disposable {
if (recentlyOpened.length !== 0) {
const span = $('div')
span.textContent = 'Recent'
span.style.fontWeight = '500'
box.append(span)
box.append(
...recentlyOpened.map(w => {
voidIconBox.append(
...recentlyOpened.map((w, i) => {
let fullPath: string;
let windowOpenable: IWindowOpenable;
@ -235,14 +230,13 @@ export class EditorGroupWatermark extends Disposable {
const { name, parentPath } = splitRecentLabel(fullPath);
const li = $('li');
const link = $('span');
link.classList.add('void-link')
const linkSpan = $('span');
linkSpan.classList.add('void-link')
linkSpan.style.display = 'flex'
linkSpan.style.gap = '4px'
linkSpan.style.padding = '8px'
link.innerText = name;
link.title = fullPath;
link.setAttribute('aria-label', localize('welcomePage.openFolderWithPath', "Open folder {0} with path {1}", name, parentPath));
link.addEventListener('click', e => {
linkSpan.addEventListener('click', e => {
this.hostService.openWindow([windowOpenable], {
forceNewWindow: e.ctrlKey || e.metaKey,
remoteAuthority: w.remoteAuthority || null // local window if remoteAuthority is not set or can not be deducted from the openable
@ -250,29 +244,30 @@ export class EditorGroupWatermark extends Disposable {
e.preventDefault();
e.stopPropagation();
});
li.appendChild(link);
const span = $('span');
span.style.paddingLeft = '4px';
span.classList.add('path');
span.classList.add('detail');
span.innerText = parentPath;
span.title = fullPath;
li.appendChild(span);
const nameSpan = $('span');
nameSpan.innerText = name;
nameSpan.title = fullPath;
linkSpan.appendChild(nameSpan);
return li
const dirSpan = $('span');
dirSpan.style.paddingLeft = '4px';
dirSpan.innerText = parentPath;
dirSpan.title = fullPath;
linkSpan.appendChild(dirSpan);
return linkSpan
}).filter(v => !!v)
)
}
}
else {
// show them Void keybindings
const keys = this.keybindingService.lookupKeybinding(VOID_CTRL_L_ACTION_ID);
const dl = append(box, $('dl'));
const dl = append(voidIconBox, $('dl'));
const dt = append(dl, $('dt'));
dt.textContent = 'Chat'
const dd = append(dl, $('dd'));
@ -283,7 +278,7 @@ export class EditorGroupWatermark extends Disposable {
const keys2 = this.keybindingService.lookupKeybinding(VOID_CTRL_K_ACTION_ID);
const dl2 = append(box, $('dl'));
const dl2 = append(voidIconBox, $('dl'));
const dt2 = append(dl2, $('dt'));
dt2.textContent = 'Quick Edit'
const dd2 = append(dl2, $('dd'));
@ -293,7 +288,7 @@ export class EditorGroupWatermark extends Disposable {
this.currentDisposables.add(label2);
const keys3 = this.keybindingService.lookupKeybinding('workbench.action.openGlobalKeybindings');
const button3 = append(boxBelow, $('button'));
const button3 = append(recentsBox, $('button'));
button3.textContent = 'Void Settings'
button3.style.display = 'block'
button3.style.marginLeft = 'auto'

View file

@ -0,0 +1,6 @@
// Normally you'd want to put these exports in the files that register them, but if you do that you'll get an import order error if you import them in certain cases.
// (importing them runs the whole file to get the ID, causing an import error). I guess it's best practice to separate out IDs, pretty annoying...
export const VOID_CTRL_L_ACTION_ID = 'void.ctrlLAction'
export const VOID_CTRL_K_ACTION_ID = 'void.ctrlKAction'

View file

@ -17,7 +17,7 @@ import { IEditorService } from '../../../services/editor/common/editorService.js
import { isCodeEditor } from '../../../../editor/browser/editorBrowser.js';
import { EditorResourceAccessor } from '../../../common/editor.js';
import { IModelService } from '../../../../editor/common/services/model.js';
import { extractCodeFromResult } from './helpers/extractCodeFromResult.js';
import { extractCodeFromRegular } from './helpers/extractCodeFromResult.js';
// The extension this was called from is here - https://github.com/voideditor/void/blob/autocomplete/extensions/void/src/extension/extension.ts
@ -652,7 +652,8 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
// newAutocompletion.abortRef = { current: () => { } }
newAutocompletion.status = 'finished'
// newAutocompletion.promise = undefined
newAutocompletion.insertText = postprocessResult(extractCodeFromResult(fullText))
const [text, _] = extractCodeFromRegular({ text: fullText, recentlyAddedTextLen: 0 })
newAutocompletion.insertText = postprocessResult(text)
resolve(newAutocompletion.insertText)
@ -662,7 +663,7 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
newAutocompletion.status = 'error'
reject(message)
},
featureName: 'Autocomplete',
useProviderFor: 'Autocomplete',
range: { startLineNumber: position.lineNumber, startColumn: position.column, endLineNumber: position.lineNumber, endColumn: position.column },
})
newAutocompletion.requestId = requestId

View file

@ -0,0 +1,302 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { Disposable } from '../../../../base/common/lifecycle.js';
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
import { URI } from '../../../../base/common/uri.js';
import { Emitter, Event } from '../../../../base/common/event.js';
import { IRange } from '../../../../editor/common/core/range.js';
import { ILLMMessageService } from '../../../../platform/void/common/llmMessageService.js';
import { IModelService } from '../../../../editor/common/services/model.js';
import { VSReadFile } from './helpers/readFile.js';
import { chat_prompt, chat_systemMessage } from './prompt/prompts.js';
export type CodeSelection = {
fileURI: URI;
selectionStr: string | null;
content: string; // TODO remove this (replace `selectionStr` with `content`)
range: IRange;
}
// if selectionStr is null, it means to use the entire file at send time
export type CodeStagingSelection = {
type: 'Selection',
fileURI: URI,
selectionStr: string,
range: IRange
} | {
type: 'File',
fileURI: URI,
selectionStr: null,
range: null
}
// WARNING: changing this format is a big deal!!!!!! need to migrate old format to new format on users' computers so people don't get errors.
export type ChatMessage =
| {
role: 'user';
content: string | null; // content sent to the llm - allowed to be '', will be replaced with (empty)
displayContent: string | null; // content displayed to user - allowed to be '', will be ignored
selections: CodeSelection[] | null; // the user's selection
}
| {
role: 'assistant';
content: string | null; // content received from LLM - allowed to be '', will be replaced with (empty)
displayContent: string | null; // content displayed to user (this is the same as content for now) - allowed to be '', will be ignored
}
| {
role: 'system';
content: string;
displayContent?: undefined;
}
// a 'thread' means a chat message history
export type ChatThreads = {
[id: string]: {
id: string; // store the id here too
createdAt: string; // ISO string
lastModified: string; // ISO string
messages: ChatMessage[];
};
}
export type ThreadsState = {
allThreads: ChatThreads;
currentThreadId: string; // intended for internal use only
currentStagingSelections: CodeStagingSelection[] | null;
}
export type ThreadStreamState = {
[threadId: string]: undefined | {
streamingToken?: string;
error?: { message: string, fullError: Error | null };
messageSoFar?: string;
}
}
const newThreadObject = () => {
const now = new Date().toISOString()
return {
id: new Date().getTime().toString(),
createdAt: now,
lastModified: now,
messages: [],
} satisfies ChatThreads[string]
}
const THREAD_STORAGE_KEY = 'void.chatThreadStorage'
export interface IChatThreadService {
readonly _serviceBrand: undefined;
readonly state: ThreadsState;
readonly streamState: ThreadStreamState;
onDidChangeCurrentThread: Event<void>;
onDidChangeStreamState: Event<{ threadId: string }>
getCurrentThread(): ChatThreads[string];
openNewThread(): void;
switchToThread(threadId: string): void;
setStaging(stagingSelection: CodeStagingSelection[] | null): void;
addUserMessageAndStreamResponse(userMessage: string): Promise<void>;
cancelStreaming(threadId: string): void;
dismissStreamError(threadId: string): void;
}
export const IChatThreadService = createDecorator<IChatThreadService>('voidChatThreadService');
class ChatThreadService extends Disposable implements IChatThreadService {
_serviceBrand: undefined;
// this fires when the current thread changes at all (a switch of currentThread, or a message added to it, etc)
private readonly _onDidChangeCurrentThread = new Emitter<void>();
readonly onDidChangeCurrentThread: Event<void> = this._onDidChangeCurrentThread.event;
readonly streamState: ThreadStreamState = {}
private readonly _onDidChangeStreamState = new Emitter<{ threadId: string }>();
readonly onDidChangeStreamState: Event<{ threadId: string }> = this._onDidChangeStreamState.event;
state: ThreadsState // allThreads is persisted, currentThread is not
constructor(
@IStorageService private readonly _storageService: IStorageService,
@IModelService private readonly _modelService: IModelService,
@ILLMMessageService private readonly _llmMessageService: ILLMMessageService,
) {
super()
this.state = {
allThreads: this._readAllThreads(),
currentThreadId: null as unknown as string, // gets set in startNewThread()
currentStagingSelections: null,
}
// always be in a thread
this.openNewThread()
}
private _readAllThreads(): ChatThreads {
// PUT ANY VERSION CHANGE FORMAT CONVERSION CODE HERE
const threads = this._storageService.get(THREAD_STORAGE_KEY, StorageScope.APPLICATION)
return threads ? JSON.parse(threads) : {}
}
private _storeAllThreads(threads: ChatThreads) {
this._storageService.store(THREAD_STORAGE_KEY, JSON.stringify(threads), StorageScope.APPLICATION, StorageTarget.USER)
}
// this should be the only place this.state = ... appears besides constructor
private _setState(state: Partial<ThreadsState>, affectsCurrent: boolean) {
this.state = {
...this.state,
...state
}
if (affectsCurrent)
this._onDidChangeCurrentThread.fire()
}
private _setStreamState(threadId: string, state: Partial<NonNullable<ThreadStreamState[string]>>) {
this.streamState[threadId] = {
...this.streamState[threadId],
...state
}
this._onDidChangeStreamState.fire({ threadId })
}
// ---------- streaming ----------
async addUserMessageAndStreamResponse(userMessage: string) {
const threadId = this.getCurrentThread().id
const currSelns = this.state.currentStagingSelections ?? []
const selections = !currSelns ? null : await Promise.all(
currSelns.map(async (sel) => ({ ...sel, content: await VSReadFile(this._modelService, sel.fileURI) }))
).then(
(files) => files.filter(file => file.content !== null) as CodeSelection[]
)
// add user's message to chat history
const instructions = userMessage
const userHistoryElt: ChatMessage = { role: 'user', content: chat_prompt(instructions, selections), displayContent: instructions, selections: selections }
this._addMessageToThread(threadId, userHistoryElt)
const onDone = (content: string, error?: { message: string, fullError: Error | null }) => {
// add assistant's message to chat history, and clear selection
const assistantHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content || null }
this._addMessageToThread(threadId, assistantHistoryElt)
this._setStreamState(threadId, { messageSoFar: undefined, streamingToken: undefined, error })
}
this._setStreamState(threadId, { error: undefined })
const llmCancelToken = this._llmMessageService.sendLLMMessage({
logging: { loggingName: 'Chat' },
messages: [
{ role: 'system', content: chat_systemMessage },
...this.getCurrentThread().messages.map(m => ({ role: m.role, content: m.content || '(null)' })),
],
onText: ({ newText, fullText }) => {
this._setStreamState(threadId, { messageSoFar: fullText })
},
onFinalMessage: ({ fullText: content }) => {
onDone(content)
},
onError: (error) => {
console.log('Void Chat Error:', error)
onDone(this.streamState[threadId]?.messageSoFar ?? '', error)
},
useProviderFor: 'Ctrl+L',
})
if (llmCancelToken === null) return
this._setStreamState(threadId, { streamingToken: llmCancelToken })
}
cancelStreaming(threadId: string) {
const llmCancelToken = this.streamState[threadId]?.streamingToken
if (llmCancelToken) this._llmMessageService.abort(llmCancelToken)
this._setStreamState(threadId, { streamingToken: undefined })
}
dismissStreamError(threadId: string): void {
this._setStreamState(threadId, { error: undefined })
}
// ---------- the rest ----------
getCurrentThread(): ChatThreads[string] {
const state = this.state
return state.allThreads[state.currentThreadId];
}
switchToThread(threadId: string) {
// console.log('threadId', threadId)
// console.log('messages', this.state.allThreads[threadId].messages)
this._setState({ currentThreadId: threadId }, true)
}
openNewThread() {
// if a thread with 0 messages already exists, switch to it
const { allThreads: currentThreads } = this.state
for (const threadId in currentThreads) {
if (currentThreads[threadId].messages.length === 0) {
this.switchToThread(threadId)
return
}
}
// otherwise, start a new thread
const newThread = newThreadObject()
// update state
const newThreads: ChatThreads = {
...currentThreads,
[newThread.id]: newThread
}
this._storeAllThreads(newThreads)
this._setState({ allThreads: newThreads, currentThreadId: newThread.id }, true)
}
_addMessageToThread(threadId: string, message: ChatMessage) {
const { allThreads } = this.state
const oldThread = allThreads[threadId]
// update state and store it
const newThreads = {
...allThreads,
[oldThread.id]: {
...oldThread,
lastModified: new Date().toISOString(),
messages: [...oldThread.messages, message],
}
}
this._storeAllThreads(newThreads)
this._setState({ allThreads: newThreads }, true) // the current thread just changed (it had a message added to it)
}
setStaging(stagingSelection: CodeStagingSelection[] | null): void {
this._setState({ currentStagingSelections: stagingSelection }, true) // this is a hack for now
}
}
registerSingleton(IChatThreadService, ChatThreadService, InstantiationType.Eager);

View file

@ -80,6 +80,7 @@ export class ConsistentItemService extends Disposable {
}
const initializeEditor = (editor: ICodeEditor) => {
if (editor.getModel()?.uri.scheme !== 'file') return
addTabSwitchListeners(editor)
addDisposeListener(editor)
putItemsOnEditor(editor, editor.getModel()?.uri ?? null)
@ -126,6 +127,8 @@ 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]
@ -157,7 +160,6 @@ export class ConsistentItemService extends Disposable {
removeConsistentItemFromURI(consistentItemId: string) {
if (!(consistentItemId in this.infoOfConsistentItemId))
return
@ -173,6 +175,9 @@ 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]
}

View file

@ -0,0 +1,170 @@
// eg "bash" -> "shell"
export const nameToVscodeLanguage: { [key: string]: string } = {
// Web Technologies
'html': 'html',
'css': 'css',
'scss': 'scss',
'sass': 'scss',
'less': 'less',
'javascript': 'typescript',
'js': 'typescript', // use more general renderer
'jsx': 'typescript',
'typescript': 'typescript',
'ts': 'typescript',
'tsx': 'typescript',
'json': 'json',
'jsonc': 'json',
// Programming Languages
'python': 'python',
'py': 'python',
'java': 'java',
'cpp': 'cpp',
'c++': 'cpp',
'c': 'c',
'csharp': 'csharp',
'cs': 'csharp',
'c#': 'csharp',
'go': 'go',
'golang': 'go',
'rust': 'rust',
'rs': 'rust',
'ruby': 'ruby',
'rb': 'ruby',
'php': 'php',
'shell': 'shell',
'bash': 'shell',
'sh': 'shell',
'zsh': 'shell',
// Markup and Config
'markdown': 'markdown',
'md': 'markdown',
'xml': 'xml',
'svg': 'xml',
'yaml': 'yaml',
'yml': 'yaml',
'ini': 'ini',
'toml': 'ini',
// Database and Query Languages
'sql': 'sql',
'mysql': 'sql',
'postgresql': 'sql',
'graphql': 'graphql',
'gql': 'graphql',
// Others
'dockerfile': 'dockerfile',
'docker': 'dockerfile',
'makefile': 'makefile',
'plaintext': 'plaintext',
'text': 'plaintext'
};
// eg ".ts" -> "typescript"
const fileExtensionToVscodeLanguage: { [key: string]: string } = {
// Web
'html': 'html',
'htm': 'html',
'css': 'css',
'scss': 'scss',
'less': 'less',
'js': 'javascript',
'jsx': 'javascript',
'ts': 'typescript',
'tsx': 'typescript',
'json': 'json',
'jsonc': 'json',
// Programming Languages
'py': 'python',
'java': 'java',
'cpp': 'cpp',
'cc': 'cpp',
'c': 'c',
'h': 'cpp',
'hpp': 'cpp',
'cs': 'csharp',
'go': 'go',
'rs': 'rust',
'rb': 'ruby',
'php': 'php',
'sh': 'shell',
'bash': 'shell',
'zsh': 'shell',
// Markup/Config
'md': 'markdown',
'markdown': 'markdown',
'xml': 'xml',
'svg': 'xml',
'yaml': 'yaml',
'yml': 'yaml',
'ini': 'ini',
'toml': 'ini',
// Other
'sql': 'sql',
'graphql': 'graphql',
'gql': 'graphql',
'dockerfile': 'dockerfile',
'docker': 'dockerfile',
'mk': 'makefile',
// Config Files and Dot Files
'npmrc': 'ini',
'env': 'ini',
'gitignore': 'ignore',
'dockerignore': 'ignore',
'eslintrc': 'json',
'babelrc': 'json',
'prettierrc': 'json',
'stylelintrc': 'json',
'editorconfig': 'ini',
'htaccess': 'apacheconf',
'conf': 'ini',
'config': 'ini',
// Package Files
'package': 'json',
'package-lock': 'json',
'gemfile': 'ruby',
'podfile': 'ruby',
'rakefile': 'ruby',
// Build Systems
'cmake': 'cmake',
'makefile': 'makefile',
'gradle': 'groovy',
// Shell Scripts
'bashrc': 'shell',
'zshrc': 'shell',
'fish': 'shell',
// Version Control
'gitconfig': 'ini',
'hgrc': 'ini',
'svnconfig': 'ini',
// Web Server
'nginx': 'nginx',
// Misc Config
'properties': 'properties',
'cfg': 'ini',
'reg': 'ini'
};
export function filenameToVscodeLanguage(filename: string): string | undefined {
const ext = filename.toLowerCase().split('.').pop();
if (!ext) return undefined;
return fileExtensionToVscodeLanguage[ext];
}

View file

@ -3,16 +3,167 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
export const extractCodeFromResult = (result: string) => {
class SurroundingsRemover {
readonly originalS: string
i: number
j: number
// string is s[i...j]
constructor(s: string) {
this.originalS = s
this.i = 0
this.j = s.length - 1
}
value() {
return this.originalS.substring(this.i, this.j + 1)
}
// returns whether it removed the whole prefix
removePrefix = (prefix: string): boolean => {
let offset = 0
// console.log('prefix', prefix, Math.min(this.j, prefix.length - 1))
while (this.i <= this.j && offset <= prefix.length - 1) {
if (this.originalS.charAt(this.i) !== prefix.charAt(offset))
break
offset += 1
this.i += 1
}
return offset === prefix.length
}
// // removes suffix from right to left
removeSuffix = (suffix: string): boolean => {
// e.g. suffix = <PRE/>, the string is <PRE>hi<P
const s = this.value()
// for every possible prefix of `suffix`, check if string ends with it
for (let len = Math.min(s.length, suffix.length); len >= 1; len -= 1) {
if (s.endsWith(suffix.substring(0, len))) { // the end of the string equals a prefix
this.j -= len
return len === suffix.length
}
}
return false
}
// removeSuffix = (suffix: string): boolean => {
// let offset = 0
// while (this.j >= Math.max(this.i, 0)) {
// if (this.originalS.charAt(this.j) !== suffix.charAt(suffix.length - 1 - offset))
// break
// offset += 1
// this.j -= 1
// }
// return offset === suffix.length
// }
removeFromStartUntil = (until: string, alsoRemoveUntilStr: boolean) => {
const index = this.originalS.indexOf(until, this.i)
if (index === -1) {
this.i = this.j + 1
return false
}
// console.log('index', index, until.length)
if (alsoRemoveUntilStr)
this.i = index + until.length
else
this.i = index
return true
}
removeCodeBlock = () => {
const pm = this
const foundCodeBlock = pm.removePrefix('```')
if (!foundCodeBlock) return false
pm.removeFromStartUntil('\n', true) // language
const foundCodeBlockEnd = pm.removeSuffix('```')
if (!foundCodeBlockEnd) return false
pm.removeSuffix('\n')
return true
}
actualRecentlyAdded = (recentlyAddedTextLen: number) => {
// aaaaaatextaaaaaa{recentlyAdded}
// i ^ j
// |
// recentyAddedIdx
const recentlyAddedIdx = this.j - recentlyAddedTextLen + 1
return this.originalS.substring(Math.max(this.i, recentlyAddedIdx), this.j + 1)
}
}
export const extractCodeFromRegular = ({ text, recentlyAddedTextLen }: { text: string, recentlyAddedTextLen: number }): [string, string] => {
// Match either:
// 1. ```language\n<code>```
// 2. ```<code>```
const match = result.match(/```(?:\w+\n)?([\s\S]*?)```|```([\s\S]*?)```/);
if (!match) {
return result;
}
const pm = new SurroundingsRemover(text)
// Return whichever group matched (non-empty)
return match[1] ?? match[2] ?? result;
pm.removeCodeBlock()
const s = pm.value()
const actual = pm.actualRecentlyAdded(recentlyAddedTextLen)
return [s, actual]
}
// Ollama has its own FIM, we should not use this if we use that
export const extractCodeFromFIM = ({ text, recentlyAddedTextLen, midTag, }: { text: string, recentlyAddedTextLen: number, midTag: string }): [string, string] => {
/* ------------- summary of the regex -------------
[optional ` | `` | ```]
(match optional_language_name)
[optional strings here]
[required <MID> tag]
(match the stuff between mid tags)
[optional <MID/> tag]
[optional ` | `` | ```]
*/
const pm = new SurroundingsRemover(text)
pm.removeCodeBlock()
const foundMid = pm.removePrefix(`<${midTag}>`)
if (foundMid) {
pm.removeSuffix(`</${midTag}>`)
}
const s = pm.value()
const actual = pm.actualRecentlyAdded(recentlyAddedTextLen)
return [s, actual]
// // const regex = /[\s\S]*?(?:`{1,3}\s*([a-zA-Z_]+[\w]*)?[\s\S]*?)?<MID>([\s\S]*?)(?:<\/MID>|`{1,3}|$)/;
// const regex = new RegExp(
// `[\\s\\S]*?(?:\`{1,3}\\s*([a-zA-Z_]+[\\w]*)?[\\s\\S]*?)?<${midTag}>([\\s\\S]*?)(?:</${midTag}>|\`{1,3}|$)`,
// ''
// );
// const match = text.match(regex);
// if (match) {
// const [_, languageName, codeBetweenMidTags] = match;
// return [languageName, codeBetweenMidTags] as const
// } else {
// return [undefined, extractCodeFromRegular(text)] as const
// }
}

View file

@ -1,20 +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 { isMacintosh } from '../../../../../base/common/platform.js';
// import { OperatingSystem, OS } from '../../../../base/common/platform.js';
// OS === OperatingSystem.Macintosh
export function getCmdKey(): string {
if (isMacintosh) {
return '⌘';
} else {
return 'Ctrl';
}
}

View file

@ -0,0 +1,10 @@
import { URI } from '../../../../../base/common/uri'
import { EndOfLinePreference } from '../../../../../editor/common/model'
import { IModelService } from '../../../../../editor/common/services/model.js'
// read files from VSCode
export const VSReadFile = async (modelService: IModelService, uri: URI): Promise<string | null> => {
const model = modelService.getModel(uri)
if (!model) return null
return model.getValue(EndOfLinePreference.LF)
}

File diff suppressed because it is too large Load diff

View file

@ -70,4 +70,99 @@
.void-link {
color: #3b82f6;
cursor: pointer;
transition: all 0.2s ease;
}
.void-link:hover {
opacity: 80%;
}
.void-scrollable-element::-webkit-scrollbar,
.void-scrollable-element *::-webkit-scrollbar {
width: 14px !important;
height: 14px !important;
}
.void-scrollable-element::-webkit-scrollbar-track,
.void-scrollable-element *::-webkit-scrollbar-track {
background: transparent !important;
}
.void-scrollable-element::-webkit-scrollbar-thumb,
.void-scrollable-element *::-webkit-scrollbar-thumb {
background-color: transparent !important;
border-radius: 0px !important;
}
.void-scrollable-element::-webkit-scrollbar-thumb:hover,
.void-scrollable-element *::-webkit-scrollbar-thumb:hover {
background-color: var(--vscode-scrollbarSlider-hoverBackground) !important;
}
.void-scrollable-element::-webkit-scrollbar-thumb:active,
.void-scrollable-element *::-webkit-scrollbar-thumb:active {
background-color: var(--vscode-scrollbarSlider-activeBackground) !important;
}
.void-scrollable-element::-webkit-scrollbar-corner,
.void-scrollable-element *::-webkit-scrollbar-corner {
background-color: transparent !important;
}
.void-scrollable-element.show-scrollbar-0::-webkit-scrollbar-thumb,
.void-scrollable-element.show-scrollbar-0 *::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 0%, transparent) !important;
}
.void-scrollable-element.show-scrollbar-1::-webkit-scrollbar-thumb,
.void-scrollable-element.show-scrollbar-1 *::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 10%, transparent) !important;
}
.void-scrollable-element.show-scrollbar-2::-webkit-scrollbar-thumb,
.void-scrollable-element.show-scrollbar-2 *::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 20%, transparent) !important;
}
.void-scrollable-element.show-scrollbar-3::-webkit-scrollbar-thumb,
.void-scrollable-element.show-scrollbar-3 *::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 30%, transparent) !important;
}
.void-scrollable-element.show-scrollbar-4::-webkit-scrollbar-thumb,
.void-scrollable-element.show-scrollbar-4 *::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 40%, transparent) !important;
}
.void-scrollable-element.show-scrollbar-5::-webkit-scrollbar-thumb,
.void-scrollable-element.show-scrollbar-5 *::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 50%, transparent) !important;
}
.void-scrollable-element.show-scrollbar-6::-webkit-scrollbar-thumb,
.void-scrollable-element.show-scrollbar-6 *::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 60%, transparent) !important;
}
.void-scrollable-element.show-scrollbar-7::-webkit-scrollbar-thumb,
.void-scrollable-element.show-scrollbar-7 *::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 70%, transparent) !important;
}
.void-scrollable-element.show-scrollbar-8::-webkit-scrollbar-thumb,
.void-scrollable-element.show-scrollbar-8 *::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 80%, transparent) !important;
}
.void-scrollable-element.show-scrollbar-9::-webkit-scrollbar-thumb,
.void-scrollable-element.show-scrollbar-9 *::-webkit-scrollbar-thumb {
background-color: color-mix(in srgb, var(--vscode-scrollbarSlider-background) 90%, transparent) !important;
}
.void-scrollable-element.show-scrollbar-10::-webkit-scrollbar-thumb,
.void-scrollable-element.show-scrollbar-10 *::-webkit-scrollbar-thumb {
background-color: var(--vscode-scrollbarSlider-background) !important;
}

View file

@ -4,7 +4,9 @@
*--------------------------------------------------------------------------------------*/
import { CodeSelection } from '../threadHistoryService.js';
import { URI } from '../../../../../base/common/uri.js';
import { filenameToVscodeLanguage } from '../helpers/detectLanguage.js';
import { CodeSelection } from '../chatThreadService.js';
export const chat_systemMessage = `\
You are a coding assistant. You are given a list of relevant files \`files\`, a selection that the user is making \`selection\`, and instructions to follow \`instructions\`.
@ -22,25 +24,25 @@ Instructions:
FILES
selected file \`math.ts\`:
\`\`\`
\`\`\` typescript
const addNumbers = (a, b) => a + b
const subtractNumbers = (a, b) => a - b
const divideNumbers = (a, b) => a / b
\`\`\`
SELECTION
\`\`\`
\`\`\` typescript
const subtractNumbers = (a, b) => a - b
\`\`\`
INSTRUCTIONS
\`\`\`
\`\`\` typescript
add a function that multiplies numbers below this
\`\`\`
EXPECTED OUTPUT
We can add the following code to the file:
\`\`\`
\`\`\` typescript
// existing code...
const subtractNumbers = (a, b) => a - b;
const multiplyNumbers = (a, b) => a * b;
@ -51,7 +53,7 @@ const multiplyNumbers = (a, b) => a * b;
FILES
selected file \`fib.ts\`:
\`\`\`
\`\`\` typescript
const dfs = (root) => {
if (!root) return;
@ -66,18 +68,18 @@ const fib = (n) => {
\`\`\`
SELECTION
\`\`\`
\`\`\` typescript
return fib(n - 1) + fib(n - 2)
\`\`\`
INSTRUCTIONS
\`\`\`
\`\`\` typescript
memoize results
\`\`\`
EXPECTED OUTPUT
To implement memoization in your Fibonacci function, you can use a JavaScript object to store previously computed results. This will help avoid redundant calculations and improve performance. Here's how you can modify your function:
\`\`\`
\`\`\` typescript
// existing code...
const fib = (n, memo = {}) => {
if (n < 1) return 1;
@ -100,7 +102,7 @@ const stringifySelections = (selections: CodeSelection[]) => {
return selections.map(({ fileURI, content, selectionStr }) =>
`\
File: ${fileURI.fsPath}
\`\`\`
\`\`\` ${filenameToVscodeLanguage(fileURI.fsPath) ?? ''}
${content // this was the enite file which is foolish
}
\`\`\`${selectionStr === null ? '' : `
@ -136,7 +138,7 @@ Directions:
ORIGINAL_FILE
\`Sidebar.tsx\`:
\`\`\`
\`\`\` typescript
import React from 'react';
import styles from './Sidebar.module.css';
@ -172,7 +174,7 @@ export default Sidebar;
\`\`\`
DIFF
\`\`\`
\`\`\` typescript
@@ ... @@
-<div className={styles.sidebar}>
-<ul>
@ -211,7 +213,7 @@ DIFF
\`\`\`
NEW_FILE
\`\`\`
\`\`\` typescript
import React from 'react';
import styles from './Sidebar.module.css';
@ -226,7 +228,7 @@ const Sidebar: React.FC<SidebarProps> = ({ items, onItemSelect, onExtraButtonCli
\`\`\`
COMPLETION
\`\`\`
\`\`\` typescript
<div className={styles.sidebar}>
<ul>
{items.map((item, index) => (
@ -253,10 +255,13 @@ export default Sidebar;\`\`\`
export const ctrlLStream_prompt = ({ originalCode, userMessage }: { originalCode: string, userMessage: string }) => {
export const ctrlLStream_prompt = ({ originalCode, userMessage, uri }: { originalCode: string, userMessage: string, uri: URI }) => {
const language = filenameToVscodeLanguage(uri.fsPath) ?? ''
return `\
ORIGINAL_CODE
\`\`\`
\`\`\` ${language}
${originalCode}
\`\`\`
@ -281,7 +286,7 @@ export const ctrlKStream_prefixAndSuffix = ({ fullFileStr, startLine, endLine }:
const fullFileLines = fullFileStr.split('\n')
// we can optimize this later
const MAX_CHARS = 1024
const MAX_PREFIX_SUFFIX_CHARS = 20_000
/*
a
@ -302,7 +307,7 @@ export const ctrlKStream_prefixAndSuffix = ({ fullFileStr, startLine, endLine }:
// we'll include fullFileLines[i...(startLine-1)-1].join('\n') in the prefix.
while (i !== 0) {
const newLine = fullFileLines[i - 1]
if (newLine.length + 1 + prefix.length <= MAX_CHARS) { // +1 to include the \n
if (newLine.length + 1 + prefix.length <= MAX_PREFIX_SUFFIX_CHARS) { // +1 to include the \n
prefix = `${newLine}\n${prefix}`
i -= 1
}
@ -313,7 +318,7 @@ export const ctrlKStream_prefixAndSuffix = ({ fullFileStr, startLine, endLine }:
let j = endLine - 1
while (j !== fullFileLines.length - 1) {
const newLine = fullFileLines[j + 1]
if (newLine.length + 1 + suffix.length <= MAX_CHARS) { // +1 to include the \n
if (newLine.length + 1 + suffix.length <= MAX_PREFIX_SUFFIX_CHARS) { // +1 to include the \n
suffix = `${suffix}\n${newLine}`
j += 1
}
@ -324,50 +329,54 @@ export const ctrlKStream_prefixAndSuffix = ({ fullFileStr, startLine, endLine }:
}
export const ctrlKStream_prompt = ({ selection, prefix, suffix, userMessage }: { selection: string, prefix: string, suffix: string, userMessage: string, }) => {
const onlySpeaksFIM = false
if (onlySpeaksFIM) {
const preTag = 'PRE'
const sufTag = 'SUF'
const midTag = 'MID'
return `\
<${preTag}>
/* Original Selection:
${selection}*/
/* Instructions:
${userMessage}*/
${prefix}</${preTag}>
<${sufTag}>${suffix}</${sufTag}>
<${midTag}>`
}
// prompt the model on how to do FIM
else {
const preTag = 'PRE'
const sufTag = 'SUF'
const midTag = 'MID'
return `\
Here is the user's original selection:
\`\`\`
export type FimTagsType = {
preTag: string,
sufTag: string,
midTag: string
}
export const defaultFimTags: FimTagsType = {
preTag: 'BEFORE',
sufTag: 'AFTER',
midTag: 'SELECTION',
}
export const ctrlKStream_prompt = ({ selection, prefix, suffix, userMessage, fimTags, isOllamaFIM, language }:
{
selection: string, prefix: string, suffix: string, userMessage: string, fimTags: FimTagsType, language: string,
isOllamaFIM: false, // we require this be false for clarity
}) => {
const { preTag, sufTag, midTag } = fimTags
// prompt the model artifically on how to do FIM
// const preTag = 'BEFORE'
// const sufTag = 'AFTER'
// const midTag = 'SELECTION'
return `\
The user is selecting this code as their SELECTION:
\`\`\` ${language}
<${midTag}>${selection}</${midTag}>
\`\`\`
The user wants to apply the following instructions to the selection:
The user wants to apply the following INSTRUCTIONS to the SELECTION:
${userMessage}
Please rewrite the selection following the user's instructions.
Please edit the SELECTION following the user's INSTRUCTIONS, and return the edited selection.
Instructions to follow:
1. Follow the user's instructions
2. You may ONLY CHANGE the selection, and nothing else in the file
3. Make sure all brackets in the new selection are balanced the same was as in the original selection
3. Be careful not to duplicate or remove variables, comments, or other syntax by mistake
Note that the SELECTION has code that comes before it. This code is indicated with <${preTag}>...before</${preTag}>.
Note also that the SELECTION has code that comes after it. This code is indicated with <${sufTag}>...after</${sufTag}>.
Complete the following:
Instructions:
1. Your OUTPUT should be a SINGLE PIECE OF CODE of the form <${midTag}>...new_selection</${midTag}>. Do NOT output any text or explanations before or after this.
2. You may ONLY CHANGE the original SELECTION, and NOT the content in the <${preTag}>...</${preTag}> or <${sufTag}>...</${sufTag}> tags.
3. Make sure all brackets in the new selection are balanced the same as in the original selection.
4. Be careful not to duplicate or remove variables, comments, or other syntax by mistake.
Given the code:
<${preTag}>${prefix}</${preTag}>
<${sufTag}>${suffix}</${sufTag}>
<${midTag}>`
}
Return only the completion block of code (of the form \`\`\` ${language}\n <${midTag}>...new_selection</${midTag}>\`\`\`):`
};

View file

@ -10,14 +10,16 @@ import { KeybindingWeight } from '../../../../platform/keybinding/common/keybind
import { IMetricsService } from '../../../../platform/void/common/metricsService.js';
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
import { IInlineDiffsService } from './inlineDiffsService.js';
import { InputBox } from '../../../../base/browser/ui/inputbox/inputBox.js';
import { roundRangeToLines } from './sidebarActions.js';
import { VOID_CTRL_K_ACTION_ID } from './actionIDs.js';
export type QuickEditPropsType = {
diffareaid: number,
onGetInputBox: (i: InputBox) => void;
initStreamingDiffZoneId: number | null,
textAreaRef: (ref: HTMLTextAreaElement | null) => void;
onChangeHeight: (height: number) => void;
onUserUpdateText: (text: string) => void;
onChangeText: (text: string) => void;
initText: string | null;
}
@ -30,7 +32,6 @@ export type QuickEdit = {
}
export const VOID_CTRL_K_ACTION_ID = 'void.ctrlKAction'
registerAction2(class extends Action2 {
constructor(
) {
@ -48,21 +49,18 @@ registerAction2(class extends Action2 {
const editorService = accessor.get(ICodeEditorService)
const metricsService = accessor.get(IMetricsService)
metricsService.capture('User Action', { type: 'Open Ctrl+K' })
metricsService.capture('Ctrl+K', {})
const editor = editorService.getActiveCodeEditor()
if (!editor) return;
const model = editor.getModel()
if (!model) return;
const selection = editor.getSelection()
const selection = roundRangeToLines(editor.getSelection(), { emptySelectionBehavior: 'line' })
if (!selection) return;
const { startLineNumber: startLine, endLineNumber: endLine } = selection
// deselect - clear selection
editor.setSelection({ startLineNumber: startLine, endLineNumber: startLine, startColumn: 1, endColumn: 1 })
const inlineDiffsService = accessor.get(IInlineDiffsService)
inlineDiffsService.addCtrlKZone({ startLine, endLine, editor })
}

View file

@ -73,7 +73,7 @@ function saveStylesFile() {
} catch (err) {
console.error('[scope-tailwind] Error saving styles.css:', err);
}
}, 5000);
}, 3000);
}
const args = process.argv.slice(2);

View file

@ -1,177 +0,0 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import React, { FormEvent, useCallback, useEffect, useRef, useState } from 'react';
import { useSettingsState, useSidebarState, useThreadsState, useQuickEditState, useAccessor } from '../util/services.js';
import { OnError } from '../../../../../../../platform/void/common/llmMessageTypes.js';
import { InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js';
import { getCmdKey } from '../../../helpers/getCmdKey.js';
import { VoidInputBox } from '../util/inputs.js';
import { QuickEditPropsType } from '../../../quickEditActions.js';
import { ButtonStop, ButtonSubmit } from '../sidebar-tsx/SidebarChat.js';
import { ModelDropdown } from '../void-settings-tsx/ModelDropdown.js';
import { X } from 'lucide-react';
export const CtrlKChat = ({ diffareaid, onGetInputBox, onUserUpdateText, onChangeHeight, initText }: QuickEditPropsType) => {
const accessor = useAccessor()
const inlineDiffsService = accessor.get('IInlineDiffsService')
const sizerRef = useRef<HTMLDivElement | null>(null)
const inputBoxRef: React.MutableRefObject<InputBox | null> = useRef(null);
useEffect(() => {
const inputContainer = sizerRef.current
if (!inputContainer) return;
// only observing 1 element
let resizeObserver: ResizeObserver | undefined
resizeObserver = new ResizeObserver((entries) => {
const height = entries[0].borderBoxSize[0].blockSize
onChangeHeight(height)
})
resizeObserver.observe(inputContainer);
return () => { resizeObserver?.disconnect(); };
}, [onChangeHeight]);
// state of current message
const [instructions, setInstructions] = useState(initText ?? '') // the user's instructions
const onChangeText = useCallback((newStr: string) => {
setInstructions(newStr)
onUserUpdateText(newStr)
}, [setInstructions])
const isDisabled = !instructions.trim()
const currentlyStreamingIdRef = useRef<number | undefined>(undefined)
const [isStreaming, setIsStreaming] = useState(false)
const onSubmit = useCallback((e: FormEvent) => {
if (currentlyStreamingIdRef.current !== undefined) return
inputBoxRef.current?.disable()
currentlyStreamingIdRef.current = inlineDiffsService.startApplying({
featureName: 'Ctrl+K',
diffareaid: diffareaid,
userMessage: instructions,
})
setIsStreaming(true)
}, [inlineDiffsService, diffareaid, instructions])
const onInterrupt = useCallback(() => {
if (currentlyStreamingIdRef.current !== undefined)
inlineDiffsService.interruptStreaming(currentlyStreamingIdRef.current)
inputBoxRef.current?.enable()
setIsStreaming(false)
}, [inlineDiffsService])
// sync init value
const alreadySetRef = useRef(false)
useEffect(() => {
if (!inputBoxRef.current) return
if (alreadySetRef.current) return
alreadySetRef.current = true
inputBoxRef.current.value = instructions
}, [initText, instructions])
return <div ref={sizerRef} className='py-2 w-full max-w-xl'>
<form
// copied from SidebarChat.tsx
className={`
flex flex-col gap-2 py-1 px-2 relative input text-left shrink-0
transition-all duration-200
rounded-md
bg-vscode-input-bg
border border-vscode-commandcenter-inactive-border focus-within:border-vscode-commandcenter-active-border hover:border-vscode-commandcenter-active-border
`
}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
onSubmit(e)
return
}
}}
onSubmit={(e) => {
if (isDisabled) {
// __TODO__ show disabled
return
}
console.log('submit!')
onSubmit(e)
}}
onClick={(e) => {
inputBoxRef.current?.focus()
}}
>
{/* // this div is used to position the input box properly */}
<div
className={`w-full z-[999] relative
@@[&_textarea]:!void-bg-transparent
@@[&_textarea]:!void-outline-none
@@[&_textarea]:!void-text-vscode-input-fg
@@[&_div.monaco-inputbox]:!void-border-none
@@[&_div.monaco-inputbox]:!void-outline-none`}
>
<div className='flex flex-row justify-between items-end gap-1'>
<div className='absolute size-0.5 top-0 right-4 z-[1]'>
<X
onClick={() => { inlineDiffsService.removeCtrlKZone({ diffareaid }) }}
/>
</div>
{/* input */}
<div // copied from SidebarChat.tsx
className={`w-full
@@[&_textarea]:!void-bg-transparent @@[&_textarea]:!void-outline-none @@[&_textarea]:!void-text-vscode-input-fg @@[&_div.monaco-inputbox]:!void-outline-none`}>
{/* text input */}
<VoidInputBox
placeholder={`${getCmdKey()}+K to select`}
onChangeText={onChangeText}
onCreateInstance={useCallback((instance: InputBox) => {
inputBoxRef.current = instance;
onGetInputBox(instance);
instance.focus()
}, [onGetInputBox])}
multiline={true}
/>
</div>
</div>
{/* bottom row */}
<div
className='flex flex-row justify-between items-end gap-1'
>
{/* submit options */}
<div className='max-w-[150px]
@@[&_select]:!void-border-none
@@[&_select]:!void-outline-none'
>
<ModelDropdown featureName='Ctrl+K' />
</div>
{/* submit / stop button */}
{isStreaming ?
// stop button
<ButtonStop
onClick={onInterrupt}
/>
:
// submit button (up arrow)
<ButtonSubmit
disabled={isDisabled}
/>
}
</div>
</div>
</form>
</div>
}

View file

@ -3,85 +3,30 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { ReactNode } from "react"
import { VoidCodeEditor } from '../util/inputs.js';
import React from 'react';
import { VoidCodeEditor, VoidCodeEditorProps } from '../util/inputs.js';
const extensionMap: { [key: string]: string } = {
// Web
'html': 'html',
'htm': 'html',
'css': 'css',
'scss': 'scss',
'less': 'less',
'js': 'javascript',
'jsx': 'javascript',
'ts': 'typescript',
'tsx': 'typescript',
'json': 'json',
'jsonc': 'json',
export const BlockCode = ({ buttonsOnHover, ...codeEditorProps }: { buttonsOnHover?: React.ReactNode } & VoidCodeEditorProps) => {
// Programming Languages
'py': 'python',
'java': 'java',
'cpp': 'cpp',
'cc': 'cpp',
'h': 'cpp',
'hpp': 'cpp',
'cs': 'csharp',
'go': 'go',
'rs': 'rust',
'rb': 'ruby',
'php': 'php',
'sh': 'shell',
'bash': 'shell',
'zsh': 'shell',
const isSingleLine = !codeEditorProps.initValue.includes('\n')
// Markup/Config
'md': 'markdown',
'markdown': 'markdown',
'xml': 'xml',
'svg': 'xml',
'yaml': 'yaml',
'yml': 'yaml',
'ini': 'ini',
'toml': 'ini',
return (
<>
<div className={`relative group w-full overflow-hidden`}>
// Other
'sql': 'sql',
'graphql': 'graphql',
'gql': 'graphql',
'dockerfile': 'dockerfile',
'docker': 'dockerfile'
};
{buttonsOnHover === null ? null : (
<div className="z-[1] absolute top-0 right-0 opacity-0 group-hover:opacity-100 duration-200">
<div className={`flex space-x-2 ${isSingleLine ? '' : 'p-2'}`}>
{buttonsOnHover}
</div>
</div>
)}
export function getLanguageFromFileName(fileName: string): string {
<VoidCodeEditor {...codeEditorProps} />
const ext = fileName.toLowerCase().split('.').pop();
if (!ext) return 'plaintext';
return extensionMap[ext] || 'plaintext';
}
export const BlockCode = ({ text, buttonsOnHover, language }: { text: string, buttonsOnHover?: ReactNode, language?: string }) => {
const isSingleLine = !text.includes('\n')
return (<>
<div className={`relative group w-full bg-vscode-editor-bg overflow-hidden isolate`}>
{buttonsOnHover === null ? null : (
<div className="z-[1] absolute top-0 right-0 opacity-0 group-hover:opacity-100 duration-200">
<div className={`flex space-x-2 ${isSingleLine ? '' : 'p-2'}`}>{buttonsOnHover}</div>
</div>
)}
<VoidCodeEditor
initValue={text}
language={language}
/>
</div>
</>
</div>
</>
)
}

View file

@ -7,6 +7,7 @@ import React, { JSX, useCallback, useEffect, useState } from 'react'
import { marked, MarkedToken, Token } from 'marked'
import { BlockCode } from './BlockCode.js'
import { useAccessor } from '../util/services.js'
import { nameToVscodeLanguage } from '../../../helpers/detectLanguage.js'
enum CopyButtonState {
@ -23,6 +24,8 @@ const CodeButtonsOnHover = ({ text }: { text: string }) => {
const [copyButtonState, setCopyButtonState] = useState(CopyButtonState.Copy)
const inlineDiffService = accessor.get('IInlineDiffsService')
const clipboardService = accessor.get('IClipboardService')
const metricsService = accessor.get('IMetricsService')
useEffect(() => {
if (copyButtonState !== CopyButtonState.Copy) {
@ -36,6 +39,8 @@ const CodeButtonsOnHover = ({ text }: { text: string }) => {
clipboardService.writeText(text)
.then(() => { setCopyButtonState(CopyButtonState.Copied) })
.catch(() => { setCopyButtonState(CopyButtonState.Error) })
metricsService.capture('Copy Code', { length: text.length }) // capture the length only
}, [text, clipboardService])
const onApply = useCallback(() => {
@ -43,20 +48,21 @@ const CodeButtonsOnHover = ({ text }: { text: string }) => {
featureName: 'Ctrl+L',
userMessage: text,
})
metricsService.capture('Apply Code', { length: text.length }) // capture the length only
}, [inlineDiffService])
const isSingleLine = !text.includes('\n')
return <>
<button
className={`${isSingleLine ? '' : 'p-1'} text-xs hover:brightness-110 bg-vscode-input-bg border border-vscode-input-border rounded text-xs text-vscode-input-fg`}
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-1 text-void-fg-1 hover:brightness-110 border border-vscode-input-border rounded`}
onClick={onCopy}
>
{copyButtonState}
</button>
<button
// btn btn-secondary btn-sm border text-xs text-vscode-input-fg border-vscode-input-border rounded
className={`${isSingleLine ? '' : 'p-1'} text-xs hover:brightness-110 bg-vscode-input-bg border border-vscode-input-border rounded text-xs text-vscode-input-fg`}
// btn btn-secondary btn-sm border text-sm border-vscode-input-border rounded
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-1 text-void-fg-1 hover:brightness-110 border border-vscode-input-border rounded`}
onClick={onApply}
>
Apply
@ -64,8 +70,21 @@ const CodeButtonsOnHover = ({ text }: { text: string }) => {
</>
}
export const CodeSpan = ({ children, className }: { children: React.ReactNode, className?: string }) => {
return <code className={`
bg-void-bg-1
px-1
rounded-sm
font-mono font-medium
break-all
${className}
`}
>
{children}
</code>
}
const RenderToken = ({ token, nested = false }: { token: Token | string, nested?: boolean }): JSX.Element => {
const RenderToken = ({ token, nested = false, noSpace = false }: { token: Token | string, nested?: boolean, noSpace?: boolean }): JSX.Element => {
// deal with built-in tokens first (assume marked token)
const t = token as MarkedToken
@ -76,53 +95,68 @@ const RenderToken = ({ token, nested = false }: { token: Token | string, nested?
if (t.type === "code") {
return <BlockCode
text={t.text}
// language={t.lang} // instead use vscode to detect language
initValue={t.text}
language={t.lang === undefined ? undefined : nameToVscodeLanguage[t.lang]} // use vscode to detect language
buttonsOnHover={<CodeButtonsOnHover text={t.text} />}
/>
}
if (t.type === "heading") {
const HeadingTag = `h${t.depth}` as keyof JSX.IntrinsicElements
return <HeadingTag>{t.text}</HeadingTag>
const headingClasses: { [h: string]: string } = {
h1: "text-4xl font-semibold mt-6 mb-4 pb-2 border-b border-void-bg-2",
h2: "text-3xl font-semibold mt-6 mb-4 pb-2 border-b border-void-bg-2",
h3: "text-2xl font-semibold mt-6 mb-4",
h4: "text-xl font-semibold mt-6 mb-4",
h5: "text-lg font-semibold mt-6 mb-4",
h6: "text-base font-semibold mt-6 mb-4 text-gray-600"
}
return <HeadingTag className={headingClasses[HeadingTag]}>{t.text}</HeadingTag>
}
if (t.type === "table") {
return (
<table>
<thead>
<tr>
{t.header.map((cell: any, index: number) => (
<th key={index} style={{ textAlign: t.align[index] || "left" }}>
{cell.raw}
</th>
))}
</tr>
</thead>
<tbody>
{t.rows.map((row: any[], rowIndex: number) => (
<tr key={rowIndex}>
{row.map((cell: any, cellIndex: number) => (
<td
key={cellIndex}
style={{ textAlign: t.align[cellIndex] || "left" }}
<div className={`${noSpace ? '' : 'my-4'} overflow-x-auto`}>
<table className="min-w-full border border-void-bg-2">
<thead>
<tr className="bg-void-bg-1">
{t.header.map((cell: any, index: number) => (
<th
key={index}
className="px-4 py-2 border border-void-bg-2 font-semibold"
style={{ textAlign: t.align[index] || "left" }}
>
{cell.raw}
</td>
</th>
))}
</tr>
))}
</tbody>
</table>
</thead>
<tbody>
{t.rows.map((row: any[], rowIndex: number) => (
<tr key={rowIndex} className={rowIndex % 2 === 0 ? 'bg-white' : 'bg-void-bg-1'}>
{row.map((cell: any, cellIndex: number) => (
<td
key={cellIndex}
className="px-4 py-2 border border-void-bg-2"
style={{ textAlign: t.align[cellIndex] || "left" }}
>
{cell.raw}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
)
}
if (t.type === "hr") {
return <hr />
return <hr className="my-6 border-t border-void-bg-2" />
}
if (t.type === "blockquote") {
return <blockquote>{t.text}</blockquote>
return <blockquote className={`pl-4 border-l-4 border-void-bg-2 italic ${noSpace ? '' : 'my-4'}`}>{t.text}</blockquote>
}
if (t.type === "list") {
@ -130,14 +164,16 @@ const RenderToken = ({ token, nested = false }: { token: Token | string, nested?
return (
<ListTag
start={t.start ? t.start : undefined}
className={`list-inside ${t.ordered ? "list-decimal" : "list-disc"}`}
className={`list-inside pl-2 ${noSpace ? '' : 'my-4'} ${t.ordered ? "list-decimal" : "list-disc"}`}
>
{t.items.map((item, index) => (
<li key={index}>
<li key={index} className={`${noSpace ? '' : 'mb-4'}`}>
{item.task && (
<input type="checkbox" checked={item.checked} readOnly />
<input type="checkbox" checked={item.checked} readOnly className="mr-2 form-checkbox" />
)}
<ChatMarkdownRender string={item.text} nested={true} />
<span className="ml-1">
<ChatMarkdownRender string={item.text} nested={true} />
</span>
</li>
))}
</ListTag>
@ -152,13 +188,12 @@ const RenderToken = ({ token, nested = false }: { token: Token | string, nested?
</>
if (nested)
return contents
return <p>{contents}</p>
return <p className={`${noSpace ? '' : 'my-4'} leading`}>{contents}</p>
}
// don't actually render <html> tags, just render strings of them
if (t.type === "html") {
return (
<pre>
<pre className={`bg-4oid-bg-2 p-4 rounded-lg ${noSpace ? '' : 'my-4'} font-mono text-sm`}>
{`<html>`}
{t.raw}
{`</html>`}
@ -176,30 +211,40 @@ const RenderToken = ({ token, nested = false }: { token: Token | string, nested?
if (t.type === "link") {
return (
<a className='underline' onClick={() => { window.open(t.href) }} href={t.href} title={t.title ?? undefined}>
<a
className='underline'
onClick={() => { window.open(t.href) }}
href={t.href}
title={t.title ?? undefined}
>
{t.text}
</a>
)
}
if (t.type === "image") {
return <img src={t.href} alt={t.text} title={t.title ?? undefined} />
return <img
src={t.href}
alt={t.text}
title={t.title ?? undefined}
className={`max4w-full h-auto rounded ${noSpace ? '' : 'my-4'}`}
/>
}
if (t.type === "strong") {
return <strong>{t.text}</strong>
return <strong className="font-semibold">{t.text}</strong>
}
if (t.type === "em") {
return <em>{t.text}</em>
return <em className="italic">{t.text}</em>
}
// inline code
if (t.type === "codespan") {
return (
<code className="text-vscode-text-preformat-fg bg-vscode-text-preformat-bg px-1 rounded-sm font-mono">
<CodeSpan>
{t.text}
</code>
</CodeSpan>
)
}
@ -209,24 +254,24 @@ const RenderToken = ({ token, nested = false }: { token: Token | string, nested?
// strikethrough
if (t.type === "del") {
return <del>{t.text}</del>
return <del className="line-through">{t.text}</del>
}
// default
return (
<div className="bg-orange-50 rounded-sm overflow-hidden">
<span className="text-xs text-orange-500">Unknown type:</span>
<div className="bg-orange-50 rounded-sm overflow-hidden p-2">
<span className="text-sm text-orange-500">Unknown type:</span>
{t.raw}
</div>
)
}
export const ChatMarkdownRender = ({ string, nested = false }: { string: string, nested?: boolean }) => {
export const ChatMarkdownRender = ({ string, nested = false, noSpace }: { string: string, nested?: boolean, noSpace?: boolean }) => {
const tokens = marked.lexer(string); // https://marked.js.org/using_pro#renderer
return (
<>
{tokens.map((token, index) => (
<RenderToken key={index} token={token} nested={nested} />
<RenderToken key={index} token={token} nested={nested} noSpace={noSpace} />
))}
</>
)

View file

@ -3,19 +3,19 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { useEffect, useState } from 'react'
import React, { useEffect, useState } from 'react'
import { useIsDark, useSidebarState } from '../util/services.js'
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
import { CtrlKChat } from './CtrlKChat.js'
import { QuickEditChat } from './QuickEditChat.js'
import { QuickEditPropsType } from '../../../quickEditActions.js'
export const CtrlK = (props: QuickEditPropsType) => {
export const QuickEdit = (props: QuickEditPropsType) => {
const isDark = useIsDark()
return <div className={`@@void-scope ${isDark ? 'dark' : ''}`}>
<ErrorBoundary>
<CtrlKChat {...props} />
<QuickEditChat {...props} />
</ErrorBoundary>
</div>

View file

@ -0,0 +1,190 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import React, { FormEvent, useCallback, useEffect, useRef, useState } from 'react';
import { useSettingsState, useSidebarState, useChatThreadsState, useQuickEditState, useAccessor } from '../util/services.js';
import { TextAreaFns, VoidInputBox2 } from '../util/inputs.js';
import { QuickEditPropsType } from '../../../quickEditActions.js';
import { ButtonStop, ButtonSubmit, IconX } from '../sidebar-tsx/SidebarChat.js';
import { ModelDropdown } from '../void-settings-tsx/ModelDropdown.js';
import { VOID_CTRL_K_ACTION_ID } from '../../../actionIDs.js';
import { useRefState } from '../util/helpers.js';
import { useScrollbarStyles } from '../util/useScrollbarStyles.js';
export const QuickEditChat = ({
diffareaid,
initStreamingDiffZoneId,
onChangeHeight,
onChangeText: onChangeText_,
textAreaRef: textAreaRef_,
initText
}: QuickEditPropsType) => {
const accessor = useAccessor()
const inlineDiffsService = accessor.get('IInlineDiffsService')
const sizerRef = useRef<HTMLDivElement | null>(null)
const textAreaRef = useRef<HTMLTextAreaElement | null>(null)
const textAreaFnsRef = useRef<TextAreaFns | null>(null)
useEffect(() => {
const inputContainer = sizerRef.current
if (!inputContainer) return;
// only observing 1 element
let resizeObserver: ResizeObserver | undefined
resizeObserver = new ResizeObserver((entries) => {
const height = entries[0].borderBoxSize[0].blockSize
onChangeHeight(height)
})
resizeObserver.observe(inputContainer);
return () => { resizeObserver?.disconnect(); };
}, [onChangeHeight]);
// state of current message
const [instructionsAreEmpty, setInstructionsAreEmpty] = useState(!(initText ?? '')) // the user's instructions
const isDisabled = instructionsAreEmpty
const [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone] = useRefState<number | null>(initStreamingDiffZoneId)
const isStreaming = currStreamingDiffZoneRef.current !== null
const onSubmit = useCallback((e: FormEvent) => {
if (isDisabled) return
if (currStreamingDiffZoneRef.current !== null) return
textAreaFnsRef.current?.disable()
const instructions = textAreaRef.current?.value ?? ''
const id = inlineDiffsService.startApplying({
featureName: 'Ctrl+K',
diffareaid: diffareaid,
userMessage: instructions,
})
setCurrentlyStreamingDiffZone(id ?? null)
}, [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone, isDisabled, inlineDiffsService, diffareaid])
const onInterrupt = useCallback(() => {
if (currStreamingDiffZoneRef.current === null) return
inlineDiffsService.interruptStreaming(currStreamingDiffZoneRef.current)
setCurrentlyStreamingDiffZone(null)
textAreaFnsRef.current?.enable()
}, [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone, inlineDiffsService])
const onX = useCallback(() => {
onInterrupt()
inlineDiffsService.removeCtrlKZone({ diffareaid })
}, [inlineDiffsService, diffareaid])
useScrollbarStyles(sizerRef)
const keybindingString = accessor.get('IKeybindingService').lookupKeybinding(VOID_CTRL_K_ACTION_ID)?.getLabel()
return <div ref={sizerRef} style={{ maxWidth: 500 }} className={`py-2 w-full`}>
<form
// copied from SidebarChat.tsx
className={`
flex flex-col gap-2 p-2 relative input text-left shrink-0
transition-all duration-200
rounded-md
bg-vscode-input-bg
border border-void-border-3 focus-within:border-void-border-1 hover:border-void-border-1
`}
onClick={(e) => {
textAreaRef.current?.focus()
}}
>
{/* // this div is used to position the input box properly */}
<div
className={`w-full z-[999] relative`}
>
<div className='flex flex-row items-center justify-between items-end gap-1'>
{/* input */}
<div // copied from SidebarChat.tsx
className={`w-full`}
>
{/* text input */}
<VoidInputBox2
className='px-1'
initValue={initText}
ref={useCallback((r: HTMLTextAreaElement | null) => {
textAreaRef.current = r
textAreaRef_(r)
// if presses the esc key, X
r?.addEventListener('keydown', (e) => {
if (e.key === 'Escape')
onX()
})
}, [textAreaRef_, onX])}
fnsRef={textAreaFnsRef}
placeholder={`Enter instructions...`}
// ${keybindingString} to select.
onChangeText={useCallback((newStr: string) => {
setInstructionsAreEmpty(!newStr)
onChangeText_(newStr)
}, [onChangeText_])}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
onSubmit(e)
return
}
}}
multiline={true}
/>
</div>
{/* X button */}
<div className='absolute -top-1 -right-1 cursor-pointer z-1'>
<IconX
size={16}
className="p-[1px] stroke-[2] opacity-80 text-void-fg-3 hover:brightness-95"
onClick={onX}
/>
</div>
</div>
{/* bottom row */}
<div
className='flex flex-row justify-between items-end gap-1'
>
{/* submit options */}
<div className='max-w-[150px]
@@[&_select]:!void-border-none
@@[&_select]:!void-outline-none'
>
<ModelDropdown featureName='Ctrl+K' />
</div>
{/* submit / stop button */}
{isStreaming ?
// stop button
<ButtonStop
onClick={onInterrupt}
/>
:
// submit button (up arrow)
<ButtonSubmit
onClick={onSubmit}
disabled={isDisabled}
/>
}
</div>
</div>
</form>
</div>
}

View file

@ -4,9 +4,9 @@
*--------------------------------------------------------------------------------------*/
import { mountFnGenerator } from '../util/mountFnGenerator.js'
import { CtrlK } from './CtrlK.js'
import { QuickEdit } from './QuickEdit.js'
export const mountCtrlK = mountFnGenerator(CtrlK)
export const mountCtrlK = mountFnGenerator(QuickEdit)

View file

@ -3,12 +3,13 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import { AlertCircle, ChevronDown, ChevronUp, X } from 'lucide-react';
import { errorDetails } from '../../../../../../../platform/void/common/llmMessageTypes.js';
export const ErrorDisplay = ({
message,
message:message_,
fullError,
onDismiss,
showDismiss,
@ -20,54 +21,46 @@ export const ErrorDisplay = ({
}) => {
const [isExpanded, setIsExpanded] = useState(false);
let details: string | null = null;
const details = errorDetails(fullError)
if (fullError === null) {
details = null
}
else if (typeof fullError === 'object') {
details = JSON.stringify(fullError, null, 2)
}
else if (typeof fullError === 'string') {
details = null
}
const message = message_ === 'TypeError: fetch failed' ? 'TypeError: fetch failed. This likely means you specified the wrong endpoint in Void Settings.' : message_
return (
<div className={`rounded-lg border border-red-200 bg-red-50 p-4 overflow-auto`}>
{/* Header */}
<div className="flex items-start justify-between">
<div className="flex gap-3">
<AlertCircle className="h-5 w-5 text-red-600 mt-0.5" />
<div className="flex-1">
<h3 className="font-semibold text-red-800">
<div className='flex items-start justify-between'>
<div className='flex gap-3'>
<AlertCircle className='h-5 w-5 text-red-600 mt-0.5' />
<div className='flex-1'>
<h3 className='font-semibold text-red-800'>
{/* eg Error */}
Error
</h3>
<p className="text-red-700 mt-1">
<p className='text-red-700 mt-1'>
{/* eg Something went wrong */}
{message}
</p>
</div>
</div>
<div className="flex gap-2">
<div className='flex gap-2'>
{details && (
<button className="text-red-600 hover:text-red-800 p-1 rounded"
<button className='text-red-600 hover:text-red-800 p-1 rounded'
onClick={() => setIsExpanded(!isExpanded)}
>
{isExpanded ? (
<ChevronUp className="h-5 w-5" />
<ChevronUp className='h-5 w-5' />
) : (
<ChevronDown className="h-5 w-5" />
<ChevronDown className='h-5 w-5' />
)}
</button>
)}
{showDismiss && onDismiss && (
<button className="text-red-600 hover:text-red-800 p-1 rounded"
<button className='text-red-600 hover:text-red-800 p-1 rounded'
onClick={onDismiss}
>
<X className="h-5 w-5" />
<X className='h-5 w-5' />
</button>
)}
</div>
@ -75,10 +68,10 @@ export const ErrorDisplay = ({
{/* Expandable Details */}
{isExpanded && details && (
<div className="mt-4 space-y-3 border-t border-red-200 pt-3 overflow-auto">
<div className='mt-4 space-y-3 border-t border-red-200 pt-3 overflow-auto'>
<div>
<span className="font-semibold text-red-800">Full Error: </span>
<pre className="text-red-700">{details}</pre>
<span className='font-semibold text-red-800'>Full Error: </span>
<pre className='text-red-700'>{details}</pre>
</div>
</div>
)}

View file

@ -13,18 +13,26 @@ import { useIsDark, useSidebarState } from '../util/services.js';
// import { SidebarChat } from './SidebarChat.js';
import '../styles.css'
import { SidebarThreadSelector } from './SidebarThreadSelector.js';
import { SidebarChat } from './SidebarChat.js';
import ErrorBoundary from './ErrorBoundary.js';
export const Sidebar = ({ className }: { className: string }) => {
const sidebarState = useSidebarState()
const { isHistoryOpen, currentTab: tab } = sidebarState
const { currentTab: tab } = sidebarState
const isDark = useIsDark()
// ${isDark ? 'dark' : ''}
return <div className={`@@void-scope`} style={{ width: '100%', height: '100%' }}>
<div className={`w-full h-full flex flex-col py-2 bg-vscode-sidebar-bg`}>
// const isDark = useIsDark()
return <div
className={`@@void-scope`} // ${isDark ? 'dark' : ''}
style={{ width: '100%', height: '100%' }}
>
<div
// default background + text styles for sidebar
className={`
w-full h-full py-2
bg-void-bg-2
text-void-fg-1
`}
>
{/* <span onClick={() => {
const tabs = ['chat', 'settings', 'threadSelector']
@ -32,11 +40,11 @@ export const Sidebar = ({ className }: { className: string }) => {
sidebarStateService.setState({ currentTab: tabs[(index + 1) % tabs.length] as any })
}}>clickme {tab}</span> */}
<div className={`w-full h-auto mb-2 ${isHistoryOpen ? '' : 'hidden'} ring-2 ring-widget-shadow z-10`}>
{/* <div className={`w-full h-auto mb-2 ${isHistoryOpen ? '' : 'hidden'} ring-2 ring-widget-shadow z-10`}>
<ErrorBoundary>
<SidebarThreadSelector />
</ErrorBoundary>
</div>
</div> */}
<div className={`w-full h-full ${tab === 'chat' ? '' : 'hidden'}`}>
<ErrorBoundary>

View file

@ -6,27 +6,32 @@
import React, { ButtonHTMLAttributes, FormEvent, FormHTMLAttributes, Fragment, useCallback, useEffect, useRef, useState } from 'react';
import { useAccessor, useThreadsState } from '../util/services.js';
import { ChatMessage, CodeSelection, CodeStagingSelection, IThreadHistoryService } from '../../../threadHistoryService.js';
import { useAccessor, useSidebarState, useChatThreadsState, useChatThreadsStreamState } from '../util/services.js';
import { ChatMessage, CodeSelection, CodeStagingSelection } from '../../../chatThreadService.js';
import { BlockCode, getLanguageFromFileName } from '../markdown/BlockCode.js';
import { BlockCode } from '../markdown/BlockCode.js';
import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js';
import { URI } from '../../../../../../../base/common/uri.js';
import { EndOfLinePreference } from '../../../../../../../editor/common/model.js';
import { IDisposable } from '../../../../../../../base/common/lifecycle.js';
import { ErrorDisplay } from './ErrorDisplay.js';
import { OnError, ServiceSendLLMMessageParams } from '../../../../../../../platform/void/common/llmMessageTypes.js';
import { getCmdKey } from '../../../helpers/getCmdKey.js'
import { HistoryInputBox, InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js';
import { VoidInputBox } from '../util/inputs.js';
import { TextAreaFns, VoidCodeEditorProps, VoidInputBox2 } from '../util/inputs.js';
import { ModelDropdown } from '../void-settings-tsx/ModelDropdown.js';
import { chat_systemMessage, chat_prompt } from '../../../prompt/prompts.js';
import { ISidebarStateService } from '../../../sidebarStateService.js';
import { ILLMMessageService } from '../../../../../../../platform/void/common/llmMessageService.js';
import { IModelService } from '../../../../../../../editor/common/services/model.js';
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'
const IconX = ({ size, className = '', ...props }: { size: number, className?: string } & React.SVGProps<SVGSVGElement>) => {
export const IconX = ({ size, className = '', ...props }: { size: number, className?: string } & React.SVGProps<SVGSVGElement>) => {
return (
<svg
xmlns='http://www.w3.org/2000/svg'
@ -47,14 +52,13 @@ const IconX = ({ size, className = '', ...props }: { size: number, className?: s
);
};
const IconArrowUp = ({ size, className = '' }: { size: number, className?: string }) => {
return (
<svg
width={size}
height={size}
className={className}
viewBox="0 0 32 32"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
@ -62,10 +66,9 @@ const IconArrowUp = ({ size, className = '' }: { size: number, className?: strin
fill="black"
fillRule="evenodd"
clipRule="evenodd"
d="M15.1918 8.90615C15.6381 8.45983 16.3618 8.45983 16.8081 8.90615L21.9509 14.049C22.3972 14.4953 22.3972 15.2189 21.9509 15.6652C21.5046 16.1116 20.781 16.1116 20.3347 15.6652L17.1428 12.4734V22.2857C17.1428 22.9169 16.6311 23.4286 15.9999 23.4286C15.3688 23.4286 14.8571 22.9169 14.8571 22.2857V12.4734L11.6652 15.6652C11.2189 16.1116 10.4953 16.1116 10.049 15.6652C9.60265 15.2189 9.60265 14.4953 10.049 14.049L15.1918 8.90615Z"
d="M5.293 9.707a1 1 0 010-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 01-1.414 1.414L11 7.414V15a1 1 0 11-2 0V7.414L6.707 9.707a1 1 0 01-1.414 0z"
></path>
</svg>
);
};
@ -169,38 +172,40 @@ const useResizeObserver = () => {
type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement>
const DEFAULT_BUTTON_SIZE = 20;
const DEFAULT_BUTTON_SIZE = 22;
export const ButtonSubmit = ({ className, disabled, ...props }: ButtonProps & Required<Pick<ButtonProps, 'disabled'>>) => {
return <button
type='submit'
className={`size-[20px] rounded-full shrink-0 grow-0 cursor-pointer
${disabled ? 'bg-vscode-disabled-fg' : 'bg-white'}
type='button'
className={`rounded-full flex-shrink-0 flex-grow-0 flex items-center justify-center
${disabled ? 'bg-vscode-disabled-fg cursor-default' : 'bg-white cursor-pointer'}
${className}
`}
{...props}
>
<IconArrowUp size={DEFAULT_BUTTON_SIZE} className="stroke-[2]" />
<IconArrowUp size={DEFAULT_BUTTON_SIZE} className="stroke-[2] p-[2px]" />
</button>
}
export const ButtonStop = ({ className, ...props }: ButtonHTMLAttributes<HTMLButtonElement>) => {
return <button
className={`rounded-full bg-white shrink-0 grow-0 cursor-pointer flex items-center justify-center
className={`rounded-full flex-shrink-0 flex-grow-0 cursor-pointer flex items-center justify-center
bg-white
${className}
`}
type='button'
{...props}
>
<IconSquare size={DEFAULT_BUTTON_SIZE} className="stroke-[2] p-[6px]" />
<IconSquare size={DEFAULT_BUTTON_SIZE} className="stroke-[3] p-[7px]" />
</button>
}
const ScrollToBottomContainer = ({ children, className, style }: { children: React.ReactNode, className?: string, style?: React.CSSProperties }) => {
const ScrollToBottomContainer = ({ children, className, style, scrollContainerRef }: { children: React.ReactNode, className?: string, style?: React.CSSProperties, scrollContainerRef: React.MutableRefObject<HTMLDivElement | null> }) => {
const [isAtBottom, setIsAtBottom] = useState(true); // Start at bottom
const divRef = useRef<HTMLDivElement>(null);
const divRef = scrollContainerRef
const scrollToBottom = () => {
if (divRef.current) {
@ -245,13 +250,6 @@ const ScrollToBottomContainer = ({ children, className, style }: { children: Rea
};
// read files from VSCode
const VSReadFile = async (modelService: IModelService, uri: URI): Promise<string | null> => {
const model = modelService.getModel(uri)
if (!model) return null
return model.getValue(EndOfLinePreference.LF)
}
const getBasename = (pathStr: string) => {
// 'unixify' path
@ -269,97 +267,132 @@ export const SelectedFiles = (
// index -> isOpened
const [selectionIsOpened, setSelectionIsOpened] = useState<(boolean)[]>(selections?.map(() => false) ?? [])
// state for tracking hover on clear all button
const [isClearHovered, setIsClearHovered] = useState(false)
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
return (
!!selections && selections.length !== 0 && (
<div
className='flex flex-wrap gap-2 text-left'
className='flex items-center flex-wrap gap-0.5 text-left relative'
>
{selections.map((selection, i) => {
const isThisSelectionOpened = !!(selection.selectionStr && selectionIsOpened[i])
const isThisSelectionAFile = selection.selectionStr === null
return (
<div key={i} // container for `selectionSummary` and `selectionText`
className={`${isThisSelectionOpened ? 'w-full' : ''}`}
return <div key={i} // container for `selectionSummary` and `selectionText`
className={`${isThisSelectionOpened ? 'w-full' : ''}`}
>
{/* selection summary */}
<div // container for delete button
className='flex items-center gap-0.5'
>
{/* selection summary */}
<div
// className="relative rounded rounded-e-2xl flex items-center space-x-2 mx-1 mb-1 disabled:cursor-default"
className={`flex items-center gap-1 relative
rounded-md p-1
<div // styled summary box
className={`flex items-center gap-0.5 relative
rounded-md px-1
w-fit h-fit
select-none
bg-vscode-editor-bg hover:brightness-95
border border-vscode-commandcenter-border rounded-xs
text-xs text-vscode-editor-fg text-nowrap
`}
bg-void-bg-3 hover:brightness-95
text-void-fg-1 text-xs text-nowrap
border rounded-xs ${isClearHovered ? 'border-void-border-1' : 'border-void-border-2'} hover:border-void-border-1
transition-all duration-150`}
onClick={() => {
setSelectionIsOpened(s => {
const newS = [...s]
newS[i] = !newS[i]
return newS
});
// open the file if it is a file
if (isThisSelectionAFile) {
commandService.executeCommand('vscode.open', selection.fileURI, {
preview: true,
// preserveFocus: false,
});
} else {
// open the selection if it is a text-selection
setSelectionIsOpened(s => {
const newS = [...s]
newS[i] = !newS[i]
return newS
});
}
}}
>
<span className=''>
<span>
{/* file name */}
{getBasename(selection.fileURI.fsPath)}
{/* selection range */}
{selection.selectionStr !== null ? ` (${selection.range.startLineNumber}-${selection.range.endLineNumber})` : ''}
{!isThisSelectionAFile ? ` (${selection.range.startLineNumber}-${selection.range.endLineNumber})` : ''}
</span>
{/* X button */}
{type === 'staging' &&
<span
className='
cursor-pointer
bg-vscode-editorwidget-bg hover:bg-vscode-toolbar-hover-bg
rounded-md
z-1
'
className='cursor-pointer hover:brightness-95 rounded-md z-1'
onClick={(e) => {
e.stopPropagation();
if (type !== 'staging') return;
setStaging([...selections.slice(0, i), ...selections.slice(i + 1)])
setSelectionIsOpened(o => [...o.slice(0, i), ...o.slice(i + 1)])
}}
>
<IconX size={16} className="p-[2px] stroke-[3] text-vscode-toolbar-foreground" />
</span>
}
{/* type of selection */}
{/* <span className='truncate'>{selection.selectionStr !== null ? 'Selection' : 'File'}</span> */}
{/* X button */}
{/* {type === 'staging' && // hoveredIdx === i
<span className='absolute right-0 top-0 translate-x-[50%] translate-y-[-50%] cursor-pointer bg-white rounded-full border border-vscode-input-border z-1'
onClick={(e) => {
e.stopPropagation();
e.stopPropagation(); // don't open/close selection
if (type !== 'staging') return;
setStaging([...selections.slice(0, i), ...selections.slice(i + 1)])
setSelectionIsOpened(o => [...o.slice(0, i), ...o.slice(i + 1)])
}}
>
<IconX size={16} className="p-[2px] stroke-[3]" />
</span>
} */}
</span>}
</div>
{/* selection text */}
{isThisSelectionOpened &&
<div className='w-full p-1 rounded-sm border-vscode-editor-border'>
<BlockCode text={selection.selectionStr!} language={getLanguageFromFileName(selection.fileURI.path)} />
{/* clear all selections button */}
{type !== 'staging' || selections.length === 0 || i !== selections.length - 1
? null
: <div key={i} className={`flex items-center gap-0.5 ${isThisSelectionOpened ? 'w-full' : ''}`}>
<div
className='rounded-md'
onMouseEnter={() => setIsClearHovered(true)}
onMouseLeave={() => setIsClearHovered(false)}
>
<Delete
size={16}
className={`stroke-[1]
stroke-void-fg-1
fill-void-bg-3
opacity-40
hover:opacity-60
transition-all duration-150
cursor-pointer
`}
onClick={() => { setStaging([]) }}
/>
</div>
</div>
}
</div>
)
{/* selection text */}
{isThisSelectionOpened &&
<div
className='w-full px-1 rounded-sm border-vscode-editor-border'
onClick={(e) => {
e.stopPropagation(); // don't focus input box
}}
>
<BlockCode
initValue={selection.selectionStr!}
language={filenameToVscodeLanguage(selection.fileURI.path)}
maxHeight={200}
showScrollbars={true}
/>
</div>
}
</div>
})}
</div>
)
)
}
const ChatBubble = ({ chatMessage, isLoading }: {
chatMessage: ChatMessage,
isLoading?: boolean,
@ -367,8 +400,8 @@ const ChatBubble = ({ chatMessage, isLoading }: {
const role = chatMessage.role
if (!chatMessage.displayContent)
return null
// edit mode state
const [isEditMode, setIsEditMode] = useState(false)
let chatbubbleContents: React.ReactNode
@ -376,30 +409,69 @@ const ChatBubble = ({ chatMessage, isLoading }: {
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} /> // sectionsHTML
chatbubbleContents = <ChatMarkdownRender string={chatMessage.displayContent ?? ''} />
}
return <div
// style + align chatbubble accoridng to role
className={`p-2 mx-2 text-left space-y-2 rounded-lg max-w-full
${role === 'user' ? 'self-end' : 'self-start'}
${role === 'user' ? 'bg-vscode-input-bg text-vscode-input-fg' : ''}
${role === 'assistant' ? 'w-full' : ''}
`}
// align chatbubble accoridng to role
className={`
relative
${isEditMode ? 'px-2 w-full max-w-full'
: role === 'user' ? `px-2 self-end w-fit max-w-full`
: role === 'assistant' ? `px-2 self-start w-full max-w-full` : ''
}
`}
>
{chatbubbleContents}
{isLoading && <IconLoading className='opacity-50 text-sm' />}
<div
// style chatbubble according to role
className={`
p-2 text-left space-y-2 rounded-lg
overflow-x-auto max-w-full
${role === 'user' ? 'bg-void-bg-1 text-void-fg-1' : ''}
`}
>
{chatbubbleContents}
{isLoading && <IconLoading className='opacity-50 text-sm' />}
</div>
{/* edit button */}
{/* {role === 'user' &&
<Pencil
size={16}
className={`
absolute top-0 right-2
translate-x-0 -translate-y-0
cursor-pointer z-1
`}
onClick={() => { setIsEditMode(v => !v); }}
/>
} */}
</div>
}
export const SidebarChat = () => {
const inputBoxRef: React.MutableRefObject<InputBox | null> = useRef(null);
const textAreaRef = useRef<HTMLTextAreaElement | null>(null)
const textAreaFnsRef = useRef<TextAreaFns | null>(null)
const accessor = useAccessor()
const modelService = accessor.get('IModelService')
@ -410,172 +482,103 @@ export const SidebarChat = () => {
useEffect(() => {
const disposables: IDisposable[] = []
disposables.push(
sidebarStateService.onDidFocusChat(() => { inputBoxRef.current?.focus() }),
sidebarStateService.onDidBlurChat(() => { inputBoxRef.current?.blur() })
sidebarStateService.onDidFocusChat(() => { textAreaRef.current?.focus() }),
sidebarStateService.onDidBlurChat(() => { textAreaRef.current?.blur() })
)
return () => disposables.forEach(d => d.dispose())
}, [sidebarStateService, inputBoxRef])
}, [sidebarStateService, textAreaRef])
const { isHistoryOpen } = useSidebarState()
// threads state
const threadsState = useThreadsState()
const threadsStateService = accessor.get('IThreadHistoryService')
const chatThreadsState = useChatThreadsState()
const chatThreadsService = accessor.get('IChatThreadService')
const llmMessageService = accessor.get('ILLMMessageService')
const currentThread = chatThreadsService.getCurrentThread()
const previousMessages = currentThread?.messages ?? []
const selections = chatThreadsState.currentStagingSelections
// stream state
const chatThreadsStreamState = useChatThreadsStreamState(chatThreadsState.currentThreadId)
const isCurrThreadStreaming = !!chatThreadsStreamState?.streamingToken
const latestError = chatThreadsStreamState?.error
const messageSoFar = chatThreadsStreamState?.messageSoFar
// ----- SIDEBAR CHAT state (local) -----
// state of chat
const [messageStream, setMessageStream] = useState<string | null>(null)
const [isLoading, setIsLoading] = useState(false)
const latestRequestIdRef = useRef<string | null>(null)
const [latestError, setLatestError] = useState<Parameters<OnError>[0] | null>(null)
// state of current message
const [instructions, setInstructions] = useState('') // the user's instructions
const isDisabled = !instructions.trim()
const initVal = ''
const [instructionsAreEmpty, setInstructionsAreEmpty] = useState(!initVal)
const isDisabled = instructionsAreEmpty
const [sidebarRef, sidebarDimensions] = useResizeObserver()
const [formRef, formDimensions] = useResizeObserver()
const [historyRef, historyDimensions] = useResizeObserver()
// const [formHeight, setFormHeight] = useState(0) // TODO should use resize observer instead
// const [sidebarHeight, setSidebarHeight] = useState(0)
const onChangeText = useCallback((newStr: string) => { setInstructions(newStr) }, [setInstructions])
useScrollbarStyles(sidebarRef)
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
const onSubmit = async () => {
e.preventDefault()
if (isDisabled) return
if (isLoading) return
const currSelns = threadsStateService.state._currentStagingSelections ?? []
const selections = !currSelns ? null : await Promise.all(
currSelns.map(async (sel) => ({ ...sel, content: await VSReadFile(modelService, sel.fileURI) }))
).then(
(files) => files.filter(file => file.content !== null) as CodeSelection[]
)
// // TODO don't save files to the thread history
// const selectedSnippets = currSelns.filter(sel => sel.selectionStr !== null)
// const selectedFiles = await Promise.all( // do not add these to the context history
// currSelns.filter(sel => sel.selectionStr === null)
// .map(async (sel) => ({ ...sel, content: await VSReadFile(modelService, sel.fileURI) }))
// ).then(
// (files) => files.filter(file => file.content !== null) as CodeSelection[]
// )
// const contextToSendToLLM = ''
// const contextToAddToHistory = ''
// add system message to chat history
const systemPromptElt: ChatMessage = { role: 'system', content: chat_systemMessage }
threadsStateService.addMessageToCurrentThread(systemPromptElt)
// add user's message to chat history
const userHistoryElt: ChatMessage = { role: 'user', content: chat_prompt(instructions, selections), displayContent: instructions, selections: selections }
threadsStateService.addMessageToCurrentThread(userHistoryElt)
const currentThread = threadsStateService.getCurrentThread(threadsStateService.state) // the the instant state right now, don't wait for the React state
if (isCurrThreadStreaming) return
// send message to LLM
setIsLoading(true) // must come before message is sent so onError will work
setLatestError(null)
if (inputBoxRef.current) {
inputBoxRef.current.value = ''; // this triggers onDidChangeText
inputBoxRef.current.blur();
}
const userMessage = textAreaRef.current?.value ?? ''
await chatThreadsService.addUserMessageAndStreamResponse(userMessage)
const object: ServiceSendLLMMessageParams = {
logging: { loggingName: 'Chat' },
messages: [...(currentThread?.messages ?? []).map(m => ({ role: m.role, content: m.content || '(null)' })),],
onText: ({ newText, fullText }) => setMessageStream(fullText),
onFinalMessage: ({ fullText: content }) => {
console.log('chat: running final message')
// add assistant's message to chat history, and clear selection
const assistantHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content || null }
threadsStateService.addMessageToCurrentThread(assistantHistoryElt)
setMessageStream(null)
setIsLoading(false)
},
onError: ({ message, fullError }) => {
console.log('chat: running error', message, fullError)
// add assistant's message to chat history, and clear selection
let content = messageStream ?? ''; // just use the current content
const assistantHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content || null, }
threadsStateService.addMessageToCurrentThread(assistantHistoryElt)
setMessageStream('')
setIsLoading(false)
setLatestError({ message, fullError })
},
featureName: 'Ctrl+L',
}
const latestRequestId = llmMessageService.sendLLMMessage(object)
latestRequestIdRef.current = latestRequestId
threadsStateService.setStaging([]) // clear staging
inputBoxRef.current?.focus() // focus input after submit
chatThreadsService.setStaging([]) // clear staging
textAreaFnsRef.current?.setValue('')
textAreaRef.current?.focus() // focus input after submit
}
const onAbort = () => {
// abort the LLM call
if (latestRequestIdRef.current)
llmMessageService.abort(latestRequestIdRef.current)
// if messageStream was not empty, add it to the history
const llmContent = messageStream ?? ''
const assistantHistoryElt: ChatMessage = { role: 'assistant', content: llmContent, displayContent: messageStream || null, }
threadsStateService.addMessageToCurrentThread(assistantHistoryElt)
setMessageStream('')
setIsLoading(false)
const token = chatThreadsStreamState?.streamingToken
if (!token) return
chatThreadsService.cancelStreaming(token)
}
const currentThread = threadsStateService.getCurrentThread(threadsState)
const selections = threadsState._currentStagingSelections
const previousMessages = currentThread?.messages ?? []
// const [_test_messages, _set_test_messages] = useState<string[]>([])
const keybindingString = accessor.get('IKeybindingService').lookupKeybinding(VOID_CTRL_L_ACTION_ID)?.getLabel()
// scroll to top on thread switch
const scrollContainerRef = useRef<HTMLDivElement | null>(null)
useEffect(() => {
if (isHistoryOpen)
scrollContainerRef.current?.scrollTo({ top: 0, left: 0 })
}, [isHistoryOpen, currentThread.id])
return <div
ref={sidebarRef}
className={`w-full h-full`}
>
{/* thread selector */}
<div ref={historyRef}
className={`w-full h-auto ${isHistoryOpen ? '' : 'hidden'} ring-2 ring-widget-shadow ring-inset z-10`}
>
<SidebarThreadSelector />
</div>
{/* previous messages + current stream */}
<ScrollToBottomContainer
scrollContainerRef={scrollContainerRef}
className={`
w-full h-auto
flex flex-col gap-0
overflow-x-hidden
overflow-y-auto
`}
style={{ maxHeight: sidebarDimensions.height - formDimensions.height - 30 }}
style={{ maxHeight: sidebarDimensions.height - historyDimensions.height - formDimensions.height - 30 }} // the height of the previousMessages is determined by all other heights
>
{/* previous messages */}
{previousMessages.map((message, i) => <ChatBubble key={i} chatMessage={message} />)}
{previousMessages.map((message, i) =>
<ChatBubble key={i} chatMessage={message} />
)}
{/* message stream */}
<ChatBubble chatMessage={{ role: 'assistant', content: messageStream, displayContent: messageStream || null }} isLoading={isLoading} />
{/* {_test_messages.map((_, i) => <div key={i}>div {i}</div>)}
<div>{`totalHeight: ${sidebarHeight - formHeight - 30}`}</div>
<div>{`sidebarHeight: ${sidebarHeight}`}</div>
<div>{`formHeight: ${formHeight}`}</div>
<button type='button' onClick={() => { _set_test_messages(d => [...d, 'asdasdsadasd']) }}>add div</button> */}
<ChatBubble chatMessage={{ role: 'assistant', content: messageSoFar ?? '', displayContent: messageSoFar || null }} isLoading={isCurrThreadStreaming} />
</ScrollToBottomContainer>
@ -584,73 +587,53 @@ export const SidebarChat = () => {
<div // this div is used to position the input box properly
className={`right-0 left-0 m-2 z-[999] overflow-hidden ${previousMessages.length > 0 ? 'absolute bottom-0' : ''}`}
>
<form
<div
ref={formRef}
className={`
flex flex-col gap-2 p-2 relative input text-left shrink-0
transition-all duration-200
rounded-md
bg-vscode-input-bg
border border-vscode-commandcenter-inactive-border focus-within:border-vscode-commandcenter-active-border hover:border-vscode-commandcenter-active-border
max-h-[80vh] overflow-y-auto
border border-void-border-3 focus-within:border-void-border-1 hover:border-void-border-1
`}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
onSubmit(e)
}
}}
onSubmit={(e) => {
console.log('submit!')
onSubmit(e)
}}
onClick={(e) => {
inputBoxRef.current?.focus()
textAreaRef.current?.focus()
}}
>
{/* top row */}
<>
{/* selections */}
{(selections && selections.length !== 0) &&
<SelectedFiles type='staging' selections={selections} setStaging={threadsStateService.setStaging.bind(threadsStateService)} />
<SelectedFiles type='staging' selections={selections} setStaging={chatThreadsService.setStaging.bind(chatThreadsService)} />
}
{/* error message */}
{latestError === null ? null :
{latestError === undefined ? null :
<ErrorDisplay
message={latestError.message}
fullError={latestError.fullError}
onDismiss={() => { setLatestError(null) }}
onDismiss={() => { chatThreadsService.dismissStreamError(currentThread.id) }}
showDismiss={true}
/>
}
</>
{/* middle row */}
<div
className={
// // hack to overwrite vscode styles (generated with this code):
// `bg-transparent outline-none text-vscode-input-fg min-h-[81px] max-h-[500px]`
// .split(' ')
// .map(style => `@@[&_textarea]:!void-${style}`) // apply styles to ancestor textarea elements
// .join(' ') +
// ` outline-none border-none`
// .split(' ')
// .map(style => `@@[&_div.monaco-inputbox]:!void-${style}`)
// .join(' ');
`@@[&_textarea]:!void-bg-transparent
@@[&_textarea]:!void-outline-none
@@[&_textarea]:!void-text-vscode-input-fg
@@[&_textarea]:!void-min-h-[81px]
@@[&_textarea]:!void-max-h-[500px]
@@[&_div.monaco-inputbox]:!void-border-none
@@[&_div.monaco-inputbox]:!void-outline-none`
}
>
<div>
{/* text input */}
<VoidInputBox
placeholder={`${getCmdKey()}+L to select`}
onChangeText={onChangeText}
inputBoxRef={inputBoxRef}
<VoidInputBox2
className='min-h-[81px] p-1'
placeholder={`${keybindingString} to select. Enter instructions...`}
onChangeText={useCallback((newStr: string) => { setInstructionsAreEmpty(!newStr) }, [setInstructionsAreEmpty])}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
onSubmit()
}
}}
ref={textAreaRef}
fnsRef={textAreaFnsRef}
multiline={true}
/>
</div>
@ -662,13 +645,15 @@ export const SidebarChat = () => {
{/* submit options */}
<div className='max-w-[150px]
@@[&_select]:!void-border-none
@@[&_select]:!void-outline-none'
@@[&_select]:!void-outline-none
flex-grow
'
>
<ModelDropdown featureName='Ctrl+L' />
</div>
{/* submit / stop button */}
{isLoading ?
{isCurrThreadStreaming ?
// stop button
<ButtonStop
onClick={onAbort}
@ -676,13 +661,14 @@ export const SidebarChat = () => {
:
// submit button (up arrow)
<ButtonSubmit
onClick={onSubmit}
disabled={isDisabled}
/>
}
</div>
</form>
</div>
</div >
</div >
}

View file

@ -4,9 +4,9 @@
*--------------------------------------------------------------------------------------*/
import React from "react";
import { useAccessor, useThreadsState } from '../util/services.js';
import { IThreadHistoryService } from '../../../threadHistoryService.js';
import { useAccessor, useChatThreadsState } from '../util/services.js';
import { ISidebarStateService } from '../../../sidebarStateService.js';
import { IconX } from './SidebarChat.js';
const truncate = (s: string) => {
@ -19,10 +19,10 @@ const truncate = (s: string) => {
export const SidebarThreadSelector = () => {
const threadsState = useThreadsState()
const threadsState = useChatThreadsState()
const accessor = useAccessor()
const threadsStateService = accessor.get('IThreadHistoryService')
const chatThreadsService = accessor.get('IChatThreadService')
const sidebarStateService = accessor.get('ISidebarStateService')
const { allThreads } = threadsState
@ -31,63 +31,87 @@ export const SidebarThreadSelector = () => {
const sortedThreadIds = Object.keys(allThreads ?? {}).sort((threadId1, threadId2) => allThreads![threadId1].lastModified > allThreads![threadId2].lastModified ? -1 : 1)
return (
<div className="flex flex-col gap-y-1 max-h-[400px] overflow-y-auto">
<div className="flex p-2 flex-col mb-2 gap-y-1 max-h-[400px] overflow-y-auto">
{/* X button at top right */}
<div className="text-right">
<button className="btn btn-sm" onClick={() => sidebarStateService.setState({ isHistoryOpen: false })}>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
className="size-4"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M6 18 18 6M6 6l12 12"
/>
</svg>
<div className="w-full relative flex justify-center items-center">
{/* title */}
<h2 className='font-bold text-lg'>{`History`}</h2>
{/* X button at top right */}
<button
type='button'
className='absolute top-0 right-0'
onClick={() => sidebarStateService.setState({ isHistoryOpen: false })}
>
<IconX
size={16}
className="p-[1px] stroke-[2] opacity-80 text-void-fg-3 hover:brightness-95"
/>
</button>
</div>
{/* a list of all the past threads */}
<div className='flex flex-col gap-y-1 overflow-y-auto'>
{sortedThreadIds.map((threadId) => {
if (!allThreads)
return <>Error: Threads not found.</>
const pastThread = allThreads[threadId]
<div className="px-1">
<ul className="flex flex-col gap-y-0.5 overflow-y-auto list-disc">
let btnStringArr: string[] = []
{sortedThreadIds.length === 0
const firstMsgIdx = allThreads[threadId].messages.findIndex(msg => msg.role !== 'system' && !!msg.displayContent) ?? ''
if (firstMsgIdx !== -1)
btnStringArr.push(truncate(allThreads[threadId].messages[firstMsgIdx].displayContent ?? ''))
else
btnStringArr.push('""')
? <div key="nothreads" className="text-center text-void-fg-3 brightness-90 text-sm">{`There are no chat threads yet.`}</div>
const secondMsgIdx = allThreads[threadId].messages.findIndex((msg, i) => msg.role !== 'system' && !!msg.displayContent && i > firstMsgIdx) ?? ''
if (secondMsgIdx !== -1)
btnStringArr.push(truncate(allThreads[threadId].messages[secondMsgIdx].displayContent ?? ''))
: sortedThreadIds.map((threadId) => {
if (!allThreads) {
return <li key="error" className="text-void-warning">{`Error accessing chat history.`}</li>;
}
const numMessagesRemaining = allThreads[threadId].messages.filter((msg, i) => msg.role !== 'system' && !!msg.displayContent && i > secondMsgIdx).length
if (numMessagesRemaining > 0)
btnStringArr.push(numMessagesRemaining + '')
const pastThread = allThreads[threadId];
let firstMsg = null;
// let secondMsg = null;
const btnString = btnStringArr.join(' / ')
const firstMsgIdx = pastThread.messages.findIndex(
(msg) => msg.role !== 'system' && !!msg.displayContent
);
return (
<button
key={pastThread.id}
className={`rounded-sm`}
onClick={() => threadsStateService.switchToThread(pastThread.id)}
title={new Date(pastThread.createdAt).toLocaleString()}
>
{btnString}
</button>
)
})}
if (firstMsgIdx !== -1) {
// firstMsg = truncate(pastThread.messages[firstMsgIdx].displayContent ?? '');
firstMsg = pastThread.messages[firstMsgIdx].displayContent ?? '';
} else {
firstMsg = '""';
}
// const secondMsgIdx = pastThread.messages.findIndex(
// (msg, i) => msg.role !== 'system' && !!msg.displayContent && i > firstMsgIdx
// );
// if (secondMsgIdx !== -1) {
// secondMsg = truncate(pastThread.messages[secondMsgIdx].displayContent ?? '');
// }
const numMessages = pastThread.messages.filter(
(msg) => msg.role !== 'system'
).length;
return (
<li key={pastThread.id}>
<button
type='button'
className={`
hover:bg-void-bg-1
${threadsState.currentThreadId === pastThread.id ? 'bg-void-bg-1' : ''}
rounded-sm px-2 py-1
w-full
text-left
flex items-center
`}
onClick={() => chatThreadsService.switchToThread(pastThread.id)}
title={new Date(pastThread.createdAt).toLocaleString()}
>
<div className='truncate'>{`${firstMsg}`}</div>
<div>{`\u00A0(${numMessages})`}</div>
</button>
</li>
);
})
}
</ul>
</div>
</div>

View file

@ -20,6 +20,16 @@
.inherit-bg-all-restyle > * {
background-color: inherit !important;
}
.bg-editor-style-override {
--vscode-sideBar-background: var(--vscode-editor-background);
}
/* html {
font-size: var(--vscode-font-size);

View file

@ -0,0 +1,19 @@
import { useCallback, useRef, useState } from 'react'
type ReturnType<T> = [
{ readonly current: T },
(t: T) => void
]
// use this if state might be too slow to catch
export const useRefState = <T,>(initVal: T): ReturnType<T> => {
const [_, _setState] = useState(false)
const ref = useRef<T>(initVal)
const setState = useCallback((newVal: T) => {
_setState(n => !n) // call rerender
ref.current = newVal
}, [])
return [ref, setState]
}

View file

@ -3,7 +3,7 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import React, { useCallback, useEffect, useRef } from 'react';
import React, { forwardRef, MutableRefObject, useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
import { IInputBoxStyles, InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js';
import { defaultCheckboxStyles, defaultInputBoxStyles, defaultSelectBoxStyles } from '../../../../../../../platform/theme/browser/defaultStyles.js';
import { SelectBox } from '../../../../../../../base/browser/ui/selectBox/selectBox.js';
@ -12,6 +12,9 @@ import { Checkbox } from '../../../../../../../base/browser/ui/toggle/toggle.js'
import { CodeEditorWidget } from '../../../../../../../editor/browser/widget/codeEditor/codeEditorWidget.js'
import { useAccessor } from './services.js';
import { ITextModel } from '../../../../../../../editor/common/model.js';
import { asCssVariable } from '../../../../../../../platform/theme/common/colorUtils.js';
import { inputBackground, inputForeground } from '../../../../../../../platform/theme/common/colorRegistry.js';
// type guard
@ -45,8 +48,105 @@ export const WidgetComponent = <CtorParams extends any[], Instance>({ ctor, prop
}
export type TextAreaFns = { setValue: (v: string) => void, enable: () => void, disable: () => void }
type InputBox2Props = {
initValue?: string | null;
placeholder: string;
multiline: boolean;
fnsRef?: { current: null | TextAreaFns };
className?: string;
onChangeText?: (value: string) => void;
onKeyDown?: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
onChangeHeight?: (newHeight: number) => void;
}
export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(function X({ initValue, placeholder, multiline, fnsRef, className, onKeyDown, onChangeText }, ref) {
export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, placeholder, multiline, styles }: {
// mirrors whatever is in ref
const textAreaRef = useRef<HTMLTextAreaElement | null>(null)
const [isEnabled, setEnabled] = useState(true)
const adjustHeight = useCallback(() => {
const r = textAreaRef.current
if (!r) return
r.style.height = 'auto' // set to auto to reset height, then set to new height
if (r.scrollHeight === 0) return requestAnimationFrame(adjustHeight)
const h = r.scrollHeight
const newHeight = Math.min(h + 1, 500) // plus one to avoid scrollbar appearing when it shouldn't
r.style.height = `${newHeight}px`
}, []);
const fns: TextAreaFns = useMemo(() => ({
setValue: (val) => {
const r = textAreaRef.current
if (!r) return
r.value = val
onChangeText?.(r.value)
adjustHeight()
},
enable: () => { setEnabled(true) },
disable: () => { setEnabled(false) },
}), [onChangeText, adjustHeight])
useEffect(() => {
if (initValue)
fns.setValue(initValue)
}, [initValue])
return (
<textarea
ref={useCallback((r: HTMLTextAreaElement | null) => {
if (fnsRef)
fnsRef.current = fns
textAreaRef.current = r
if (typeof ref === 'function') ref(r)
else if (ref) ref.current = r
adjustHeight()
}, [fnsRef, fns, setEnabled, adjustHeight, ref])}
disabled={!isEnabled}
className={`w-full resize-none max-h-[500px] overflow-y-auto text-void-fg-1 placeholder:text-void-fg-3 ${className}`}
style={{
// defaultInputBoxStyles
background: asCssVariable(inputBackground),
color: asCssVariable(inputForeground)
// inputBorder: asCssVariable(inputBorder),
}}
onChange={useCallback(() => {
const r = textAreaRef.current
if (!r) return
onChangeText?.(r.value)
adjustHeight()
}, [onChangeText, adjustHeight])}
onKeyDown={useCallback((e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter') {
// Shift + Enter when multiline = newline
const shouldAddNewline = e.shiftKey && multiline
if (!shouldAddNewline) e.preventDefault(); // prevent newline from being created
}
onKeyDown?.(e)
}, [onKeyDown, multiline])}
rows={1}
placeholder={placeholder}
/>
)
})
export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, placeholder, multiline }: {
onChangeText: (value: string) => void;
styles?: Partial<IInputBoxStyles>,
onCreateInstance?: (instance: InputBox) => void | IDisposable[];
@ -60,6 +160,10 @@ export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, plac
const contextViewProvider = accessor.get('IContextViewService')
return <WidgetComponent
ctor={InputBox}
className='
bg-void-bg-1
@@[&_::placeholder]:!void-text-void-fg-3
'
propsFn={useCallback((container) => [
container,
contextViewProvider,
@ -69,7 +173,6 @@ export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, plac
inputForeground: "var(--vscode-foreground)",
// inputBackground: 'transparent',
// inputBorder: 'none',
...styles,
},
placeholder,
tooltip: '',
@ -193,11 +296,225 @@ export const VoidCheckBox = ({ label, value, onClick, className }: { label: stri
}
export const VoidSelectBox = <T,>({ onChangeSelection, onCreateInstance, selectBoxRef, options }: {
export const VoidCustomSelectBox = <T extends any>({
options,
selectedOption: selectedOption_,
onChangeOption,
getOptionDropdownName,
getOptionDisplayName,
getOptionsEqual,
className,
arrowTouchesText = true,
matchInputWidth = false,
isMenuPositionFixed = true,
gap = 0,
}: {
options: T[];
selectedOption?: T;
onChangeOption: (newValue: T) => void;
getOptionDropdownName: (option: T) => string;
getOptionDisplayName: (option: T) => string;
getOptionsEqual: (a: T, b: T) => boolean;
className?: string;
arrowTouchesText?: boolean;
matchInputWidth?: boolean;
isMenuPositionFixed?: boolean;
gap?: number;
}) => {
const [isOpen, setIsOpen] = useState(false);
const [readyToShow, setReadyToShow] = useState(false);
const [position, setPosition] = useState({ top: 0, left: 0, width: 0 });
const containerRef = useRef<HTMLDivElement | null>(null);
const buttonRef = useRef<HTMLButtonElement | null>(null);
const measureRef = useRef<HTMLDivElement | null>(null);
// if the selected option is null, use the 0th option as the selected, and set the option to options[0]
useEffect(() => {
if (!options[0]) return
if (!selectedOption_) {
onChangeOption(options[0]);
}
}, [selectedOption_, options])
const selectedOption = !selectedOption_ ? options[0] : selectedOption_
const updatePosition = useCallback(() => {
if (!buttonRef.current || !containerRef.current || !measureRef.current) return;
const buttonRect = buttonRef.current.getBoundingClientRect();
const containerRect = containerRef.current.getBoundingClientRect();
const containerWidth = containerRef.current.offsetWidth;
const viewportHeight = window.innerHeight;
const spaceBelow = viewportHeight - buttonRect.bottom;
const spaceNeeded = options.length * 28;
const showAbove = spaceBelow < spaceNeeded && buttonRect.top > spaceBelow;
// Calculate the menu width
let menuWidth = matchInputWidth ? containerWidth : buttonRect.width;
// If not matchInputWidth, calculate content width from measurement div
if (!matchInputWidth) {
const contentWidth = measureRef.current.offsetWidth;
menuWidth = Math.max(buttonRect.width, contentWidth);
}
if (isMenuPositionFixed) {
// Fixed positioning (relative to viewport)
setPosition({
top: showAbove
? buttonRect.top - spaceNeeded
: buttonRect.bottom + gap,
left: buttonRect.left,
width: menuWidth,
});
} else {
// Absolute positioning (relative to parent container)
setPosition({
top: showAbove
? -(spaceNeeded + gap)
: buttonRect.height + gap,
left: 0,
width: menuWidth,
});
}
setReadyToShow(true);
}, [gap, matchInputWidth, options.length, isMenuPositionFixed]);
useEffect(() => {
if (isOpen) {
setReadyToShow(false);
updatePosition();
window.addEventListener('scroll', updatePosition, true);
window.addEventListener('resize', updatePosition);
return () => {
window.removeEventListener('scroll', updatePosition, true);
window.removeEventListener('resize', updatePosition);
};
} else {
setReadyToShow(false);
}
}, [isOpen, updatePosition]);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};
if (isOpen) {
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}
}, [isOpen]);
return (
<div
ref={containerRef}
className={`inline-block relative ${className}`}
>
{/* Hidden measurement div */}
<div
ref={measureRef}
className="opacity-0 pointer-events-none absolute -left-[999999px] -top-[999999px] flex flex-col"
aria-hidden="true"
>
{options.map((option) => (
<div key={getOptionDropdownName(option)} className="flex items-center whitespace-nowrap">
<div className="w-4" />
<span className="px-2">{getOptionDropdownName(option)}</span>
</div>
))}
</div>
{/* Select Button */}
<button
type='button'
ref={buttonRef}
className="flex items-center h-4 bg-transparent whitespace-nowrap hover:brightness-90 w-full"
onClick={() => {
setIsOpen(!isOpen);
}}
>
<span className={`max-w-[120px] truncate ${arrowTouchesText ? 'mr-1' : ''}`}>
{getOptionDisplayName(selectedOption)}
</span>
<svg
className={`size-3 flex-shrink-0 ${arrowTouchesText ? '' : 'ml-auto'}`}
viewBox="0 0 12 12"
fill="none"
>
<path
d="M2.5 4.5L6 8L9.5 4.5"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</button>
{/* Dropdown Menu */}
{isOpen && readyToShow && (
<div
className={`${isMenuPositionFixed ? 'fixed' : 'absolute'} z-10 bg-void-bg-1 border-void-border-1 border overflow-hidden rounded shadow-lg`}
style={{
top: position.top,
left: position.left,
width: position.width,
}}
>
{options.map((option) => {
const thisOptionIsSelected = getOptionsEqual(option, selectedOption);
const optionName = getOptionDropdownName(option);
return (
<div
key={optionName}
className={`flex items-center px-2 py-1 cursor-pointer whitespace-nowrap
transition-all duration-100
bg-void-bg-1
${thisOptionIsSelected ? 'bg-void-bg-2' : 'hover:bg-void-bg-2'}
`}
onClick={() => {
onChangeOption(option);
setIsOpen(false);
}}
>
<div className="w-4 flex justify-center flex-shrink-0">
{thisOptionIsSelected && (
<svg className="size-3" viewBox="0 0 12 12" fill="none">
<path
d="M10 3L4.5 8.5L2 6"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)}
</div>
<span>{optionName}</span>
</div>
);
})}
</div>
)}
</div>
);
};
export const _VoidSelectBox = <T,>({ onChangeSelection, onCreateInstance, selectBoxRef, options, className }: {
onChangeSelection: (value: T) => void;
onCreateInstance?: ((instance: SelectBox) => void | IDisposable[]);
selectBoxRef?: React.MutableRefObject<SelectBox | null>;
options: readonly { text: string, value: T }[];
className?: string;
}) => {
const accessor = useAccessor()
const contextViewProvider = accessor.get('IContextViewService')
@ -205,7 +522,13 @@ export const VoidSelectBox = <T,>({ onChangeSelection, onCreateInstance, selectB
let containerRef = useRef<HTMLDivElement | null>(null);
return <WidgetComponent
className='@@select-child-restyle'
className={`
@@select-child-restyle
@@[&_select]:!void-text-void-fg-3
@@[&_select]:!void-text-xs
!text-void-fg-3
${className ?? ''}
`}
ctor={SelectBox}
propsFn={useCallback((container) => {
containerRef.current = container
@ -214,14 +537,15 @@ export const VoidSelectBox = <T,>({ onChangeSelection, onCreateInstance, selectB
options.map(opt => ({ text: opt.text })),
defaultIndex,
contextViewProvider,
defaultSelectBoxStyles
defaultSelectBoxStyles,
] as const;
}, [containerRef, options, contextViewProvider])}
}, [containerRef, options])}
dispose={useCallback((instance: SelectBox) => {
instance.dispose();
for (let child of containerRef.current?.childNodes ?? [])
containerRef.current?.childNodes.forEach(child => {
containerRef.current?.removeChild(child)
})
}, [containerRef])}
onCreateInstance={useCallback((instance: SelectBox) => {
@ -286,24 +610,48 @@ const normalizeIndentation = (code: string): string => {
}
export const VoidCodeEditor = ({ initValue, language }: { initValue: string, language: string | undefined }) => {
const MAX_HEIGHT = Infinity;
const modelOfEditorId: { [id: string]: ITextModel | undefined } = {}
export type VoidCodeEditorProps = { initValue: string, language?: string, maxHeight?: number, showScrollbars?: boolean }
export const VoidCodeEditor = ({ initValue, language, maxHeight, showScrollbars }: VoidCodeEditorProps) => {
initValue = normalizeIndentation(initValue)
// default settings
const MAX_HEIGHT = maxHeight ?? Infinity;
const SHOW_SCROLLBARS = showScrollbars ?? false;
const divRef = useRef<HTMLDivElement | null>(null)
const accessor = useAccessor()
const instantiationService = accessor.get('IInstantiationService')
// const languageDetectionService = accessor.get('ILanguageDetectionService')
const modelService = accessor.get('IModelService')
const languageDetectionService = accessor.get('ILanguageDetectionService')
initValue = normalizeIndentation(initValue)
return <div ref={divRef}>
const id = useId()
// these are used to pass to the model creation of modelRef
const initValueRef = useRef(initValue)
const languageRef = useRef(language)
const modelRef = useRef<ITextModel | null>(null)
// if we change the initial value, don't re-render the whole thing, just set it here. same for language
useEffect(() => {
initValueRef.current = initValue
modelRef.current?.setValue(initValue)
}, [initValue])
useEffect(() => {
languageRef.current = language
if (language) modelRef.current?.setLanguage(language)
}, [language])
return <div ref={divRef} className='relative z-0 px-2 py-1 bg-void-bg-3'>
<WidgetComponent
className='relative z-0 text-sm bg-vscode-editor-bg'
ctor={useCallback((container) =>
instantiationService.createInstance(
className='@@bg-editor-style-override' // text-sm
ctor={useCallback((container) => {
return instantiationService.createInstance(
CodeEditorWidget,
container,
{
@ -312,10 +660,19 @@ export const VoidCodeEditor = ({ initValue, language }: { initValue: string, lan
scrollbar: {
alwaysConsumeMouseWheel: false,
vertical: 'hidden',
horizontal: 'hidden',
verticalScrollbarSize: 0,
horizontalScrollbarSize: 0,
...SHOW_SCROLLBARS ? {
vertical: 'auto',
verticalScrollbarSize: 8,
horizontal: 'auto',
horizontalScrollbarSize: 8,
} : {
vertical: 'hidden',
verticalScrollbarSize: 0,
horizontal: 'auto',
horizontalScrollbarSize: 8,
ignoreHorizontalScrollbarInContentHeight: true,
},
},
scrollBeyondLastLine: false,
@ -347,27 +704,25 @@ export const VoidCodeEditor = ({ initValue, language }: { initValue: string, lan
{
isSimpleWidget: true,
})
, [instantiationService])
}
}, [instantiationService])}
onCreateInstance={useCallback((editor: CodeEditorWidget) => {
const model = modelService.createModel(
initValue,
language ? {
languageId: language,
onDidChange: () => ({
dispose: () => { }
})
} : null
);
const model = modelOfEditorId[id] ?? modelService.createModel(
initValueRef.current, {
languageId: languageRef.current ? languageRef.current : 'typescript',
onDidChange: (e) => { return { dispose: () => { } } } // no idea why they'd require this
})
modelRef.current = model
editor.setModel(model);
const container = editor.getDomNode()
const parentNode = container?.parentElement
const resize = () => {
const height = editor.getScrollHeight() + 1
if (parentNode) {
const height = Math.min(editor.getScrollHeight() + 1, MAX_HEIGHT);
// const height = Math.min(, MAX_HEIGHT);
parentNode.style.height = `${height}px`;
parentNode.style.maxHeight = `${MAX_HEIGHT}px`;
editor.layout();
}
}
@ -375,12 +730,12 @@ export const VoidCodeEditor = ({ initValue, language }: { initValue: string, lan
resize()
const disposable = editor.onDidContentSizeChange(() => { resize() });
return [disposable]
}, [modelService, initValue, language])}
return [disposable, model]
}, [modelService])}
dispose={useCallback((editor: CodeEditorWidget) => {
editor.dispose();
}, [modelService, languageDetectionService])}
}, [modelService])}
propsFn={useCallback(() => { return [] }, [])}
/>
@ -389,6 +744,13 @@ export const VoidCodeEditor = ({ initValue, language }: { initValue: string, lan
}
export const VoidButton = ({ children, disabled, onClick }: { children: React.ReactNode; disabled?: boolean; onClick: () => void }) => {
return <button disabled={disabled}
className='px-3 py-1 bg-black/10 dark:bg-gray-200/10 rounded-sm overflow-hidden'
onClick={onClick}
>{children}</button>
}
// export const VoidScrollableElt = ({ options, children }: { options: ScrollableElementCreationOptions, children: React.ReactNode }) => {
// const instanceRef = useRef<DomScrollableElement | null>(null);
// const [childrenPortal, setChildrenPortal] = useState<React.ReactNode | null>(null)

View file

@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------*/
import React, { useState, useEffect } from 'react'
import { ThreadsState } from '../../../threadHistoryService.js'
import { ThreadStreamState, ThreadsState } from '../../../chatThreadService.js'
import { RefreshableProviderName, SettingsOfProvider } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
import { IDisposable } from '../../../../../../../base/common/lifecycle.js'
import { VoidSidebarState } from '../../../sidebarStateService.js'
@ -30,7 +30,7 @@ import { IVoidSettingsService } from '../../../../../../../platform/void/common/
import { IInlineDiffsService } from '../../../inlineDiffsService.js';
import { IQuickEditStateService } from '../../../quickEditStateService.js';
import { ISidebarStateService } from '../../../sidebarStateService.js';
import { IThreadHistoryService } from '../../../threadHistoryService.js';
import { IChatThreadService } from '../../../chatThreadService.js';
import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js'
import { ICodeEditorService } from '../../../../../../../editor/browser/services/codeEditorService.js'
import { ICommandService } from '../../../../../../../platform/commands/common/commands.js'
@ -40,7 +40,11 @@ import { IAccessibilityService } from '../../../../../../../platform/accessibili
import { ILanguageConfigurationService } from '../../../../../../../editor/common/languages/languageConfigurationRegistry.js'
import { ILanguageFeaturesService } from '../../../../../../../editor/common/services/languageFeatures.js'
import { ILanguageDetectionService } from '../../../../../../services/languageDetection/common/languageDetectionWorkerService.js'
import { IKeybindingService } from '../../../../../../../platform/keybinding/common/keybinding.js'
import { IEnvironmentService } from '../../../../../../../platform/environment/common/environment.js'
import { IConfigurationService } from '../../../../../../../platform/configuration/common/configuration.js'
import { IPathService } from '../../../../../../../workbench/services/path/common/pathService.js'
import { IMetricsService } from '../../../../../../../platform/void/common/metricsService.js'
// normally to do this you'd use a useEffect that calls .onDidChangeState(), but useEffect mounts too late and misses initial state changes
@ -53,8 +57,11 @@ const quickEditStateListeners: Set<(s: VoidQuickEditState) => void> = new Set()
let sidebarState: VoidSidebarState
const sidebarStateListeners: Set<(s: VoidSidebarState) => void> = new Set()
let threadsState: ThreadsState
const threadsStateListeners: Set<(s: ThreadsState) => void> = new Set()
let chatThreadsState: ThreadsState
const chatThreadsStateListeners: Set<(s: ThreadsState) => void> = new Set()
let chatThreadsStreamState: ThreadStreamState
const chatThreadsStreamStateListeners: Set<(threadId: string) => void> = new Set()
let settingsState: VoidSettingsState
const settingsStateListeners: Set<(s: VoidSettingsState) => void> = new Set()
@ -85,13 +92,14 @@ export const _registerServices = (accessor: ServicesAccessor) => {
const stateServices = {
quickEditStateService: accessor.get(IQuickEditStateService),
sidebarStateService: accessor.get(ISidebarStateService),
threadsStateService: accessor.get(IThreadHistoryService),
chatThreadsStateService: accessor.get(IChatThreadService),
settingsStateService: accessor.get(IVoidSettingsService),
refreshModelService: accessor.get(IRefreshModelService),
themeService: accessor.get(IThemeService),
inlineDiffsService: accessor.get(IInlineDiffsService),
}
const { sidebarStateService, quickEditStateService, settingsStateService, threadsStateService, refreshModelService, themeService, } = stateServices
const { sidebarStateService, quickEditStateService, settingsStateService, chatThreadsStateService, refreshModelService, themeService, inlineDiffsService } = stateServices
quickEditState = quickEditStateService.state
disposables.push(
@ -109,11 +117,20 @@ export const _registerServices = (accessor: ServicesAccessor) => {
})
)
threadsState = threadsStateService.state
chatThreadsState = chatThreadsStateService.state
disposables.push(
threadsStateService.onDidChangeCurrentThread(() => {
threadsState = threadsStateService.state
threadsStateListeners.forEach(l => l(threadsState))
chatThreadsStateService.onDidChangeCurrentThread(() => {
chatThreadsState = chatThreadsStateService.state
chatThreadsStateListeners.forEach(l => l(chatThreadsState))
})
)
// same service, different state
chatThreadsStreamState = chatThreadsStateService.streamState
disposables.push(
chatThreadsStateService.onDidChangeStreamState(({ threadId }) => {
chatThreadsStreamState = chatThreadsStateService.streamState
chatThreadsStreamStateListeners.forEach(l => l(threadId))
})
)
@ -142,6 +159,7 @@ export const _registerServices = (accessor: ServicesAccessor) => {
})
)
return disposables
}
@ -162,7 +180,7 @@ const getReactAccessor = (accessor: ServicesAccessor) => {
IInlineDiffsService: accessor.get(IInlineDiffsService),
IQuickEditStateService: accessor.get(IQuickEditStateService),
ISidebarStateService: accessor.get(ISidebarStateService),
IThreadHistoryService: accessor.get(IThreadHistoryService),
IChatThreadService: accessor.get(IChatThreadService),
IInstantiationService: accessor.get(IInstantiationService),
ICodeEditorService: accessor.get(ICodeEditorService),
@ -173,6 +191,12 @@ const getReactAccessor = (accessor: ServicesAccessor) => {
ILanguageConfigurationService: accessor.get(ILanguageConfigurationService),
ILanguageDetectionService: accessor.get(ILanguageDetectionService),
ILanguageFeaturesService: accessor.get(ILanguageFeaturesService),
IKeybindingService: accessor.get(IKeybindingService),
IEnvironmentService: accessor.get(IEnvironmentService),
IConfigurationService: accessor.get(IConfigurationService),
IPathService: accessor.get(IPathService),
IMetricsService: accessor.get(IMetricsService),
} as const
return reactAccessor
@ -230,17 +254,36 @@ export const useSettingsState = () => {
return s
}
export const useThreadsState = () => {
const [s, ss] = useState(threadsState)
export const useChatThreadsState = () => {
const [s, ss] = useState(chatThreadsState)
useEffect(() => {
ss(threadsState)
threadsStateListeners.add(ss)
return () => { threadsStateListeners.delete(ss) }
ss(chatThreadsState)
chatThreadsStateListeners.add(ss)
return () => { chatThreadsStateListeners.delete(ss) }
}, [ss])
return s
}
export const useChatThreadsStreamState = (threadId: string) => {
const [s, ss] = useState<ThreadStreamState[string] | undefined>(chatThreadsStreamState[threadId])
useEffect(() => {
ss(chatThreadsStreamState[threadId])
const listener = (threadId_: string) => {
if (threadId_ !== threadId) return
ss(chatThreadsStreamState[threadId])
}
chatThreadsStreamStateListeners.add(listener)
return () => { chatThreadsStreamStateListeners.delete(listener) }
}, [ss, threadId])
return s
}
export const useRefreshModelState = () => {
const [s, ss] = useState(refreshModelState)
useEffect(() => {

View file

@ -0,0 +1,101 @@
import { useEffect } from 'react';
export const useScrollbarStyles = (containerRef: React.MutableRefObject<HTMLDivElement | null>) => {
useEffect(() => {
if (!containerRef.current) return;
// Create selector for specific overflow classes
const overflowSelector = [
'[class*="overflow-auto"]',
'[class*="overflow-x-auto"]',
'[class*="overflow-y-auto"]'
].join(',');
// Get all matching elements within the container, including the container itself
const scrollElements = [
...(containerRef.current.matches(overflowSelector) ? [containerRef.current] : []),
...Array.from(containerRef.current.querySelectorAll(overflowSelector))
];
// Apply styles and listeners to each scroll element
scrollElements.forEach(element => {
// Add the scrollable class directly to the overflow element
element.classList.add('void-scrollable-element');
let fadeTimeout: NodeJS.Timeout | null = null;
let fadeInterval: NodeJS.Timeout | null = null;
const fadeIn = () => {
if (fadeInterval) clearInterval(fadeInterval);
let step = 0;
fadeInterval = setInterval(() => {
if (step <= 10) {
element.classList.remove(`show-scrollbar-${step - 1}`);
element.classList.add(`show-scrollbar-${step}`);
step++;
} else {
clearInterval(fadeInterval!);
}
}, 10);
};
const fadeOut = () => {
if (fadeInterval) clearInterval(fadeInterval);
let step = 10;
fadeInterval = setInterval(() => {
if (step >= 0) {
element.classList.remove(`show-scrollbar-${step + 1}`);
element.classList.add(`show-scrollbar-${step}`);
step--;
} else {
clearInterval(fadeInterval!);
}
}, 60);
};
const onMouseEnter = () => {
if (fadeTimeout) clearTimeout(fadeTimeout);
if (fadeInterval) clearInterval(fadeInterval);
fadeIn();
};
const onMouseLeave = () => {
if (fadeTimeout) clearTimeout(fadeTimeout);
fadeTimeout = setTimeout(() => {
fadeOut();
}, 10);
};
element.addEventListener('mouseenter', onMouseEnter);
element.addEventListener('mouseleave', onMouseLeave);
// Store cleanup function
const cleanup = () => {
element.removeEventListener('mouseenter', onMouseEnter);
element.removeEventListener('mouseleave', onMouseLeave);
if (fadeTimeout) clearTimeout(fadeTimeout);
if (fadeInterval) clearInterval(fadeInterval);
element.classList.remove('void-scrollable-element');
// Remove any remaining show-scrollbar classes
for (let i = 0; i <= 10; i++) {
element.classList.remove(`show-scrollbar-${i}`);
}
};
// Store the cleanup function on the element for later use
(element as any).__scrollbarCleanup = cleanup;
});
return () => {
// Clean up all scroll elements
scrollElements.forEach(element => {
if ((element as any).__scrollbarCleanup) {
(element as any).__scrollbarCleanup();
}
});
};
}, [containerRef]);
};

View file

@ -6,10 +6,10 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { FeatureName, featureNames, ModelSelection, modelSelectionsEqual, ProviderName, providerNames } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
import { useSettingsState, useRefreshModelState, useAccessor } from '../util/services.js'
import { VoidSelectBox } from '../util/inputs.js'
import { _VoidSelectBox, VoidCustomSelectBox } from '../util/inputs.js'
import { SelectBox } from '../../../../../../../base/browser/ui/selectBox/selectBox.js'
import { IconWarning } from '../sidebar-tsx/SidebarChat.js'
import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js'
import { VOID_OPEN_SETTINGS_ACTION_ID, VOID_TOGGLE_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js'
import { ModelOption } from '../../../../../../../platform/void/common/voidSettingsService.js'
@ -17,42 +17,64 @@ import { ModelOption } from '../../../../../../../platform/void/common/voidSetti
const optionsEqual = (m1: ModelOption[], m2: ModelOption[]) => {
if (m1.length !== m2.length) return false
for (let i = 0; i < m1.length; i++) {
if (!modelSelectionsEqual(m1[i].value, m2[i].value)) return false
if (!modelSelectionsEqual(m1[i].selection, m2[i].selection)) return false
}
return true
}
const ModelSelectBox = ({ options, featureName }: { options: ModelOption[], featureName: FeatureName }) => {
const accessor = useAccessor()
const voidSettingsService = accessor.get('IVoidSettingsService')
let weChangedText = false
const selection = voidSettingsService.state.modelSelectionOfFeature[featureName]
const selectedOption = selection ? voidSettingsService.state._modelOptions.find(v => modelSelectionsEqual(v.selection, selection)) : options[0]
return <VoidSelectBox
const onChangeOption = useCallback((newOption: ModelOption) => {
voidSettingsService.setModelSelectionOfFeature(featureName, newOption.selection)
}, [voidSettingsService, featureName])
return <VoidCustomSelectBox
options={options}
onChangeSelection={useCallback((newVal: ModelSelection) => {
if (weChangedText) return
voidSettingsService.setModelSelectionOfFeature(featureName, newVal)
}, [voidSettingsService, featureName])}
// we are responsible for setting the initial state here. always sync instance when state changes.
onCreateInstance={useCallback((instance: SelectBox) => {
const syncInstance = () => {
const modelsListRef = voidSettingsService.state._modelOptions // as a ref
const settingsAtProvider = voidSettingsService.state.modelSelectionOfFeature[featureName]
const selectionIdx = settingsAtProvider === null ? -1 : modelsListRef.findIndex(v => modelSelectionsEqual(v.value, settingsAtProvider))
weChangedText = true
instance.select(selectionIdx === -1 ? 0 : selectionIdx)
weChangedText = false
}
syncInstance()
const disposable = voidSettingsService.onDidChangeState(syncInstance)
return [disposable]
}, [voidSettingsService, featureName])}
selectedOption={selectedOption}
onChangeOption={onChangeOption}
getOptionDisplayName={(option) => option.selection.modelName}
getOptionDropdownName={(option) => option.name}
getOptionsEqual={(a, b) => optionsEqual([a], [b])}
className={`text-xs text-void-fg-3 px-1`}
matchInputWidth={false}
isMenuPositionFixed={featureName === 'Ctrl+K' ? false : true}
/>
}
// const ModelSelectBox = ({ options, featureName }: { options: ModelOption[], featureName: FeatureName }) => {
// const accessor = useAccessor()
// const voidSettingsService = accessor.get('IVoidSettingsService')
// let weChangedText = false
// return <VoidSelectBox
// className='@@[&_select]:!void-text-xs text-void-fg-3'
// options={options}
// onChangeSelection={useCallback((newVal: ModelSelection) => {
// if (weChangedText) return
// voidSettingsService.setModelSelectionOfFeature(featureName, newVal)
// }, [voidSettingsService, featureName])}
// // we are responsible for setting the initial state here. always sync instance when state changes.
// onCreateInstance={useCallback((instance: SelectBox) => {
// const syncInstance = () => {
// const modelsListRef = voidSettingsService.state._modelOptions // as a ref
// const settingsAtProvider = voidSettingsService.state.modelSelectionOfFeature[featureName]
// const selectionIdx = settingsAtProvider === null ? -1 : modelsListRef.findIndex(v => modelSelectionsEqual(v.value, settingsAtProvider))
// weChangedText = true
// instance.select(selectionIdx === -1 ? 0 : selectionIdx)
// weChangedText = false
// }
// syncInstance()
// const disposable = voidSettingsService.onDidChangeState(syncInstance)
// return [disposable]
// }, [voidSettingsService, featureName])}
// />
// }
const MemoizedModelSelectBox = ({ featureName }: { featureName: FeatureName }) => {
const settingsState = useSettingsState()
@ -71,30 +93,23 @@ const MemoizedModelSelectBox = ({ featureName }: { featureName: FeatureName }) =
}
const DummySelectBox = () => {
const accessor = useAccessor()
const comandService = accessor.get('ICommandService')
const openSettings = () => {
comandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID);
};
export const WarningBox = ({ text, onClick, className }: { text: string; onClick?: () => void; className?: string }) => {
return <div
className={`
flex items-center
flex-nowrap text-ellipsis
text-vscode-charts-yellow
hover:brightness-90 transition-all duration-200
cursor-pointer
text-void-warning brightness-90 opacity-90
text-xs text-ellipsis
${onClick ? `hover:brightness-75 transition-all duration-200 cursor-pointer` : ''}
flex items-center flex-nowrap
${className}
`}
onClick={openSettings}
onClick={onClick}
>
<IconWarning
size={20}
className='mr-1 brightness-90'
size={14}
className='mr-1'
/>
<span>Model required</span>
<span>{text}</span>
</div>
// return <VoidSelectBox
// options={[{ text: 'Please add a model!', value: null }]}
@ -104,7 +119,16 @@ const DummySelectBox = () => {
export const ModelDropdown = ({ featureName }: { featureName: FeatureName }) => {
const settingsState = useSettingsState()
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
const openSettings = () => { commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID); };
return <>
{settingsState._modelOptions.length === 0 ? <DummySelectBox /> : <MemoizedModelSelectBox featureName={featureName} />}
{settingsState._modelOptions.length === 0 ?
<WarningBox onClick={openSettings} text='Provider required' />
: <MemoizedModelSelectBox featureName={featureName} />
}
</>
}

View file

@ -5,20 +5,25 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js'
import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidModelInfo, featureFlagNames, displayInfoOfFeatureFlag, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, defaultProviderSettings, nonlocalProviderNames, localProviderNames } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidModelInfo, globalSettingNames, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, defaultProviderSettings, nonlocalProviderNames, localProviderNames, GlobalSettingName } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
import { VoidCheckBox, VoidInputBox, VoidSelectBox, VoidSwitch } from '../util/inputs.js'
import { VoidButton, VoidCheckBox, VoidCustomSelectBox, VoidInputBox, VoidInputBox2, VoidSwitch } from '../util/inputs.js'
import { useAccessor, useIsDark, useRefreshModelListener, useRefreshModelState, useSettingsState } from '../util/services.js'
import { X, RefreshCw, Loader2, Check } from 'lucide-react'
import { X, RefreshCw, Loader2, Check, MoveRight } from 'lucide-react'
import { useScrollbarStyles } from '../util/useScrollbarStyles.js'
import { isWindows, isLinux, isMacintosh } from '../../../../../../../base/common/platform.js'
import { URI } from '../../../../../../../base/common/uri.js'
import { env } from '../../../../../../../base/common/process.js'
import { WarningBox } from './ModelDropdown.js'
import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js'
const SubtleButton = ({ onClick, text, icon, disabled }: { onClick: () => void, text: string, icon: React.ReactNode, disabled: boolean }) => {
return <div className='flex items-center px-3 rounded-sm overflow-hidden gap-2 hover:bg-black/10 dark:hover:bg-gray-300/10'>
return <div className='flex items-center text-void-fg-3 mb-1 px-3 rounded-sm overflow-hidden gap-2 hover:bg-black/10 dark:hover:bg-gray-300/10'>
<button className='flex items-center' disabled={disabled} onClick={onClick}>
{icon}
</button>
<span className='opacity-50'>
<span>
{text}
</span>
</div>
@ -31,30 +36,41 @@ const RefreshModelButton = ({ providerName }: { providerName: RefreshableProvide
const accessor = useAccessor()
const refreshModelService = accessor.get('IRefreshModelService')
const metricsService = accessor.get('IMetricsService')
const [justFinished, setJustFinished] = useState(false)
const [justFinished, setJustFinished] = useState<null | 'finished' | 'error'>(null)
useRefreshModelListener(
useCallback((providerName2, refreshModelState) => {
if (providerName2 !== providerName) return
const { state } = refreshModelState[providerName]
if (state !== 'finished') return
if (!(state === 'finished' || state === 'error')) return
// now we know we just entered 'finished' state for this providerName
setJustFinished(true)
const tid = setTimeout(() => { setJustFinished(false) }, 2000)
setJustFinished(state)
const tid = setTimeout(() => { setJustFinished(null) }, 2000)
return () => clearTimeout(tid)
}, [providerName])
)
const { state } = refreshModelState[providerName]
const isRefreshing = state === 'refreshing'
const { title: providerTitle } = displayInfoOfProviderName(providerName)
return <SubtleButton
onClick={() => { refreshModelService.refreshModels(providerName) }}
text={justFinished ? `${providerTitle} Models are up-to-date!` : `Refresh Models List for ${providerTitle}.`}
icon={isRefreshing ? <Loader2 className='size-3 animate-spin' /> : (justFinished ? <Check className='stroke-green-500 size-3' /> : <RefreshCw className='size-3' />)}
disabled={isRefreshing || justFinished}
onClick={() => {
refreshModelService.startRefreshingModels(providerName, { enableProviderOnSuccess: false, doNotFire: false })
metricsService.capture('Click', { providerName, action: 'Refresh Models' })
}}
text={justFinished === 'finished' ? `${providerTitle} Models are up-to-date!`
: justFinished === 'error' ? `${providerTitle} not found!`
: `Manually refresh ${providerTitle} models.`
}
icon={justFinished === 'finished' ? <Check className='stroke-green-500 size-3' />
: justFinished === 'error' ? <X className='stroke-red-500 size-3' />
: state === 'refreshing' ? <Loader2 className='size-3 animate-spin' />
: <RefreshCw className='size-3' />
}
disabled={state === 'refreshing' || justFinished !== null}
/>
}
@ -64,7 +80,7 @@ const RefreshableModels = () => {
const buttons = refreshableProviderNames.map(providerName => {
if (!settingsState.settingsOfProvider[providerName]._enabled) return null
return <div key={providerName} className='pb-4' >
return <div key={providerName} className='pb-4'>
<RefreshModelButton providerName={providerName} />
</div>
})
@ -84,68 +100,75 @@ const AddModelMenu = ({ onSubmit }: { onSubmit: () => void }) => {
const settingsState = useSettingsState()
const providerNameRef = useRef<ProviderName | null>(null)
const modelNameRef = useRef<string | null>(null)
// const providerNameRef = useRef<ProviderName | null>(null)
const [providerName, setProviderName] = useState<ProviderName | null>(null)
const modelNameRef = useRef<HTMLTextAreaElement | null>(null)
const [errorString, setErrorString] = useState('')
const providerOptions = useMemo(() => providerNames.map(providerName => ({ text: displayInfoOfProviderName(providerName).title, value: providerName })), [providerNames])
return <>
<div className='flex items-center gap-4'>
{/* provider */}
<div className='max-w-40 w-full border border-vscode-editorwidget-border'>
<VoidSelectBox
<VoidCustomSelectBox
options={providerNames}
selectedOption={providerName}
onChangeOption={(pn) => setProviderName(pn)}
getOptionDisplayName={(pn) => pn ? displayInfoOfProviderName(pn).title : '(null)'}
getOptionDropdownName={(pn) => pn ? displayInfoOfProviderName(pn).title : '(null)'}
getOptionsEqual={(a, b) => a === b}
className={`max-w-44 w-full border border-void-border-2 bg-void-bg-1 text-void-fg-3 text-root
py-[4px] px-[6px]
`}
arrowTouchesText={false}
/>
{/* <_VoidSelectBox
onCreateInstance={useCallback(() => { providerNameRef.current = providerOptions[0].value }, [providerOptions])} // initialize state
onChangeSelection={useCallback((providerName: ProviderName) => { providerNameRef.current = providerName }, [])}
options={providerOptions}
/>
</div>
/> */}
{/* model */}
<div className='max-w-40 w-full border border-vscode-editorwidget-border'>
<VoidInputBox
<div className='max-w-44 w-full border border-void-border-2 bg-void-bg-1 text-void-fg-3 text-root'>
<VoidInputBox2
placeholder='Model Name'
onChangeText={useCallback((modelName) => { modelNameRef.current = modelName }, [])}
className='mt-[2px] px-[6px] h-full w-full'
ref={modelNameRef}
multiline={false}
/>
</div>
{/* button */}
<div className='max-w-40'>
<button
className='px-3 py-1 bg-black/10 dark:bg-gray-200/10 rounded-sm overflow-hidden'
onClick={() => {
const providerName = providerNameRef.current
const modelName = modelNameRef.current
<VoidButton onClick={() => {
const modelName = modelNameRef.current?.value
if (providerName === null) {
setErrorString('Please select a provider.')
return
}
if (!modelName) {
setErrorString('Please enter a model name.')
return
}
// if model already exists here
if (settingsState.settingsOfProvider[providerName].models.find(m => m.modelName === modelName)) {
setErrorString(`This model already exists under ${providerName}.`)
return
}
if (providerName === null) {
setErrorString('Please select a provider.')
return
}
if (!modelName) {
setErrorString('Please enter a model name.')
return
}
// if model already exists here
if (settingsState.settingsOfProvider[providerName].models.find(m => m.modelName === modelName)) {
setErrorString(`This model already exists under ${providerName}.`)
return
}
settingsStateService.addModel(providerName, modelName)
onSubmit()
settingsStateService.addModel(providerName, modelName)
onSubmit()
}}>Add model</button>
}}
>Add model</VoidButton>
</div>
{!errorString ? null : <div className='text-red-500 truncate whitespace-nowrap'>
{errorString}
</div>}
</div>
</>
@ -158,10 +181,7 @@ const AddModelMenuFull = () => {
return <div className='hover:bg-black/10 dark:hover:bg-gray-300/10 py-1 my-4 pb-1 px-3 rounded-sm overflow-hidden '>
{open ?
<AddModelMenu onSubmit={() => { setOpen(false) }} />
: <button
className='px-3 py-1 bg-black/10 dark:bg-gray-200/10 rounded-sm overflow-hidden'
onClick={() => setOpen(true)}
>Add Model</button>
: <VoidButton onClick={() => setOpen(true)}>Add Model</VoidButton>
}
</div>
}
@ -195,20 +215,25 @@ export const ModelDump = () => {
const disabled = !providerEnabled
return <div key={`${modelName}${providerName}`} className={`flex items-center justify-between gap-4 hover:bg-black/10 dark:hover:bg-gray-300/10 py-1 px-3 rounded-sm overflow-hidden cursor-default truncate ${isNewProviderName ? 'mt-4' : ''}`}>
return <div key={`${modelName}${providerName}`}
className={`flex items-center justify-between gap-4 hover:bg-black/10 dark:hover:bg-gray-300/10 py-1 px-3 rounded-sm overflow-hidden cursor-default truncate
`}
>
{/* left part is width:full */}
<div className={`w-full flex items-center gap-4`}>
<span className='min-w-40'>{isNewProviderName ? displayInfoOfProviderName(providerName).title : ''}</span>
<span>{modelName}</span>
<div className={`flex-grow flex items-center gap-4`}>
<span className='w-full max-w-32'>{isNewProviderName ? displayInfoOfProviderName(providerName).title : ''}</span>
<span className='w-fit truncate'>{modelName}</span>
{/* <span>{`${modelName} (${providerName})`}</span> */}
</div>
{/* right part is anything that fits */}
<div className='w-fit flex items-center gap-4'>
<span className='opacity-50'>{isAutodetected ? '(detected locally)' : isDefault ? '' : '(custom model)'}</span>
<div className='flex items-center gap-4'>
<span className='opacity-50 truncate'>{isAutodetected ? '(detected locally)' : isDefault ? '' : '(custom model)'}</span>
<VoidSwitch
value={disabled ? false : !isHidden}
onChange={() => { settingsStateService.toggleModelHidden(providerName, modelName) }}
onChange={() => {
settingsStateService.toggleModelHidden(providerName, modelName)
}}
disabled={disabled}
size='sm'
/>
@ -235,14 +260,15 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider
const accessor = useAccessor()
const voidSettingsService = accessor.get('IVoidSettingsService')
const voidMetricsService = accessor.get('IMetricsService')
let weChangedTextRef = false
return <ErrorBoundary>
<div className='my-1'>
<VoidInputBox
// placeholder={`${providerTitle} ${settingTitle} (${placeholder}).`}
placeholder={`${settingTitle} (${placeholder}).`}
// placeholder={`${providerTitle} ${settingTitle} (${placeholder})`}
placeholder={`${settingTitle} (${placeholder})`}
onChangeText={useCallback((newVal) => {
if (weChangedTextRef) return
voidSettingsService.setSettingOfProvider(providerName, settingName, newVal)
@ -268,10 +294,12 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider
if (shouldEnable) {
voidSettingsService.setSettingOfProvider(providerName, '_enabled', true)
voidMetricsService.capture('Enable Provider', { providerName })
}
if (shouldDisable) {
voidSettingsService.setSettingOfProvider(providerName, '_enabled', false)
voidMetricsService.capture('Disable Provider', { providerName })
}
}
@ -281,8 +309,8 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider
}, [voidSettingsService, providerName, settingName])}
multiline={false}
/>
{subTextMd === undefined ? null : <div className='py-1 px-3 opacity-50 text-xs'>
<ChatMarkdownRender string={subTextMd} />
{subTextMd === undefined ? null : <div className='py-1 px-3 opacity-50 text-sm'>
<ChatMarkdownRender noSpace string={subTextMd} />
</div>}
</div>
@ -334,61 +362,257 @@ export const VoidProviderSettings = ({ providerNames }: { providerNames: Provide
</>
}
// export const VoidFeatureFlagSettings = () => {
// const accessor = useAccessor()
// const voidSettingsService = accessor.get('IVoidSettingsService')
// const voidSettingsState = useSettingsState()
// return <>
// {featureFlagNames.map((flagName) => {
// const value = voidSettingsState.featureFlagSettings[flagName]
// const { description } = displayInfoOfFeatureFlag(flagName)
// return <div key={flagName} className='hover:bg-black/10 hover:dark:bg-gray-200/10 rounded-sm overflow-hidden py-1 px-3 my-1'>
// <div className='flex items-center'>
// <VoidCheckBox
// label=''
// value={value}
// onClick={() => { voidSettingsService.setFeatureFlag(flagName, !value) }}
// />
// <h4 className='text-sm'>{description}</h4>
// </div>
// </div>
// })}
// </>
// }
export const VoidFeatureFlagSettings = () => {
type TabName = 'models' | 'general'
export const AutoRefreshToggle = () => {
const settingName: GlobalSettingName = 'autoRefreshModels'
const accessor = useAccessor()
const voidSettingsService = accessor.get('IVoidSettingsService')
const metricsService = accessor.get('IMetricsService')
const voidSettingsState = useSettingsState()
return featureFlagNames.map((flagName) => {
// right now this is just `enabled_autoRefreshModels`
const enabled = voidSettingsState.globalSettings[settingName]
// right now this is just `enabled_autoRefreshModels`
const enabled = voidSettingsState.featureFlagSettings[flagName]
const { description } = displayInfoOfFeatureFlag(flagName)
return <SubtleButton key={flagName}
onClick={() => { voidSettingsService.setFeatureFlag(flagName, !enabled) }}
text={description}
icon={enabled ? <Check className='stroke-green-500 size-3' /> : <X className='stroke-red-500 size-3' />}
disabled={false}
/>
})
return <SubtleButton
onClick={() => {
voidSettingsService.setGlobalSetting(settingName, !enabled)
metricsService.capture('Click', { action: 'Autorefresh Toggle', settingName, enabled: !enabled })
}}
text={`Automatically detect local providers and models (${refreshableProviderNames.map(providerName => displayInfoOfProviderName(providerName).title).join(', ')}).`}
icon={enabled ? <Check className='stroke-green-500 size-3' /> : <X className='stroke-red-500 size-3' />}
disabled={false}
/>
}
export const AIInstructionsBox = () => {
const accessor = useAccessor()
const voidSettingsService = accessor.get('IVoidSettingsService')
const voidSettingsState = useSettingsState()
return <VoidInputBox2
className='min-h-[81px] p-3 rounded-sm'
initValue={voidSettingsState.globalSettings.aiInstructions}
placeholder={`Do not change my indentation or delete my comments. When writing TS or JS, do not add ;'s. Respond to all queries in French. `}
multiline
onChangeText={(newText) => {
voidSettingsService.setGlobalSetting('aiInstructions', newText)
}}
/>
}
export const FeaturesTab = () => {
return <>
<h2 className={`text-3xl mb-2`}>Local Providers</h2>
{/* <h3 className={`opacity-50 mb-2`}>{`Keep your data private by hosting AI locally on your computer.`}</h3> */}
{/* <h3 className={`opacity-50 mb-2`}>{`Instructions:`}</h3> */}
{/* <h3 className={`mb-2`}>{`Void can access any model that you host locally. We automatically detect your local models by default.`}</h3> */}
<h3 className={`text-void-fg-3 mb-2`}>{`Void can access any model that you host locally. We automatically detect your local models by default.`}</h3>
<div className='pl-4 opacity-50'>
<span className={`text-sm mb-2`}><ChatMarkdownRender noSpace string={`1. Download [Ollama](https://ollama.com/download).`} /></span>
<span className={`text-sm mb-2`}><ChatMarkdownRender noSpace string={`2. Open your terminal.`} /></span>
<span className={`text-sm mb-2 select-text`}><ChatMarkdownRender noSpace string={`3. Run \`ollama run llama3.1\`. This installs Meta's llama3.1 model which is best for chat and inline edits. Requires 5GB of memory.`} /></span>
<span className={`text-sm mb-2 select-text`}><ChatMarkdownRender noSpace string={`4. Run \`ollama run qwen2.5-coder:1.5b\`. This installs a faster autocomplete model. Requires 1GB of memory.`} /></span>
<span className={`text-sm mb-2`}><ChatMarkdownRender noSpace string={`Void automatically detects locally running models and enables them.`} /></span>
{/* TODO we should create UI for downloading models without user going into terminal */}
</div>
<ErrorBoundary>
<VoidProviderSettings providerNames={localProviderNames} />
</ErrorBoundary>
<h2 className={`text-3xl mb-2 mt-12`}>Providers</h2>
<h3 className={`text-void-fg-3 mb-2`}>{`Void can access models from Anthropic, OpenAI, OpenRouter, and more.`}</h3>
{/* <h3 className={`opacity-50 mb-2`}>{`Access models like ChatGPT and Claude. We recommend using Anthropic or OpenAI as providers, or Groq as a faster alternative.`}</h3> */}
<ErrorBoundary>
<VoidProviderSettings providerNames={nonlocalProviderNames} />
</ErrorBoundary>
<h2 className={`text-3xl mb-2 mt-12`}>Models</h2>
<ErrorBoundary>
<AutoRefreshToggle />
<RefreshableModels />
<ModelDump />
<AddModelMenuFull />
</ErrorBoundary>
</>
}
// https://github.com/VSCodium/vscodium/blob/master/docs/index.md#migrating-from-visual-studio-code-to-vscodium
// https://code.visualstudio.com/docs/editor/extension-marketplace#_where-are-extensions-installed
type TransferFilesInfo = { from: URI, to: URI }[]
const transferTheseFilesOfOS = (os: 'mac' | 'windows' | 'linux' | null): TransferFilesInfo => {
if (os === null)
throw new Error(`One-click switch is not possible in this environment.`)
if (os === 'mac') {
const homeDir = env['HOME']
if (!homeDir) throw new Error(`$HOME not found`)
return [{
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Code', 'User', 'settings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'settings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Code', 'User', 'keybindings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'keybindings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.vscode', 'extensions'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'),
}]
}
if (os === 'linux') {
const homeDir = env['HOME']
if (!homeDir) throw new Error(`variable for $HOME location not found`)
return [{
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Code', 'User', 'settings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'settings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Code', 'User', 'keybindings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'keybindings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.vscode', 'extensions'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'),
}]
}
if (os === 'windows') {
const appdata = env['APPDATA']
if (!appdata) throw new Error(`variable for %APPDATA% location not found`)
const userprofile = env['USERPROFILE']
if (!userprofile) throw new Error(`variable for %USERPROFILE% location not found`)
return [{
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Code', 'User', 'settings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'settings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Code', 'User', 'keybindings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'keybindings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.vscode', 'extensions'),
to: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.void-editor', 'extensions'),
}]
}
throw new Error(`os '${os}' not recognized`)
}
const os = isWindows ? 'windows' : isMacintosh ? 'mac' : isLinux ? 'linux' : null
let transferTheseFiles: TransferFilesInfo = []
let transferError: string | null = null
try { transferTheseFiles = transferTheseFilesOfOS(os) }
catch (e) { transferError = e + '' }
const OneClickSwitchButton = () => {
const accessor = useAccessor()
const fileService = accessor.get('IFileService')
const [state, setState] = useState<{ type: 'done', error?: string } | { type: | 'loading' | 'justfinished' }>({ type: 'done' })
if (transferTheseFiles.length === 0)
return <>
<WarningBox text={transferError ?? `One-click-switch not available.`} />
</>
const onClick = async () => {
if (state.type !== 'done') return
setState({ type: 'loading' })
let errAcc = ''
for (let { from, to } of transferTheseFiles) {
console.log('transferring', from, to)
// not sure if this can fail, just wrapping it with try/catch for now
try { await fileService.copy(from, to, true) }
catch (e) { errAcc += e + '\n' }
}
const hadError = !!errAcc
if (hadError) {
setState({ type: 'done', error: errAcc })
}
else {
setState({ type: 'justfinished' })
setTimeout(() => { setState({ type: 'done' }); }, 3000)
}
}
return <>
<VoidButton disabled={state.type !== 'done'} onClick={onClick}>
{state.type === 'done' ? 'Transfer my Settings'
: state.type === 'loading' ? 'Transferring...'
: state.type === 'justfinished' ? 'Success!'
: null
}
</VoidButton>
{state.type === 'done' && state.error ? <WarningBox text={state.error} /> : null}
</>
}
const GeneralTab = () => {
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
return <>
<div className=''>
<h2 className={`text-3xl mb-2`}>One-Click Switch</h2>
<h4 className={`text-void-fg-3 mb-2`}>{`Transfer your settings from VS Code to Void in one click.`}</h4>
<OneClickSwitchButton />
</div>
<div className='mt-12'>
<h2 className={`text-3xl mb-2`}>Built-in Settings</h2>
<h4 className={`text-void-fg-3 mb-2`}>{`IDE settings, keyboard settings, and theme customization.`}</h4>
<div className='my-4'>
<VoidButton onClick={() => { commandService.executeCommand('workbench.action.openSettings') }}>
General Settings
</VoidButton>
</div>
<div className='my-4'>
<VoidButton onClick={() => { commandService.executeCommand('workbench.action.openGlobalKeybindings') }}>
Keyboard Settings
</VoidButton>
</div>
<div className='my-4'>
<VoidButton onClick={() => { commandService.executeCommand('workbench.action.selectTheme') }}>
Theme Settings
</VoidButton>
</div>
</div>
<div className='mt-12'>
<h2 className={`text-3xl mb-2`}>AI Instructions</h2>
<h4 className={`text-void-fg-3 mb-2`}>{`Instructions to include on all AI requests.`}</h4>
<AIInstructionsBox />
</div>
</>
}
// full settings
export const Settings = () => {
const isDark = useIsDark()
const [tab, setTab] = useState<'models' | 'features'>('models')
const [tab, setTab] = useState<TabName>('models')
return <div className={`@@void-scope ${isDark ? 'dark' : ''}`}>
<div className='w-full h-full px-10 py-10 select-none'>
const containerRef = useRef<HTMLDivElement | null>(null)
useScrollbarStyles(containerRef)
return <div className={`@@void-scope ${isDark ? 'dark' : ''}`} style={{ height: '100%', width: '100%' }}>
<div ref={containerRef} className='overflow-y-auto w-full h-full px-10 py-10 select-none'>
<div className='max-w-5xl mx-auto'>
@ -404,9 +628,9 @@ export const Settings = () => {
<button className={`text-left p-1 px-3 my-0.5 rounded-sm overflow-hidden ${tab === 'models' ? 'bg-black/10 dark:bg-gray-200/10' : ''} hover:bg-black/10 hover:dark:bg-gray-200/10 active:bg-black/10 active:dark:bg-gray-200/10 `}
onClick={() => { setTab('models') }}
>Models</button>
{/* <button className={`text-left p-1 px-3 my-0.5 rounded-sm overflow-hidden ${tab === 'features' ? 'bg-black/10 dark:bg-gray-200/10' : ''} hover:bg-black/10 hover:dark:bg-gray-200/10 active:bg-black/10 active:dark:bg-gray-200/10 `}
onClick={() => { setTab('features') }}
>Features</button> */}
<button className={`text-left p-1 px-3 my-0.5 rounded-sm overflow-hidden ${tab === 'general' ? 'bg-black/10 dark:bg-gray-200/10' : ''} hover:bg-black/10 hover:dark:bg-gray-200/10 active:bg-black/10 active:dark:bg-gray-200/10 `}
onClick={() => { setTab('general') }}
>General</button>
</div>
{/* separator */}
@ -414,44 +638,14 @@ export const Settings = () => {
{/* content */}
<div className='w-full overflow-y-auto'>
<div className='w-full min-w-[600px] overflow-auto'>
<div className={`${tab !== 'models' ? 'hidden' : ''}`}>
<h2 className={`text-3xl mb-2`}>Local Providers</h2>
{/* <h3 className={`text-md opacity-50 mb-2`}>{`Keep your data private by hosting AI locally on your computer.`}</h3> */}
{/* <h3 className={`text-md opacity-50 mb-2`}>{`Instructions:`}</h3> */}
<h3 className={`text-md mb-2`}>{`Void can access any model that you host locally. We automatically detect your local models by default.`}</h3>
<div className='pl-4 select-text opacity-50'>
<h4 className={`text-xs mb-2`}><ChatMarkdownRender string={`1. Download [Ollama](https://ollama.com/download).`} /></h4>
<h4 className={`text-xs mb-2`}><ChatMarkdownRender string={`2. Open your terminal.`} /></h4>
<h4 className={`text-xs mb-2`}><ChatMarkdownRender string={`3. Run \`ollama run llama3.1\`. This installs Meta's llama model which is competitive with GPT-series models. It requires 5GB of memory.`} /></h4>
<h4 className={`text-xs mb-2`}><ChatMarkdownRender string={`4. Run \`ollama run qwen2.5-coder:1.5b\`. This is a faster autocomplete model and requires 1GB of memory.`} /></h4>
{/* TODO we should create UI for downloading models without user going into terminal */}
</div>
<ErrorBoundary>
<VoidProviderSettings providerNames={localProviderNames} />
</ErrorBoundary>
<h2 className={`text-3xl mb-2 mt-16`}>More Providers</h2>
<h3 className={`text-md mb-2`}>{`Void can also access models like ChatGPT and Claude. We recommend using Anthropic or OpenAI.`}</h3>
{/* <h3 className={`text-md opacity-50 mb-2`}>{`Access models like ChatGPT and Claude. We recommend using Anthropic or OpenAI as providers, or Groq as a faster alternative.`}</h3> */}
<ErrorBoundary>
<VoidProviderSettings providerNames={nonlocalProviderNames} />
</ErrorBoundary>
<h2 className={`text-3xl mb-2 mt-16`}>Models</h2>
<ErrorBoundary>
<VoidFeatureFlagSettings />
<RefreshableModels />
<ModelDump />
<AddModelMenuFull />
</ErrorBoundary>
<FeaturesTab />
</div>
<div className={`${tab !== 'features' ? 'hidden' : ''}`}>
<h2 className={`text-3xl mb-2`} onClick={() => { setTab('features') }}>Features</h2>
{/* <VoidFeatureFlagSettings /> */}
<div className={`${tab !== 'general' ? 'hidden' : ''}`}>
<GeneralTab />
</div>
</div>

View file

@ -9,7 +9,38 @@ module.exports = {
content: ['./src2/**/*.{jsx,tsx}'], // uses these files to decide how to transform the css file
theme: {
extend: {
fontSize: {
xs: '10px',
sm: '11px',
root: '13px',
lg: '14px',
xl: '16px',
'2xl': '18px',
'3xl': '20px',
'4xl': '24px',
'5xl': '30px',
'6xl': '36px',
'7xl': '48px',
'8xl': '64px',
'9xl': '72px',
},
// common colors to use, ordered light to dark
colors: {
"void-bg-1": "var(--vscode-input-background)",
"void-bg-2": "var(--vscode-sideBar-background)",
"void-bg-3": "var(--vscode-editor-background)",
"void-fg-1": "var(--vscode-editor-foreground)",
"void-fg-2": "var(--vscode-input-foreground)",
"void-fg-3": "var(--vscode-input-placeholderForeground)",
"void-warning": "var(--vscode-charts-yellow)",
"void-border-1": "var(--vscode-commandCenter-activeBorder)",
"void-border-2": "var(--vscode-commandCenter-border)",
"void-border-3": "var(--vscode-commandCenter-inactiveBorder)",
vscode: {
// see: https://code.visualstudio.com/api/extension-guides/webview#theming-webview-content
@ -39,8 +70,8 @@ module.exports = {
"input-bg": "var(--vscode-input-background)",
"input-border": "var(--vscode-input-border)",
"input-fg": "var(--vscode-input-foreground)",
"input-placeholder-fg": "var(--vscode-placeholderForeground)",
"input-active-bg": "var(--vscode-activeBackground)",
"input-placeholder-fg": "var(--vscode-input-placeholderForeground)",
"input-active-bg": "var(--vscode-input-activeBackground)",
"input-option-active-border": "var(--vscode-inputOption-activeBorder)",
"input-option-active-fg": "var(--vscode-inputOption-activeForeground)",
"input-option-hover-bg": "var(--vscode-inputOption-hoverBackground)",

View file

@ -2,8 +2,7 @@
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
{
{
"compilerOptions": {
"strict": true,
"exactOptionalPropertyTypes": false,
@ -17,5 +16,10 @@
// this is just for type checking, so src/ is the correct dir
"./src/**/*.ts",
"./src/**/*.tsx"
]
],
"plugins": [
{
"name": "next"
}
]
}

View file

@ -9,7 +9,7 @@ export default defineConfig({
entry: [
'./src2/sidebar-tsx/index.tsx',
'./src2/void-settings-tsx/index.tsx',
'./src2/ctrl-k-tsx/index.tsx',
'./src2/quick-edit-tsx/index.tsx',
'./src2/diff/index.tsx',
],
outDir: './out',

View file

@ -11,24 +11,41 @@ import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js';
import { CodeStagingSelection, IThreadHistoryService } from './threadHistoryService.js';
import { CodeStagingSelection, IChatThreadService } from './chatThreadService.js';
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
import { IRange } from '../../../../editor/common/core/range.js';
import { ITextModel } from '../../../../editor/common/model.js';
import { VOID_VIEW_ID } from './sidebarPane.js';
import { VOID_VIEW_CONTAINER_ID, VOID_VIEW_ID } from './sidebarPane.js';
import { IMetricsService } from '../../../../platform/void/common/metricsService.js';
import { ISidebarStateService } from './sidebarStateService.js';
import { ICommandService } from '../../../../platform/commands/common/commands.js';
import { VOID_OPEN_SETTINGS_ACTION_ID } from './voidSettingsPane.js';
import { VOID_TOGGLE_SETTINGS_ACTION_ID } from './voidSettingsPane.js';
import { VOID_CTRL_L_ACTION_ID } from './actionIDs.js';
import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js';
import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
import { URI } from '../../../../base/common/uri.js';
import { localize2 } from '../../../../nls.js';
import { IViewsService } from '../../../services/views/common/viewsService.js';
// ---------- Register commands and keybindings ----------
const roundRangeToLines = (range: IRange | null | undefined) => {
export const roundRangeToLines = (range: IRange | null | undefined, options: { emptySelectionBehavior: 'null' | 'line' }) => {
if (!range)
return null
// treat as no selection if selection is empty
if (range.endColumn === range.startColumn && range.endLineNumber === range.startLineNumber) {
if (options.emptySelectionBehavior === 'null')
return null
else if (options.emptySelectionBehavior === 'line')
return { startLineNumber: range.startLineNumber, startColumn: 1, endLineNumber: range.startLineNumber, endColumn: 1 }
}
// IRange is 1-indexed
const endLine = range.endColumn === 1 ? range.endLineNumber - 1 : range.endLineNumber // e.g. if the user triple clicks, it selects column=0, line=line -> column=0, line=line+1
const newRange: IRange = {
@ -50,11 +67,28 @@ const getContentInRange = (model: ITextModel, range: IRange | null) => {
return trimmedContent
}
// Action: when press ctrl+L, show the sidebar chat and add to the selection
export const VOID_CTRL_L_ACTION_ID = 'void.ctrlLAction'
const VOID_OPEN_SIDEBAR_ACTION_ID = 'void.sidebar.open'
registerAction2(class extends Action2 {
constructor() {
super({ id: VOID_CTRL_L_ACTION_ID, title: 'Void: Show Sidebar', keybinding: { primary: KeyMod.CtrlCmd | KeyCode.KeyL, weight: KeybindingWeight.BuiltinExtension } });
super({ id: VOID_OPEN_SIDEBAR_ACTION_ID, title: localize2('voidOpenSidebar', 'Void: Open Sidebar'), f1: true });
}
async run(accessor: ServicesAccessor): Promise<void> {
const stateService = accessor.get(ISidebarStateService)
stateService.setState({ isHistoryOpen: false, currentTab: 'chat' })
stateService.fireFocusChat()
}
})
// Action: when press ctrl+L, show the sidebar chat and add to the selection
const VOID_ADD_SELECTION_TO_SIDEBAR_ACTION_ID = 'void.sidebar.select'
registerAction2(class extends Action2 {
constructor() {
super({ id: VOID_ADD_SELECTION_TO_SIDEBAR_ACTION_ID, title: localize2('voidAddToSidebar', 'Void: Add Selection to Sidebar'), f1: true });
}
async run(accessor: ServicesAccessor): Promise<void> {
@ -62,67 +96,76 @@ registerAction2(class extends Action2 {
if (!model)
return
const stateService = accessor.get(ISidebarStateService)
const metricsService = accessor.get(IMetricsService)
const editorService = accessor.get(ICodeEditorService)
metricsService.capture('User Action', { type: 'Ctrl+L' })
stateService.setState({ isHistoryOpen: false, currentTab: 'chat' })
stateService.fireFocusChat()
metricsService.capture('Ctrl+L', {})
const editor = editorService.getActiveCodeEditor()
const selectionRange = roundRangeToLines(
// accessor.get(IEditorService).activeTextEditorControl?.getSelection()
editor?.getSelection()
// accessor.get(IEditorService).activeTextEditorControl?.getSelection()
const selectionRange = roundRangeToLines(editor?.getSelection(), { emptySelectionBehavior: 'null' })
// select whole lines
if (selectionRange) {
editor?.setSelection({ startLineNumber: selectionRange.startLineNumber, endLineNumber: selectionRange.endLineNumber, startColumn: 1, endColumn: Number.MAX_SAFE_INTEGER })
}
const selectionStr = getContentInRange(model, selectionRange)
const selection: CodeStagingSelection = !selectionRange || !selectionStr || (selectionRange.startLineNumber > selectionRange.endLineNumber) ? {
type: 'File',
fileURI: model.uri,
selectionStr: null,
range: null,
} : {
type: 'Selection',
fileURI: model.uri,
selectionStr: selectionStr,
range: selectionRange,
}
// add selection to staging
const chatThreadService = accessor.get(IChatThreadService)
const currentStaging = chatThreadService.state.currentStagingSelections
const currentStagingEltIdx = currentStaging?.findIndex(s =>
s.fileURI.fsPath === model.uri.fsPath
&& s.range?.startLineNumber === selection.range?.startLineNumber
&& s.range?.endLineNumber === selection.range?.endLineNumber
)
if (selectionRange) {
// select whole lines
editor?.setSelection({ startLineNumber: selectionRange.startLineNumber, endLineNumber: selectionRange.endLineNumber, startColumn: 1, endColumn: Number.MAX_SAFE_INTEGER })
const selectionStr = getContentInRange(model, selectionRange)
const selection: CodeStagingSelection = selectionStr === null || selectionRange.startLineNumber > selectionRange.endLineNumber ? {
type: 'File',
fileURI: model.uri,
selectionStr: null,
range: null,
} : {
type: 'Selection',
fileURI: model.uri,
selectionStr: selectionStr,
range: selectionRange,
}
// add selection to staging
const threadHistoryService = accessor.get(IThreadHistoryService)
const currentStaging = threadHistoryService.state._currentStagingSelections
const currentStagingEltIdx = currentStaging?.findIndex(s =>
s.fileURI.fsPath === model.uri.fsPath
&& s.range?.startLineNumber === selection.range?.startLineNumber
&& s.range?.endLineNumber === selection.range?.endLineNumber
)
// if matches with existing selection, overwrite
if (currentStagingEltIdx !== undefined && currentStagingEltIdx !== -1) {
threadHistoryService.setStaging([
...currentStaging!.slice(0, currentStagingEltIdx),
selection,
...currentStaging!.slice(currentStagingEltIdx + 1, Infinity)
])
}
// if no match, add
else {
threadHistoryService.setStaging([...(currentStaging ?? []), selection])
}
// if matches with existing selection, overwrite
if (currentStagingEltIdx !== undefined && currentStagingEltIdx !== -1) {
chatThreadService.setStaging([
...currentStaging!.slice(0, currentStagingEltIdx),
selection,
...currentStaging!.slice(currentStagingEltIdx + 1, Infinity)
])
}
// if no match, add
else {
chatThreadService.setStaging([...(currentStaging ?? []), selection])
}
}
});
registerAction2(class extends Action2 {
constructor() {
super({ id: VOID_CTRL_L_ACTION_ID, title: 'Void: Press Ctrl+L', keybinding: { primary: KeyMod.CtrlCmd | KeyCode.KeyL, weight: KeybindingWeight.BuiltinExtension } });
}
async run(accessor: ServicesAccessor): Promise<void> {
const commandService = accessor.get(ICommandService)
await commandService.executeCommand(VOID_OPEN_SIDEBAR_ACTION_ID)
await commandService.executeCommand(VOID_ADD_SELECTION_TO_SIDEBAR_ACTION_ID)
}
})
// New chat menu button
registerAction2(class extends Action2 {
constructor() {
@ -141,8 +184,8 @@ registerAction2(class extends Action2 {
stateService.setState({ isHistoryOpen: false, currentTab: 'chat' })
stateService.fireFocusChat()
const historyService = accessor.get(IThreadHistoryService)
historyService.startNewThread()
const chatThreadService = accessor.get(IChatThreadService)
chatThreadService.openNewThread()
}
})
@ -180,6 +223,69 @@ registerAction2(class extends Action2 {
}
async run(accessor: ServicesAccessor): Promise<void> {
const commandService = accessor.get(ICommandService)
commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID)
commandService.executeCommand(VOID_TOGGLE_SETTINGS_ACTION_ID)
}
})
export class TabSwitchListener extends Disposable {
constructor(
onSwitchTab: (uri: URI) => void,
@ICodeEditorService private readonly _editorService: ICodeEditorService,
) {
super()
// when editor switches tabs (models)
const addTabSwitchListeners = (editor: ICodeEditor) => {
this._register(editor.onDidChangeModel(e => {
if (e.newModelUrl?.scheme !== 'file') return
onSwitchTab(e.newModelUrl)
}))
}
const initializeEditor = (editor: ICodeEditor) => {
addTabSwitchListeners(editor)
}
// initialize current editors + any new editors
for (let editor of this._editorService.listCodeEditors()) initializeEditor(editor)
this._register(this._editorService.onCodeEditorAdd(editor => { initializeEditor(editor) }))
}
}
class TabSwitchContribution extends Disposable implements IWorkbenchContribution {
static readonly ID = 'workbench.contrib.void.tabswitch'
constructor(
@IInstantiationService private readonly instantiationService: IInstantiationService,
@ICommandService private readonly commandService: ICommandService,
@IViewsService private readonly viewsService: IViewsService,
) {
super()
// sidebarIsVisible state
let sidebarIsVisible = this.viewsService.isViewContainerVisible(VOID_VIEW_CONTAINER_ID)
this._register(this.viewsService.onDidChangeViewVisibility(e => {
sidebarIsVisible = e.visible
}))
const addCurrentFileIfVisible = () => {
if (sidebarIsVisible)
this.commandService.executeCommand(VOID_ADD_SELECTION_TO_SIDEBAR_ACTION_ID)
}
// when sidebar becomes visible, add current file
this._register(this.viewsService.onDidChangeViewVisibility(e => { sidebarIsVisible = e.visible }))
// run on current tab if it exists, and listen for tab switches and visibility changes
addCurrentFileIfVisible()
this._register(this.viewsService.onDidChangeViewVisibility(() => { addCurrentFileIfVisible() }))
this._register(this.instantiationService.createInstance(TabSwitchListener, () => { addCurrentFileIfVisible() }))
}
}
registerWorkbenchContribution2(TabSwitchContribution.ID, TabSwitchContribution, WorkbenchPhase.BlockRestore);

View file

@ -1,213 +0,0 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { Disposable } from '../../../../base/common/lifecycle.js';
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
import { URI } from '../../../../base/common/uri.js';
import { Emitter, Event } from '../../../../base/common/event.js';
import { IAutocompleteService } from './autocompleteService.js';
import { IRange } from '../../../../editor/common/core/range.js';
export type CodeSelection = {
fileURI: URI;
selectionStr: string | null;
content: string; // TODO remove this (replace `selectionStr` with `content`)
range: IRange;
}
// if selectionStr is null, it means to use the entire file at send time
export type CodeStagingSelection = {
type: 'Selection',
fileURI: URI,
selectionStr: string,
range: IRange
} | {
type: 'File',
fileURI: URI,
selectionStr: null,
range: null
}
// WARNING: changing this format is a big deal!!!!!! need to migrate old format to new format on users' computers so people don't get errors.
export type ChatMessage =
| {
role: 'user';
content: string | null; // content sent to the llm - allowed to be '', will be replaced with (empty)
displayContent: string | null; // content displayed to user - allowed to be '', will be ignored
selections: CodeSelection[] | null; // the user's selection
}
| {
role: 'assistant';
content: string | null; // content received from LLM - allowed to be '', will be replaced with (empty)
displayContent: string | null; // content displayed to user (this is the same as content for now) - allowed to be '', will be ignored
}
| {
role: 'system';
content: string;
displayContent?: undefined;
}
// a 'thread' means a chat message history
export type ChatThreads = {
[id: string]: {
id: string; // store the id here too
createdAt: string; // ISO string
lastModified: string; // ISO string
messages: ChatMessage[];
};
}
export type ThreadsState = {
allThreads: ChatThreads;
_currentThreadId: string | null; // intended for internal use only
_currentStagingSelections: CodeStagingSelection[] | null;
}
const newThreadObject = () => {
const now = new Date().toISOString()
return {
id: new Date().getTime().toString(),
createdAt: now,
lastModified: now,
messages: [],
}
}
const THREAD_STORAGE_KEY = 'void.threadHistory'
export interface IThreadHistoryService {
readonly _serviceBrand: undefined;
readonly state: ThreadsState;
onDidChangeCurrentThread: Event<void>;
getCurrentThread(state: ThreadsState): ChatThreads[string] | null;
startNewThread(): void;
switchToThread(threadId: string): void;
addMessageToCurrentThread(message: ChatMessage): void;
setStaging(stagingSelection: CodeStagingSelection[] | null): void;
}
export const IThreadHistoryService = createDecorator<IThreadHistoryService>('voidThreadHistoryService');
class ThreadHistoryService extends Disposable implements IThreadHistoryService {
_serviceBrand: undefined;
// this fires when the current thread changes at all (a switch of currentThread, or a message added to it, etc)
private readonly _onDidChangeCurrentThread = new Emitter<void>();
readonly onDidChangeCurrentThread: Event<void> = this._onDidChangeCurrentThread.event;
state: ThreadsState // allThreads is persisted, currentThread is not
constructor(
@IStorageService private readonly _storageService: IStorageService,
@IAutocompleteService private readonly _autocomplete: IAutocompleteService,
) {
super()
this._autocomplete
this.state = {
allThreads: this._readAllThreads(),
_currentThreadId: null,
_currentStagingSelections: null,
}
}
private _readAllThreads(): ChatThreads {
const threads = this._storageService.get(THREAD_STORAGE_KEY, StorageScope.APPLICATION)
return threads ? JSON.parse(threads) : {}
}
private _storeAllThreads(threads: ChatThreads) {
this._storageService.store(THREAD_STORAGE_KEY, JSON.stringify(threads), StorageScope.APPLICATION, StorageTarget.USER)
}
// this should be the only place this.state = ... appears besides constructor
private _setState(state: Partial<ThreadsState>, affectsCurrent: boolean) {
this.state = {
...this.state,
...state
}
if (affectsCurrent)
this._onDidChangeCurrentThread.fire()
}
// must "prove" that you have access to the current state by providing it
getCurrentThread(state: ThreadsState): ChatThreads[string] | null {
return state._currentThreadId ? state.allThreads[state._currentThreadId] ?? null : null;
}
switchToThread(threadId: string) {
console.log('threadId', threadId)
console.log('messages', this.state.allThreads[threadId].messages)
this._setState({ _currentThreadId: threadId }, true)
}
startNewThread() {
// if a thread with 0 messages already exists, switch to it
const { allThreads: currentThreads } = this.state
for (const threadId in currentThreads) {
if (currentThreads[threadId].messages.length === 0) {
this.switchToThread(threadId)
return
}
}
// otherwise, start a new thread
const newThread = newThreadObject()
// update state
const newThreads = {
...currentThreads,
[newThread.id]: newThread
}
this._storeAllThreads(newThreads)
this._setState({ allThreads: newThreads, _currentThreadId: newThread.id }, true)
}
addMessageToCurrentThread(message: ChatMessage) {
console.log('adding ', message.role, 'to chat')
const { allThreads, _currentThreadId } = this.state
// get the current thread, or create one
let currentThread: ChatThreads[string]
if (_currentThreadId && (_currentThreadId in allThreads)) {
currentThread = allThreads[_currentThreadId]
}
else {
currentThread = newThreadObject()
this.state._currentThreadId = currentThread.id
}
// update state and store it
const newThreads = {
...allThreads,
[currentThread.id]: {
...currentThread,
lastModified: new Date().toISOString(),
messages: [...currentThread.messages, message],
}
}
this._storeAllThreads(newThreads)
this._setState({ allThreads: newThreads }, true) // the current thread just changed (it had a message added to it)
}
setStaging(stagingSelection: CodeStagingSelection[] | null): void {
this._setState({ _currentStagingSelections: stagingSelection }, true) // this is a hack for now
}
}
registerSingleton(IThreadHistoryService, ThreadHistoryService, InstantiationType.Eager);

View file

@ -16,7 +16,7 @@ import './sidebarStateService.js'
import './quickEditActions.js'
// register Thread History
import './threadHistoryService.js'
import './chatThreadService.js'
// register Autocomplete
import './autocompleteService.js'

View file

@ -26,7 +26,6 @@ import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextke
import { mountVoidSettings } from './react/out/void-settings-tsx/index.js'
import { Codicon } from '../../../../base/common/codicons.js';
import { IDisposable } from '../../../../base/common/lifecycle.js';
import { DomScrollableElement } from '../../../../base/browser/ui/scrollbar/scrollableElement.js';
// refer to preferences.contribution.ts keybindings editor
@ -63,7 +62,7 @@ class VoidSettingsInput extends EditorInput {
class VoidSettingsPane extends EditorPane {
static readonly ID = 'workbench.test.myCustomPane';
private _scrollbar: DomScrollableElement | undefined;
// private _scrollbar: DomScrollableElement | undefined;
constructor(
group: IEditorGroup,
@ -79,32 +78,31 @@ class VoidSettingsPane extends EditorPane {
parent.style.height = '100%';
parent.style.width = '100%';
const scrollableContent = document.createElement('div');
scrollableContent.style.height = '100%';
scrollableContent.style.width = '100%';
const settingsElt = document.createElement('div');
settingsElt.style.height = '100%';
settingsElt.style.width = '100%';
this._scrollbar = this._register(new DomScrollableElement(scrollableContent, {}));
parent.appendChild(this._scrollbar.getDomNode());
this._scrollbar.scanDomNode();
parent.appendChild(settingsElt);
// this._scrollbar = this._register(new DomScrollableElement(scrollableContent, {}));
// parent.appendChild(this._scrollbar.getDomNode());
// this._scrollbar.scanDomNode();
// Mount React into the scrollable content
this.instantiationService.invokeFunction(accessor => {
const disposables: IDisposable[] | undefined = mountVoidSettings(scrollableContent, accessor);
const disposables: IDisposable[] | undefined = mountVoidSettings(settingsElt, accessor);
setTimeout(() => { // this is a complete hack and I don't really understand how scrollbar works here
this._scrollbar?.scanDomNode();
}, 1000)
// setTimeout(() => { // this is a complete hack and I don't really understand how scrollbar works here
// this._scrollbar?.scanDomNode();
// }, 1000)
disposables?.forEach(d => this._register(d));
});
}
layout(dimension: Dimension): void {
if (!this._scrollbar) return;
this._scrollbar.getDomNode().style.height = `${dimension.height}px`;
this._scrollbar.getDomNode().style.width = `${dimension.width}px`;
this._scrollbar.scanDomNode();
// if (!settingsElt) return
// settingsElt.style.height = `${dimension.height}px`;
// settingsElt.style.width = `${dimension.width}px`;
}
@ -119,14 +117,13 @@ Registry.as<IEditorPaneRegistry>(EditorExtensions.EditorPane).registerEditorPane
);
export const VOID_OPEN_SETTINGS_ACTION_ID = 'workbench.action.openVoidSettings'
// register the gear on the top right
export const VOID_TOGGLE_SETTINGS_ACTION_ID = 'workbench.action.toggleVoidSettings'
registerAction2(class extends Action2 {
constructor() {
super({
id: VOID_OPEN_SETTINGS_ACTION_ID,
title: nls.localize2('voidSettings', "Void: Settings"),
f1: true,
id: VOID_TOGGLE_SETTINGS_ACTION_ID,
title: nls.localize2('voidSettings', "Void: Toggle Settings"),
icon: Codicon.settingsGear,
menu: [
{
@ -146,9 +143,8 @@ registerAction2(class extends Action2 {
const editorService = accessor.get(IEditorService);
const instantiationService = accessor.get(IInstantiationService);
const openEditors = editorService.findEditors(VoidSettingsInput.RESOURCE);
// close all instances if found
const openEditors = editorService.findEditors(VoidSettingsInput.RESOURCE);
if (openEditors.length > 0) {
await editorService.closeEditors(openEditors);
return;
@ -161,11 +157,42 @@ registerAction2(class extends Action2 {
})
export const VOID_OPEN_SETTINGS_ACTION_ID = 'workbench.action.openVoidSettings'
registerAction2(class extends Action2 {
constructor() {
super({
id: VOID_OPEN_SETTINGS_ACTION_ID,
title: nls.localize2('voidSettings', "Void: Open Settings"),
f1: true,
icon: Codicon.settingsGear,
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const editorService = accessor.get(IEditorService);
const instantiationService = accessor.get(IInstantiationService);
// close all instances if found
const openEditors = editorService.findEditors(VoidSettingsInput.RESOURCE);
if (openEditors.length > 0) {
await editorService.closeEditors(openEditors);
}
// then, open one single editor
const input = instantiationService.createInstance(VoidSettingsInput);
await editorService.openEditor(input);
}
})
// add to settings gear on bottom left
MenuRegistry.appendMenuItem(MenuId.GlobalActivity, {
group: '0_command',
command: {
id: VOID_OPEN_SETTINGS_ACTION_ID,
id: VOID_TOGGLE_SETTINGS_ACTION_ID,
title: nls.localize('voidSettings', "Void Settings")
},
order: 1