diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..7e6d5909 --- /dev/null +++ b/CHANGELOG.md @@ -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. + diff --git a/build/azure-pipelines/darwin/product-build-darwin-universal.yml b/build/azure-pipelines/darwin/product-build-darwin-universal.yml index f8c10a03..549e4107 100644 --- a/build/azure-pipelines/darwin/product-build-darwin-universal.yml +++ b/build/azure-pipelines/darwin/product-build-darwin-universal.yml @@ -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 diff --git a/build/darwin/create-universal-app.ts b/build/darwin/create-universal-app.ts index 94b8a23b..e732bd69 100644 --- a/build/darwin/create-universal-app.ts +++ b/build/darwin/create-universal-app.ts @@ -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'; diff --git a/build/darwin/sign.ts b/build/darwin/sign.ts index 5b3413b7..9e605801 100644 --- a/build/darwin/sign.ts +++ b/build/darwin/sign.ts @@ -92,21 +92,21 @@ async function main(buildDir?: string): Promise { '-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}` ]); } diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index d9aa780d..4d90766e 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -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 })) diff --git a/package-lock.json b/package-lock.json index 1c1b3eb3..91937c1c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 443e9329..9833d8a9 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/vs/editor/common/services/editorSimpleWorker.ts b/src/vs/editor/common/services/editorSimpleWorker.ts index ff8bbb85..9bc3a7f1 100644 --- a/src/vs/editor/common/services/editorSimpleWorker.ts +++ b/src/vs/editor/common/services/editorSimpleWorker.ts @@ -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 { + 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; diff --git a/src/vs/platform/extensions/common/extensionValidator.ts b/src/vs/platform/extensions/common/extensionValidator.ts index 4a9d3295..94f3bcf8 100644 --- a/src/vs/platform/extensions/common/extensionValidator.ts +++ b/src/vs/platform/extensions/common/extensionValidator.ts @@ -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; diff --git a/src/vs/platform/telemetry/common/serverTelemetryService.ts b/src/vs/platform/telemetry/common/serverTelemetryService.ts index f6fc225a..8839ecf7 100644 --- a/src/vs/platform/telemetry/common/serverTelemetryService.ts +++ b/src/vs/platform/telemetry/common/serverTelemetryService.ts @@ -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> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck) { - 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> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck) { - 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 { diff --git a/src/vs/platform/telemetry/common/telemetryService.ts b/src/vs/platform/telemetry/common/telemetryService.ts index 51fbb8fa..c3e05eb2 100644 --- a/src/vs/platform/telemetry/common/telemetryService.ts +++ b/src/vs/platform/telemetry/common/telemetryService.ts @@ -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, this._cleanupPatterns); + // // remove all PII from data + // data = cleanData(data as Record, 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> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck) { - 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> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck) { - this.publicLogError(eventName, data as ITelemetryData); + // Void commented this out + // this.publicLogError(eventName, data as ITelemetryData); } } diff --git a/src/vs/platform/update/electron-main/abstractUpdateService.ts b/src/vs/platform/update/electron-main/abstractUpdateService.ts index 48638aa1..2d3134d8 100644 --- a/src/vs/platform/update/electron-main/abstractUpdateService.ts +++ b/src/vs/platform/update/electron-main/abstractUpdateService.ts @@ -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 { 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 { - 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 { + await timeout(delay); + await this.checkForUpdates(false); + return await this.scheduleCheckForUpdates(60 * 60 * 1000); } async checkForUpdates(explicit: boolean): Promise { @@ -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 { // noop } @@ -174,6 +181,7 @@ export abstract class AbstractUpdateService implements IUpdateService { await this.doApplyUpdate(); } + // windows overrides this protected async doApplyUpdate(): Promise { // 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; } diff --git a/src/vs/platform/update/electron-main/updateService.darwin.ts b/src/vs/platform/update/electron-main/updateService.darwin.ts index d3f27d37..c521b76f 100644 --- a/src/vs/platform/update/electron-main/updateService.darwin.ts +++ b/src/vs/platform/update/electron-main/updateService.darwin.ts @@ -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'; diff --git a/src/vs/platform/update/electron-main/updateService.linux.ts b/src/vs/platform/update/electron-main/updateService.linux.ts index 6e076c72..b01840c5 100644 --- a/src/vs/platform/update/electron-main/updateService.linux.ts +++ b/src/vs/platform/update/electron-main/updateService.linux.ts @@ -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); } diff --git a/src/vs/platform/update/electron-main/updateService.win32.ts b/src/vs/platform/update/electron-main/updateService.win32.ts index 61109e54..c987ecce 100644 --- a/src/vs/platform/update/electron-main/updateService.win32.ts +++ b/src/vs/platform/update/electron-main/updateService.win32.ts @@ -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) { diff --git a/src/vs/platform/void/common/llmMessageService.ts b/src/vs/platform/void/common/llmMessageService.ts index 9cd97573..0bd8bf13 100644 --- a/src/vs/platform/void/common/llmMessageService.ts +++ b/src/vs/platform/void/common/llmMessageService.ts @@ -52,6 +52,7 @@ export class LLMMessageService extends Disposable implements ILLMMessageService super() // const service = ProxyChannel.toService(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>)(e => { this.onSuccess_ollama[e.requestId]?.(e) })) this._register((this.channel.listen('onError_ollama') satisfies Event>)(e => { this.onError_ollama[e.requestId]?.(e) })) - // openaiCompatible + // openaiCompatible .list() this._register((this.channel.listen('onSuccess_openAICompatible') satisfies Event>)(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 diff --git a/src/vs/platform/void/common/llmMessageTypes.ts b/src/vs/platform/void/common/llmMessageTypes.ts index 0509aa37..f14e82a6 100644 --- a/src/vs/platform/void/common/llmMessageTypes.ts +++ b/src/vs/platform/void/common/llmMessageTypes.ts @@ -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; } diff --git a/src/vs/platform/void/common/metricsService.ts b/src/vs/platform/void/common/metricsService.ts index 3d185669..7002af45 100644 --- a/src/vs/platform/void/common/metricsService.ts +++ b/src/vs/platform/void/common/metricsService.ts @@ -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(mainProcessService.getChannel('void-channel-metrics')); } diff --git a/src/vs/platform/void/common/refreshModelService.ts b/src/vs/platform/void/common/refreshModelService.ts index 47954bc7..811db0db 100644 --- a/src/vs/platform/void/common/refreshModelService.ts +++ b/src/vs/platform/void/common/refreshModelService.ts @@ -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 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(a: T[], b: T[]): boolean { if (a.length !== b.length) return false @@ -51,7 +62,7 @@ function eq(a: T[], b: T[]): boolean { } export interface IRefreshModelService { readonly _serviceBrand: undefined; - refreshModels: (providerName: RefreshableProviderName) => Promise; + startRefreshingModels: (providerName: RefreshableProviderName, options: { enableProviderOnSuccess: boolean, doNotFire: boolean }) => void; onDidChangeState: Event; state: RefreshModelStateOfProvider; } @@ -75,23 +86,23 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ const disposables: Set = 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) } diff --git a/src/vs/platform/void/common/voidSettingsService.ts b/src/vs/platform/void/common/voidSettingsService.ts index 5a6cc64d..ffaa5e72 100644 --- a/src/vs/platform/void/common/voidSettingsService.ts +++ b/src/vs/platform/void/common/voidSettingsService.ts @@ -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 = ( providerName: ProviderName, @@ -27,21 +28,22 @@ type SetModelSelectionOfFeatureFn = ( options?: { doNotApplyEffects?: true } ) => Promise; -type SetFeatureFlagFn = (flagName: FeatureFlagName, newVal: boolean) => void; +type SetGlobalSettingFn = (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 | 'all' +type RealVoidSettings = Exclude +type EventProp = 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 } diff --git a/src/vs/platform/void/common/voidSettingsTypes.ts b/src/vs/platform/void/common/voidSettingsTypes.ts index 72efb86b..2577bfe7 100644 --- a/src/vs/platform/void/common/voidSettingsTypes.ts +++ b/src/vs/platform/void/common/voidSettingsTypes.ts @@ -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 = {} - 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 @@ -177,6 +184,7 @@ export type SettingName = keyof SettingsForProvider 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[] diff --git a/src/vs/platform/void/electron-main/llmMessage/sendLLMMessage.ts b/src/vs/platform/void/electron-main/llmMessage/sendLLMMessage.ts index 96d51e34..5d1866db 100644 --- a/src/vs/platform/void/electron-main/llmMessage/sendLLMMessage.ts +++ b/src/vs/platform/void/electron-main/llmMessage/sendLLMMessage.ts @@ -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 }) diff --git a/src/vs/platform/void/electron-main/metricsMainService.ts b/src/vs/platform/void/electron-main/metricsMainService.ts index 31ca1252..e1811e4c 100644 --- a/src/vs/platform/void/electron-main/metricsMainService.ts +++ b/src/vs/platform/void/electron-main/metricsMainService.ts @@ -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 })) } diff --git a/src/vs/workbench/browser/layout.ts b/src/vs/workbench/browser/layout.ts index e39d7ce6..770269f8 100644 --- a/src/vs/workbench/browser/layout.ts +++ b/src/vs/workbench/browser/layout.ts @@ -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('sideBar.size', StorageScope.PROFILE, StorageTarget.MACHINE, 200), - AUXILIARYBAR_SIZE: new InitializationStateKey('auxiliaryBar.size', StorageScope.PROFILE, StorageTarget.MACHINE, 200), + AUXILIARYBAR_SIZE: new InitializationStateKey('auxiliaryBar.size', StorageScope.PROFILE, StorageTarget.MACHINE, 800), // Void changed this from 200 to 800 PANEL_SIZE: new InitializationStateKey('panel.size', StorageScope.PROFILE, StorageTarget.MACHINE, 300), PANEL_LAST_NON_MAXIMIZED_HEIGHT: new RuntimeStateKey('panel.lastNonMaximizedHeight', StorageScope.PROFILE, StorageTarget.MACHINE, 300), diff --git a/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts b/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts index f3718162..f7225d40 100644 --- a/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts +++ b/src/vs/workbench/browser/parts/auxiliarybar/auxiliaryBarPart.ts @@ -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; diff --git a/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts b/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts index c2c62d65..787632ae 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts @@ -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' diff --git a/src/vs/workbench/contrib/void/browser/actionIDs.ts b/src/vs/workbench/contrib/void/browser/actionIDs.ts new file mode 100644 index 00000000..b237ecf8 --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/actionIDs.ts @@ -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' diff --git a/src/vs/workbench/contrib/void/browser/autocompleteService.ts b/src/vs/workbench/contrib/void/browser/autocompleteService.ts index 9dda4a1e..9237c7ca 100644 --- a/src/vs/workbench/contrib/void/browser/autocompleteService.ts +++ b/src/vs/workbench/contrib/void/browser/autocompleteService.ts @@ -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 diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts new file mode 100644 index 00000000..0faf4725 --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -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; + onDidChangeStreamState: Event<{ threadId: string }> + + getCurrentThread(): ChatThreads[string]; + openNewThread(): void; + switchToThread(threadId: string): void; + + setStaging(stagingSelection: CodeStagingSelection[] | null): void; + + addUserMessageAndStreamResponse(userMessage: string): Promise; + cancelStreaming(threadId: string): void; + dismissStreamError(threadId: string): void; + +} + +export const IChatThreadService = createDecorator('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(); + readonly onDidChangeCurrentThread: Event = 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, affectsCurrent: boolean) { + this.state = { + ...this.state, + ...state + } + if (affectsCurrent) + this._onDidChangeCurrentThread.fire() + } + + private _setStreamState(threadId: string, state: Partial>) { + 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); + diff --git a/src/vs/workbench/contrib/void/browser/helperServices/consistentItemService.ts b/src/vs/workbench/contrib/void/browser/helperServices/consistentItemService.ts index f6787e4c..8bde2321 100644 --- a/src/vs/workbench/contrib/void/browser/helperServices/consistentItemService.ts +++ b/src/vs/workbench/contrib/void/browser/helperServices/consistentItemService.ts @@ -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] } diff --git a/src/vs/workbench/contrib/void/browser/helpers/detectLanguage.ts b/src/vs/workbench/contrib/void/browser/helpers/detectLanguage.ts new file mode 100644 index 00000000..b4b9d513 --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/helpers/detectLanguage.ts @@ -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]; +} diff --git a/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts b/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts index bc131f46..d7e109ae 100644 --- a/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts +++ b/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts @@ -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 =
, the string is 
hi

= 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``` // 2. `````` - 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 tag] + (match the stuff between mid tags) + [optional tag] + [optional ` | `` | ```] + */ + + const pm = new SurroundingsRemover(text) + + pm.removeCodeBlock() + + const foundMid = pm.removePrefix(`<${midTag}>`) + + if (foundMid) { + pm.removeSuffix(``) + } + 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]*?)?([\s\S]*?)(?:<\/MID>|`{1,3}|$)/; + // const regex = new RegExp( + // `[\\s\\S]*?(?:\`{1,3}\\s*([a-zA-Z_]+[\\w]*)?[\\s\\S]*?)?<${midTag}>([\\s\\S]*?)(?:|\`{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 + // } + +} + diff --git a/src/vs/workbench/contrib/void/browser/helpers/getCmdKey.ts b/src/vs/workbench/contrib/void/browser/helpers/getCmdKey.ts deleted file mode 100644 index 96ae4b8c..00000000 --- a/src/vs/workbench/contrib/void/browser/helpers/getCmdKey.ts +++ /dev/null @@ -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'; - } -} - - - - diff --git a/src/vs/workbench/contrib/void/browser/helpers/readFile.ts b/src/vs/workbench/contrib/void/browser/helpers/readFile.ts new file mode 100644 index 00000000..60e5dc5c --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/helpers/readFile.ts @@ -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 => { + const model = modelService.getModel(uri) + if (!model) return null + return model.getValue(EndOfLinePreference.LF) +} diff --git a/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts b/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts index c4feab57..d6d04ade 100644 --- a/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts +++ b/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts @@ -6,7 +6,7 @@ import { Disposable } from '../../../../base/common/lifecycle.js'; import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js'; import { createDecorator, IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { ICodeEditor, IOverlayWidget, IViewZone } from '../../../../editor/browser/editorBrowser.js'; +import { ICodeEditor, IOverlayWidget, IViewZone, OverlayWidgetPositionPreference } from '../../../../editor/browser/editorBrowser.js'; // import { IUndoRedoService } from '../../../../platform/undoRedo/common/undoRedo.js'; import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; @@ -18,29 +18,32 @@ import { registerColor } from '../../../../platform/theme/common/colorUtils.js'; import { Color, RGBA } from '../../../../base/common/color.js'; import { IModelService } from '../../../../editor/common/services/model.js'; import { IUndoRedoElement, IUndoRedoService, UndoRedoElementType } from '../../../../platform/undoRedo/common/undoRedo.js'; -import { LineSource, renderLines, RenderOptions } from '../../../../editor/browser/widget/diffEditor/components/diffEditorViewZones/renderLines.js'; -import { LineTokens } from '../../../../editor/common/tokens/lineTokens.js'; -import { ILanguageService } from '../../../../editor/common/languages/language.js'; +import { RenderOptions } from '../../../../editor/browser/widget/diffEditor/components/diffEditorViewZones/renderLines.js'; // import { IModelService } from '../../../../editor/common/services/model.js'; import * as dom from '../../../../base/browser/dom.js'; import { Widget } from '../../../../base/browser/ui/widget.js'; import { URI } from '../../../../base/common/uri.js'; import { IConsistentEditorItemService, IConsistentItemService } from './helperServices/consistentItemService.js'; -import { ctrlKStream_prefixAndSuffix, ctrlKStream_prompt, ctrlKStream_systemMessage, ctrlLStream_prompt, ctrlLStream_systemMessage } from './prompt/prompts.js'; +import { ctrlKStream_prefixAndSuffix, ctrlKStream_prompt, ctrlKStream_systemMessage, ctrlLStream_prompt, ctrlLStream_systemMessage, defaultFimTags } from './prompt/prompts.js'; import { ILLMMessageService } from '../../../../platform/void/common/llmMessageService.js'; -import { IPosition } from '../../../../editor/common/core/position.js'; -import { mountCtrlK } from '../browser/react/out/ctrl-k-tsx/index.js' +import { mountCtrlK } from '../browser/react/out/quick-edit-tsx/index.js' import { QuickEditPropsType } from './quickEditActions.js'; -import { InputBox } from '../../../../base/browser/ui/inputbox/inputBox.js'; -import { LLMMessage } from '../../../../platform/void/common/llmMessageTypes.js'; +import { errorDetails, LLMMessage } from '../../../../platform/void/common/llmMessageTypes.js'; import { IModelContentChangedEvent } from '../../../../editor/common/textModelEvents.js'; +import { extractCodeFromFIM, extractCodeFromRegular } from './helpers/extractCodeFromResult.js'; +import { IMetricsService } from '../../../../platform/void/common/metricsService.js'; +import { filenameToVscodeLanguage } from './helpers/detectLanguage.js'; +import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; +import { isMacintosh } from '../../../../base/common/platform.js'; +import { EditorOption } from '../../../../editor/common/config/editorOptions.js'; +import { Emitter } from '../../../../base/common/event.js'; const configOfBG = (color: Color) => { return { dark: color, light: color, hcDark: color, hcLight: color, } } -// gets converted to --vscode-void-greenBG, see void.css +// gets converted to --vscode-void-greenBG, see void.css, asCssVariable const greenBG = new Color(new RGBA(155, 185, 85, .3)); // default is RGBA(155, 185, 85, .2) registerColor('void.greenBG', configOfBG(greenBG), '', true); @@ -58,6 +61,43 @@ registerColor('void.sweepIdxBG', configOfBG(sweepIdxBG), '', true); + + +const getLeadingWhitespacePx = (editor: ICodeEditor, startLine: number): number => { + + const model = editor.getModel(); + if (!model) { + return 0; + } + + // Get the line content, defaulting to empty string if line doesn't exist + const lineContent = model.getLineContent(startLine) || ''; + + // Find the first non-whitespace character + const firstNonWhitespaceIndex = lineContent.search(/\S/); + + // Extract leading whitespace, handling case where line is all whitespace + const leadingWhitespace = firstNonWhitespaceIndex === -1 + ? lineContent + : lineContent.slice(0, firstNonWhitespaceIndex); + + // Get font information from editor render options + const { tabSize: numSpacesInTab } = model.getFormattingOptions(); + const spaceWidth = editor.getOption(EditorOption.fontInfo).spaceWidth; + const tabWidth = numSpacesInTab * spaceWidth; + + let paddingLeft = 0; + for (const char of leadingWhitespace) { + if (char === '\t') { + paddingLeft += tabWidth + } else if (char === ' ') { + paddingLeft += spaceWidth; + } + } + + return paddingLeft; +}; + // similar to ServiceLLM export type StartApplyingOpts = { featureName: 'Ctrl+K'; @@ -117,11 +157,13 @@ type CtrlKZone = { editorId: string; // the editor the input lives on _mountInfo: null | { - inputBoxRef: { current: InputBox | null }; // the input box that lives in the zone + textAreaRef: { current: HTMLTextAreaElement | null } dispose: () => void; refresh: () => void; } + _linkedStreamingDiffZone: number | null; // diffareaid of the diffZone currently streaming here + } & CommonZoneProps @@ -139,6 +181,7 @@ type DiffZone = { line?: undefined; }; editorId?: undefined; + linkedStreamingDiffZone?: undefined; } & CommonZoneProps @@ -153,6 +196,7 @@ const diffAreaSnapshotKeys = [ 'startLine', 'endLine', 'editorId', + ] as const satisfies (keyof DiffArea)[] type DiffAreaSnapshot = Pick @@ -172,6 +216,7 @@ export interface IInlineDiffsService { interruptStreaming(diffareaid: number): void; addCtrlKZone(opts: AddCtrlKOpts): number | undefined; removeCtrlKZone(opts: { diffareaid: number }): void; + // testDiffs(): void; } export const IInlineDiffsService = createDecorator('inlineDiffAreasService'); @@ -187,16 +232,23 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { diffOfId: Record = {}; // redundant with diffArea._diffs + // only applies to diffZones + // streamingDiffZones: Set = new Set() + private readonly _onDidChangeStreaming = new Emitter<{ uri: URI; diffareaid: number }>(); + private readonly _onDidAddOrDeleteDiffZones = new Emitter<{ uri: URI }>(); + + constructor( // @IHistoryService private readonly _historyService: IHistoryService, // history service is the history of pressing alt left/right @ICodeEditorService private readonly _editorService: ICodeEditorService, @IModelService private readonly _modelService: IModelService, @IUndoRedoService private readonly _undoRedoService: IUndoRedoService, // undoRedo service is the history of pressing ctrl+z - @ILanguageService private readonly _langService: ILanguageService, @ILLMMessageService private readonly _llmMessageService: ILLMMessageService, @IConsistentItemService private readonly _consistentItemService: IConsistentItemService, @IInstantiationService private readonly _instantiationService: IInstantiationService, @IConsistentEditorItemService private readonly _consistentEditorItemService: IConsistentEditorItemService, + @IMetricsService private readonly _metricsService: IMetricsService, + @INotificationService private readonly _notificationService: INotificationService, ) { super(); @@ -215,6 +267,24 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { this._onUserChangeContent(uri, e) }) ) + + // when a stream starts or ends + let removeAcceptRejectAllUI: (() => void) | null = null + const onChangeUriState = () => { + const uri = model.uri + const diffZones = [...this.diffAreasOfURI[uri.fsPath].values()] + .map(diffareaid => this.diffAreaOfId[diffareaid]) + .filter(diffArea => !!diffArea && diffArea.type === 'DiffZone') + const isStreaming = diffZones.find(diffZone => !!diffZone._streamState.isStreaming) + if (diffZones.length !== 0 && !isStreaming && !removeAcceptRejectAllUI) { + removeAcceptRejectAllUI = this._addAcceptRejectUI(uri) ?? null + } else { + removeAcceptRejectAllUI?.() + removeAcceptRejectAllUI = null + } + } + this._register(this._onDidAddOrDeleteDiffZones.event(({ uri: uri_ }) => { if (uri_.fsPath === model.uri.fsPath) onChangeUriState() })) + this._register(this._onDidChangeStreaming.event(({ uri: uri_ }) => { if (uri_.fsPath === model.uri.fsPath) onChangeUriState() })) } // initialize all existing models + initialize when a new model mounts for (let model of this._modelService.getModels()) { initializeModel(model) } @@ -243,7 +313,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { private _onInternalChangeContent(uri: URI, { shouldRealign }: { shouldRealign: false | { newText: string, oldRange: IRange } }) { if (shouldRealign) { const { newText, oldRange } = shouldRealign - console.log('realiging', newText, oldRange) + // console.log('realiging', newText, oldRange) this._realignAllDiffAreasLines(uri, newText, oldRange) } this._refreshStylesAndDiffsInURI(uri) @@ -281,13 +351,15 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { // sweepLine ... sweepLine const fn1 = this._addLineDecoration(model, diffArea._streamState.line, diffArea._streamState.line, 'void-sweepIdxBG') // sweepLine+1 ... endLine - const fn2 = this._addLineDecoration(model, diffArea._streamState.line + 1, diffArea.endLine, 'void-sweepBG') + const fn2 = diffArea._streamState.line + 1 <= diffArea.endLine ? + this._addLineDecoration(model, diffArea._streamState.line + 1, diffArea.endLine, 'void-sweepBG') + : null diffArea._removeStylesFns.add(() => { fn1?.(); fn2?.(); }) } } - else if (diffArea.type === 'CtrlKZone') { + else if (diffArea.type === 'CtrlKZone' && diffArea._linkedStreamingDiffZone === null) { // highlight zone's text const fn = this._addLineDecoration(model, diffArea.startLine, diffArea.endLine, 'void-highlightBG') diffArea._removeStylesFns.add(() => fn?.()); @@ -319,6 +391,40 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { } } + private _addAcceptRejectUI(uri: URI) { + + // find all diffzones that aren't streaming + const diffZones: DiffZone[] = [] + for (let diffareaid of this.diffAreasOfURI[uri.fsPath]) { + const diffArea = this.diffAreaOfId[diffareaid] + if (diffArea.type !== 'DiffZone') continue + if (diffArea._streamState.isStreaming) continue + diffZones.push(diffArea) + } + if (diffZones.length === 0) return + + const consistentItemId = this._consistentItemService.addConsistentItemToURI({ + uri, + fn: (editor) => { + const buttonsWidget = new AcceptAllRejectAllWidget({ + editor, + onAcceptAll: () => { + this.removeDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false }) + this._metricsService.capture('Accept All', {}) + }, + onRejectAll: () => { + this.removeDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false }) + this._metricsService.capture('Reject All', {}) + }, + }) + return () => { buttonsWidget.dispose() } + } + }) + + + return () => { this._consistentItemService.removeConsistentItemFromURI(consistentItemId) } + } + mostRecentTextOfCtrlKZoneId: Record = {} private _addCtrlKZoneInput = (ctrlKZone: CtrlKZone) => { @@ -329,16 +435,22 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { let zoneId: string | null = null let viewZone_: IViewZone | null = null - const inputBoxRef: { current: InputBox | null } = { current: null } + const textAreaRef: { current: HTMLTextAreaElement | null } = { current: null } + + + const paddingLeft = getLeadingWhitespacePx(editor, ctrlKZone.startLine) const itemId = this._consistentEditorItemService.addToEditor(editor, () => { const domNode = document.createElement('div'); domNode.style.zIndex = '1' + domNode.style.height = 'auto' + domNode.style.paddingLeft = `${paddingLeft}px` const viewZone: IViewZone = { afterLineNumber: ctrlKZone.startLine - 1, domNode: domNode, - heightInPx: 52, + // heightInPx: 80, suppressMouseDown: false, + showInHiddenAreas: true, }; viewZone_ = viewZone @@ -350,26 +462,30 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { // mount react this._instantiationService.invokeFunction(accessor => { mountCtrlK(domNode, accessor, { + diffareaid: ctrlKZone.diffareaid, - onGetInputBox: (inputBox) => { - inputBoxRef.current = inputBox - // if it's mounting for the first time, focus it + initStreamingDiffZoneId: ctrlKZone._linkedStreamingDiffZone, + + textAreaRef: (r) => { + textAreaRef.current = r + if (!textAreaRef.current) return + if (!(ctrlKZone.diffareaid in this.mostRecentTextOfCtrlKZoneId)) { // detect first mount this way (a hack) this.mostRecentTextOfCtrlKZoneId[ctrlKZone.diffareaid] = undefined - setTimeout(() => inputBox.focus(), 0) + setTimeout(() => textAreaRef.current?.focus(), 100) } }, onChangeHeight(height) { - if (height === undefined) return + if (height === 0) return // the viewZone sets this height to the container if it's out of view, ignore it viewZone.heightInPx = height // re-render with this new height editor.changeViewZones(accessor => { - if (zoneId) { - accessor.layoutZone(zoneId) - } + if (zoneId) accessor.layoutZone(zoneId) }) }, - onUserUpdateText: (text) => { this.mostRecentTextOfCtrlKZoneId[ctrlKZone.diffareaid] = text; }, + onChangeText: (text) => { + this.mostRecentTextOfCtrlKZoneId[ctrlKZone.diffareaid] = text; + }, initText: this.mostRecentTextOfCtrlKZoneId[ctrlKZone.diffareaid] ?? null, } satisfies QuickEditPropsType) @@ -382,7 +498,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { }) return { - inputBoxRef, + textAreaRef, refresh: () => editor.changeViewZones(accessor => { if (zoneId && viewZone_) { viewZone_.afterLineNumber = ctrlKZone.startLine - 1 @@ -403,7 +519,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { if (diffArea.type !== 'CtrlKZone') continue if (!diffArea._mountInfo) { diffArea._mountInfo = this._addCtrlKZoneInput(diffArea) - console.log('MOUNTED', diffArea.diffareaid) + // console.log('MOUNTED', diffArea.diffareaid) } else { diffArea._mountInfo.refresh() @@ -438,24 +554,55 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { const domNode = document.createElement('div'); domNode.className = 'void-redBG' - const renderOptions = RenderOptions.fromEditor(editor); - // applyFontInfo(domNode, renderOptions.fontInfo) + const renderOptions = RenderOptions.fromEditor(editor) - // Compute view-lines based on redText - const redText = diff.originalCode - const lines = redText.split('\n'); - const lineTokens = lines.map(line => LineTokens.createFromTextAndMetadata([{ text: line, metadata: 0 }], this._langService.languageIdCodec)); - const source = new LineSource(lineTokens, lines.map(() => null), false, false) - const result = renderLines(source, renderOptions, [], domNode); + const processedText = diff.originalCode.replace(/\t/g, ' '.repeat(renderOptions.tabSize)); + + const lines = processedText.split('\n'); + + const linesContainer = document.createElement('div'); + linesContainer.style.fontFamily = renderOptions.fontInfo.fontFamily + linesContainer.style.fontSize = `${renderOptions.fontInfo.fontSize}px` + linesContainer.style.lineHeight = `${renderOptions.fontInfo.lineHeight}px` + // linesContainer.style.tabSize = `${tabWidth}px` // \t + linesContainer.style.whiteSpace = 'pre' + linesContainer.style.position = 'relative' + linesContainer.style.width = '100%' + + lines.forEach(line => { + // div for current line + const lineDiv = document.createElement('div'); + lineDiv.className = 'view-line'; + lineDiv.style.whiteSpace = 'pre' + lineDiv.style.position = 'relative' + lineDiv.style.height = `${renderOptions.fontInfo.lineHeight}px` + + // span (this is just how vscode does it) + const span = document.createElement('span'); + span.textContent = line || '\u00a0'; + span.style.whiteSpace = 'pre' + span.style.display = 'inline-block' + + lineDiv.appendChild(span); + linesContainer.appendChild(lineDiv); + }); + + domNode.appendChild(linesContainer); + + // Calculate height based on number of lines and line height + const heightInLines = lines.length; + const minWidthInPx = Math.max(...lines.map(line => + Math.ceil(renderOptions.fontInfo.typicalFullwidthCharacterWidth * line.length) + )); const viewZone: IViewZone = { - // afterLineNumber: computedDiff.startLine - 1, afterLineNumber: type === 'edit' ? diff.endLine : diff.startLine - 1, - heightInLines: result.heightInLines, - minWidthInPx: result.minWidthInPx, - domNode: domNode, - marginDomNode: document.createElement('div'), // displayed to left - suppressMouseDown: true, + heightInLines, + minWidthInPx, + domNode, + marginDomNode: document.createElement('div'), + suppressMouseDown: false, + showInHiddenAreas: false, }; let zoneId: string | null = null @@ -470,23 +617,36 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { - // Accept | Reject widget - const consistentWidgetId = this._consistentItemService.addConsistentItemToURI({ - uri, - fn: (editor) => { - const buttonsWidget = new AcceptRejectWidget({ - editor, - onAccept: () => { this.acceptDiff({ diffid }) }, - onReject: () => { this.rejectDiff({ diffid }) }, - diffid: diffid.toString(), - startLine: diff.startLine, - }) - return () => { buttonsWidget.dispose() } - } - }) - disposeInThisEditorFns.push(() => { this._consistentItemService.removeConsistentItemFromURI(consistentWidgetId) }) - - + const diffZone = this.diffAreaOfId[diff.diffareaid] + if (diffZone.type === 'DiffZone' && !diffZone._streamState.isStreaming) { + // Accept | Reject widget + const consistentWidgetId = this._consistentItemService.addConsistentItemToURI({ + uri, + fn: (editor) => { + const buttonsWidget = new AcceptRejectWidget({ + editor, + onAccept: () => { + this.acceptDiff({ diffid }) + this._metricsService.capture('Accept Diff', {}) + }, + onReject: () => { + this.rejectDiff({ diffid }) + this._metricsService.capture('Reject Diff', {}) + }, + diffid: diffid.toString(), + startLine: diff.startLine, + offsetLines: ( + diff.type === 'insertion' ? 0 + : diff.type === 'deletion' ? -(diff.originalEndLine - diff.originalStartLine + 1) + : diff.type === 'edit' ? (diff.endLine - diff.startLine + 1) + : 0 // not allowed + ) + }) + return () => { buttonsWidget.dispose() } + } + }) + disposeInThisEditorFns.push(() => { this._consistentItemService.removeConsistentItemFromURI(consistentWidgetId) }) + } const disposeInEditor = () => { disposeInThisEditorFns.forEach(f => f()) } return disposeInEditor; @@ -502,8 +662,9 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { } return model } - private _readURI(uri: URI): string | null { - return this._getModel(uri)?.getValue(EndOfLinePreference.LF) ?? null + private _readURI(uri: URI, range?: IRange): string | null { + if (!range) return this._getModel(uri)?.getValue(EndOfLinePreference.LF) ?? null + else return this._getModel(uri)?.getValueInRange(range, EndOfLinePreference.LF) ?? null } private _getNumLines(uri: URI): number | null { return this._getModel(uri)?.getLineCount() ?? null @@ -517,15 +678,30 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { } weAreWriting = false - private _writeText(uri: URI, text: string, range: IRange, { shouldRealignDiffAreas }: { shouldRealignDiffAreas: boolean }) { + private async _writeText(uri: URI, text: string, range: IRange, { shouldRealignDiffAreas }: { shouldRealignDiffAreas: boolean }) { const model = this._getModel(uri) if (!model) return + const uriStr = this._readURI(uri, range) + if (uriStr === null) return + + // heuristic check if don't need to make edits + const dontNeedToWrite = uriStr === text + if (dontNeedToWrite) { + // at the end of a write, we still expect to refresh all styles + // e.g. sometimes we expect to restore all the decorations even if no edits were made when _writeText is used + this._refreshStylesAndDiffsInURI(uri) + return + } + + // minimal edits so not so flashy + // const edits = this.worker.$Void_computeMoreMinimalEdits(uri.toString(), [{ range, text }], false) this.weAreWriting = true - model.applyEdits([{ range, text }]) // applies edits without adding them to undo/redo stack + model.applyEdits([{ range, text }]) this.weAreWriting = false this._onInternalChangeContent(uri, { shouldRealign: shouldRealignDiffAreas && { newText: text, oldRange: range } }) + } @@ -564,7 +740,6 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { this._deleteAllDiffAreas(uri) this.diffAreasOfURI[uri.fsPath].clear() - console.log('RESTORING FOR', uri) const { snapshottedDiffAreaOfId, entireFileCode: entireModelCode } = structuredClone(snapshot) // don't want to destroy the snapshot // restore diffAreaOfId and diffAreasOfModelId @@ -578,7 +753,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { type: 'DiffZone', _diffOfId: {}, _URI: uri, - _streamState: { isStreaming: false }, + _streamState: { isStreaming: false }, // when restoring, we will never be streaming _removeStylesFns: new Set(), } } @@ -588,21 +763,22 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { _URI: uri, _removeStylesFns: new Set(), _mountInfo: null, + _linkedStreamingDiffZone: null, // when restoring, we will never be streaming } } this.diffAreasOfURI[uri.fsPath].add(diffareaid) } + this._onDidAddOrDeleteDiffZones.fire({ uri }) // restore file content const numLines = this._getNumLines(uri) if (numLines === null) return + + this._writeText(uri, entireModelCode, { startColumn: 1, startLineNumber: 1, endLineNumber: numLines, endColumn: Number.MAX_SAFE_INTEGER }, { shouldRealignDiffAreas: false } ) - - // restore all the decorations - // this._refreshStylesAndDiffsInURI(uri) } const beforeSnapshot: HistorySnapshot = getCurrentSnapshot() @@ -648,7 +824,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { } - // clears all Diffs (and their styles) and all styles of DiffAreas + // clears all Diffs (and their styles) and all styles of DiffAreas, etc private _clearAllEffects(uri: URI) { for (let diffareaid of this.diffAreasOfURI[uri.fsPath]) { const diffArea = this.diffAreaOfId[diffareaid] @@ -662,6 +838,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { this._clearAllDiffAreaEffects(diffZone) delete this.diffAreaOfId[diffZone.diffareaid] this.diffAreasOfURI[diffZone._URI.fsPath].delete(diffZone.diffareaid.toString()) + this._onDidAddOrDeleteDiffZones.fire({ uri: diffZone._URI }) } private _deleteCtrlKZone(ctrlKZone: CtrlKZone) { @@ -801,7 +978,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { // @throttle(100) - private _writeDiffZoneLLMText(diffZone: DiffZone, llmText: string, latestCurrentFileEnd: IPosition, newPosition: IPosition) { + private _writeStreamedDiffZoneLLMText(diffZone: DiffZone, llmText: string, deltaText: string, latest: { line: number, col: number, addedSplitYet: boolean, originalCodeStartLine: number }) { // ----------- 1. Write the new code to the document ----------- // figure out where to highlight based on where the AI is in the stream right now, use the last diff to figure that out @@ -822,6 +999,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { const lastDiff = computedDiffs.pop() if (!lastDiff) { + // console.log('!lastDiff') // if the writing is identical so far, display no changes originalCodeStartLine = 1 newCodeEndLine = 1 @@ -837,16 +1015,41 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { } - // lines are 1-indexed - const newCodeTop = llmText.split('\n').slice(0, (newCodeEndLine - 1) + 1).join('\n') - const oldFileBottom = diffZone.originalCode.split('\n').slice((originalCodeStartLine - 1) + 1, Infinity).join('\n') - const newCode = `${newCodeTop}\n${oldFileBottom}` + // at the start, add a newline between the stream and originalCode to make reasoning easier + if (!latest.addedSplitYet) { + this._writeText(uri, '\n', + { startLineNumber: latest.line, startColumn: latest.col, endLineNumber: latest.line, endColumn: latest.col, }, + { shouldRealignDiffAreas: true } + ) + latest.addedSplitYet = true + } - this._writeText(uri, newCode, - { startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER, }, // 1-indexed + // insert deltaText at latest line and col + this._writeText(uri, deltaText, + { startLineNumber: latest.line, startColumn: latest.col, endLineNumber: latest.line, endColumn: latest.col }, { shouldRealignDiffAreas: true } ) + latest.line += deltaText.split('\n').length - 1 + const lastNewlineIdx = deltaText.lastIndexOf('\n') + latest.col = lastNewlineIdx === -1 ? latest.col + deltaText.length : deltaText.length - lastNewlineIdx + + // delete or insert to get original up to speed + if (latest.originalCodeStartLine < originalCodeStartLine) { + // moved up, delete + const numLinesDeleted = originalCodeStartLine - latest.originalCodeStartLine + this._writeText(uri, '', + { startLineNumber: latest.line, startColumn: latest.col, endLineNumber: latest.line + numLinesDeleted, endColumn: Number.MAX_SAFE_INTEGER, }, + { shouldRealignDiffAreas: true } + ) + } + else if (latest.originalCodeStartLine > originalCodeStartLine) { + this._writeText(uri, '\n' + diffZone.originalCode.split('\n').slice((originalCodeStartLine - 1), (latest.originalCodeStartLine - 1) - 1 + 1).join('\n'), + { startLineNumber: latest.line, startColumn: latest.col, endLineNumber: latest.line, endColumn: latest.col }, + { shouldRealignDiffAreas: true } + ) + } + latest.originalCodeStartLine = originalCodeStartLine // add diffZone.startLine to convert to right coordinate system (line in file, not in diffarea) diffZone._streamState.line = (diffZone.startLine - 1) + newCodeEndLine @@ -914,18 +1117,21 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { const uri = editor.getModel()?.uri if (!uri) return - // check if there's overlap with any other ctrlKZones and if so, focus them - for (const diffareaid of this.diffAreasOfURI[uri.fsPath]) { - const diffArea = this.diffAreaOfId[diffareaid] - if (!diffArea) continue - if (diffArea.type !== 'CtrlKZone') continue - const noOverlap = diffArea.startLine > endLine || diffArea.endLine < startLine - if (!noOverlap) { - setTimeout(() => diffArea._mountInfo?.inputBoxRef.current?.focus(), 0) - return - } + // check if there's overlap with any other ctrlKZone and if so, focus it + const overlappingCtrlKZone = this._findOverlappingDiffArea({ startLine, endLine, uri, filter: (diffArea) => diffArea.type === 'CtrlKZone' }) + if (overlappingCtrlKZone) { + editor.revealLine(overlappingCtrlKZone.startLine) // important + setTimeout(() => (overlappingCtrlKZone as CtrlKZone)._mountInfo?.textAreaRef.current?.focus(), 100) + return } + const overlappingDiffZone = this._findOverlappingDiffArea({ startLine, endLine, uri, filter: (diffArea) => diffArea.type === 'DiffZone' }) + if (overlappingDiffZone) + return + + editor.revealLine(startLine) + editor.setSelection({ startLineNumber: startLine, endLineNumber: startLine, startColumn: 1, endColumn: 1 }) + const { onFinishEdit } = this._addToHistory(uri) const adding: Omit = { @@ -936,6 +1142,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { _URI: uri, _removeStylesFns: new Set(), _mountInfo: null, + _linkedStreamingDiffZone: null, } const ctrlKZone = this._addDiffArea(adding) this._refreshStylesAndDiffsInURI(uri) @@ -944,6 +1151,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { return ctrlKZone.diffareaid } + // _remove means delete and also add to history public removeCtrlKZone({ diffareaid }: { diffareaid: number }) { const ctrlKZone = this.diffAreaOfId[diffareaid] if (!ctrlKZone) return @@ -966,6 +1174,19 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { + private _findOverlappingDiffArea({ startLine, endLine, uri, filter }: { startLine: number, endLine: number, uri: URI, filter?: (diffArea: DiffArea) => boolean }): DiffArea | null { + // check if there's overlap with any other diffAreas and return early if there is + for (const diffareaid of this.diffAreasOfURI[uri.fsPath]) { + const diffArea = this.diffAreaOfId[diffareaid] + if (!diffArea) continue + if (!filter?.(diffArea)) continue + const noOverlap = diffArea.startLine > endLine || diffArea.endLine < startLine + if (!noOverlap) { + return diffArea + } + } + return null + } private _initializeStartApplying(opts: StartApplyingOpts): DiffZone | undefined { @@ -983,7 +1204,8 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { if (!uri_) return uri = uri_ - // __TODO__ reject all diffs in the diff area + // reject all diffZones on this URI, adding to history (there can't possibly be overlap after this) + this.removeDiffAreas({ uri, behavior: 'reject', removeCtrlKs: true }) // in ctrl+L the start and end lines are the full document const numLines = this._getNumLines(uri) @@ -991,34 +1213,20 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { startLine = 1 endLine = numLines - // check if there's overlap with any other diffAreas and return early if there is - for (const diffareaid of this.diffAreasOfURI[uri.fsPath]) { - const da2 = this.diffAreaOfId[diffareaid] - if (!da2) continue - const noOverlap = da2.startLine > endLine || da2.endLine < startLine - if (!noOverlap) { - // TODO add a message here that says this to the user too - console.error('Not diffing because found overlap:', this.diffAreasOfURI[uri.fsPath], startLine, endLine) - return - } - } - userMessage = opts.userMessage } else if (featureName === 'Ctrl+K') { const { diffareaid } = opts - const ctrlKZone = this.diffAreaOfId[diffareaid] if (ctrlKZone.type !== 'CtrlKZone') return const { startLine: startLine_, endLine: endLine_, _URI, _mountInfo } = ctrlKZone uri = _URI - startLine = startLine_ endLine = endLine_ - if (!_mountInfo?.inputBoxRef.current) return - userMessage = _mountInfo.inputBoxRef.current?.value + if (!_mountInfo?.textAreaRef.current) return + userMessage = _mountInfo.textAreaRef.current?.value } else { throw new Error(`Void: diff.type not recognized on: ${featureName}`) @@ -1035,13 +1243,9 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { // add to history const { onFinishEdit } = this._addToHistory(uri) - - // // for Ctrl+K, delete the current ctrlKZone, swapping it out for a diffZone - // if (featureName === 'Ctrl+K') { - // const { diffareaid } = opts - // const ctrlKZone = this.diffAreaOfId[diffareaid] - // this._deleteDiffArea(ctrlKZone) - // } + // __TODO__ let users customize modelFimTags + const isOllamaFIM = false // this._voidSettingsService.state.modelSelectionOfFeature['Ctrl+K']?.providerName === 'ollama' + const modelFimTags = defaultFimTags const adding: Omit = { type: 'DiffZone', @@ -1058,72 +1262,109 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { _removeStylesFns: new Set(), } const diffZone = this._addDiffArea(adding) + this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) + this._onDidAddOrDeleteDiffZones.fire({ uri }) + if (featureName === 'Ctrl+K') { + const { diffareaid } = opts + const ctrlKZone = this.diffAreaOfId[diffareaid] + if (ctrlKZone.type !== 'CtrlKZone') return + + ctrlKZone._linkedStreamingDiffZone = diffZone.diffareaid + } + + // now handle messages let messages: LLMMessage[] if (featureName === 'Ctrl+L') { - const userContent = ctrlLStream_prompt({ originalCode, userMessage }) + const userContent = ctrlLStream_prompt({ originalCode, userMessage, uri }) messages = [ - // TODO include more context too { role: 'system', content: ctrlLStream_systemMessage, }, { role: 'user', content: userContent, } ] } else if (featureName === 'Ctrl+K') { const { prefix, suffix } = ctrlKStream_prefixAndSuffix({ fullFileStr: currentFileStr, startLine, endLine }) - const userContent = ctrlKStream_prompt({ selection: originalCode, userMessage, prefix, suffix }) - console.log('PREFIX:\n', prefix) - console.log('SUFFIX:\n', suffix) - console.log('USER CONTENT:\n', userContent) + // console.log('PREFIX:\n', prefix) + // console.log('SUFFIX:\n', suffix) + // console.log('USER CONTENT:\n', userContent) + + // __TODO__ use Ollama's FIM api + // if (isOllamaFIM) {...} else: + const language = filenameToVscodeLanguage(uri.fsPath) ?? '' + const userContent = ctrlKStream_prompt({ selection: originalCode, userMessage, prefix, suffix, isOllamaFIM: false, fimTags: modelFimTags, language }) messages = [ - // TODO include more context too (LSP, file history, etc) { role: 'system', content: ctrlKStream_systemMessage, }, { role: 'user', content: userContent, } ] } else { throw new Error(`featureName ${featureName} is invalid`) } - // __TODO__ make these only move forward - const latestCurrentFileEnd: IPosition = { lineNumber: 1, column: 1 } - const latestOriginalFileStart: IPosition = { lineNumber: 1, column: 1 } - const onDone = () => { + const onDone = (hadError: boolean) => { diffZone._streamState = { isStreaming: false, } + this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) if (featureName === 'Ctrl+K') { const ctrlKZone = this.diffAreaOfId[opts.diffareaid] as CtrlKZone + + ctrlKZone._linkedStreamingDiffZone = null this._deleteCtrlKZone(ctrlKZone) } this._refreshStylesAndDiffsInURI(uri) - onFinishEdit() + + // if had error, revert! + if (hadError) { + this._undoHistory(diffZone._URI) + } } // refresh now in case onText takes a while to get 1st message this._refreshStylesAndDiffsInURI(uri) + + const extractText = (fullText: string, recentlyAddedTextLen: number) => { + if (featureName === 'Ctrl+K') { + if (isOllamaFIM) return fullText + return extractCodeFromFIM({ text: fullText, recentlyAddedTextLen, midTag: modelFimTags.midTag }) + } + else if (featureName === 'Ctrl+L') { + return extractCodeFromRegular({ text: fullText, recentlyAddedTextLen }) + } + throw 1 + } + + const latestStreamInfo = { line: diffZone.startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 } streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({ - featureName, + useProviderFor: featureName, logging: { loggingName: `startApplying - ${featureName}` }, messages, onText: ({ newText, fullText }) => { - this._writeDiffZoneLLMText(diffZone, fullText, latestCurrentFileEnd, latestOriginalFileStart) + const [text, deltaText] = extractText(fullText, newText.length) + + this._writeStreamedDiffZoneLLMText(diffZone, text, deltaText, latestStreamInfo) this._refreshStylesAndDiffsInURI(uri) }, onFinalMessage: ({ fullText }) => { + // console.log('DONE! FULL TEXT\n', extractText(fullText), diffZone.startLine, diffZone.endLine) // at the end, re-write whole thing to make sure no sync errors - this._writeText(uri, fullText, + const [text, _] = extractText(fullText, 0) + this._writeText(uri, text, { startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed - { shouldRealignDiffAreas: false } + { shouldRealignDiffAreas: true } ) - onDone() + onDone(false) }, onError: (e) => { console.error('Error rewriting file with diff', e); - // TODO indicate there was an error - if (streamRequestIdRef.current) - this._llmMessageService.abort(streamRequestIdRef.current) - onDone() + const details = errorDetails(e.fullError) + this._notificationService.notify({ + severity: Severity.Warning, + message: `Void Error: ${e.message}`, + source: details ? `(Hold ${isMacintosh ? 'Option' : 'Alt'} to hover) - ${details}` : undefined + }) + onDone(true) }, range: { startLineNumber: startLine, endLineNumber: endLine, startColumn: 1, endColumn: Number.MAX_SAFE_INTEGER }, @@ -1137,17 +1378,20 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { private _stopIfStreaming(diffZone: DiffZone) { + const uri = diffZone._URI const streamRequestId = diffZone._streamState.streamRequestIdRef?.current - if (!streamRequestId) - return + if (!streamRequestId) return this._llmMessageService.abort(streamRequestId) diffZone._streamState = { isStreaming: false, } - + this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) } + _undoHistory(uri: URI) { + this._undoRedoService.undo(uri) + } // call this outside undo/redo (it calls undo). this is only for aborting a diffzone stream interruptStreaming(diffareaid: number) { @@ -1158,7 +1402,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { if (!diffArea._streamState.isStreaming) return this._stopIfStreaming(diffArea) - this._undoRedoService.undo(diffArea._URI) + this._undoHistory(diffArea._URI) } @@ -1167,6 +1411,55 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { + // public removeDiffZone(diffZone: DiffZone, behavior: 'reject' | 'accept') { + // const uri = diffZone._URI + // const { onFinishEdit } = this._addToHistory(uri) + + // if (behavior === 'reject') this._revertAndDeleteDiffZone(diffZone) + // else if (behavior === 'accept') this._deleteDiffZone(diffZone) + + // this._refreshStylesAndDiffsInURI(uri) + // onFinishEdit() + // } + + private _revertAndDeleteDiffZone(diffZone: DiffZone) { + const uri = diffZone._URI + + const writeText = diffZone.originalCode + const toRange: IRange = { startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER } + this._writeText(uri, writeText, toRange, { shouldRealignDiffAreas: true }) + + this._deleteDiffZone(diffZone) + } + + + // remove a batch of diffareas all at once (and handle accept/reject of their diffs) + public removeDiffAreas({ uri, removeCtrlKs, behavior }: { uri: URI, removeCtrlKs: boolean, behavior: 'reject' | 'accept' }) { + + const diffareaids = this.diffAreasOfURI[uri.fsPath] + if (diffareaids.size === 0) return // do nothing + + const { onFinishEdit } = this._addToHistory(uri) + + for (const diffareaid of diffareaids) { + const diffArea = this.diffAreaOfId[diffareaid] + if (!diffArea) continue + + if (diffArea.type == 'DiffZone') { + if (behavior === 'reject') this._revertAndDeleteDiffZone(diffArea) + else if (behavior === 'accept') this._deleteDiffZone(diffArea) + } + else if (diffArea.type === 'CtrlKZone' && removeCtrlKs) { + this._deleteCtrlKZone(diffArea) + } + } + + this._refreshStylesAndDiffsInURI(uri) + onFinishEdit() + } + + + // called on void.acceptDiff public async acceptDiff({ diffid }: { diffid: number }) { @@ -1278,8 +1571,18 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { // B| <-- endLine (we want to delete this whole line) // C else if (diff.type === 'insertion') { - writeText = '' - toRange = { startLineNumber: diff.startLine, startColumn: 1, endLineNumber: diff.endLine + 1, endColumn: 1 } // 1-indexed + // console.log('REJECTING:', diff) + // handle the case where the insertion was a newline at end of diffarea (applying to the next line doesnt work because it doesnt exist, vscode just doesnt delete the correct # of newlines) + if (diff.endLine === diffArea.endLine) { + // delete the line before instead of after + writeText = '' + toRange = { startLineNumber: diff.startLine - 1, startColumn: Number.MAX_SAFE_INTEGER, endLineNumber: diff.endLine, endColumn: 1 } // 1-indexed + } + else { + writeText = '' + toRange = { startLineNumber: diff.startLine, startColumn: 1, endLineNumber: diff.endLine + 1, endColumn: 1 } // 1-indexed + } + } // if it was an edit, just edit the range // (this image applies to writeText and toRange, not newOriginalCode) @@ -1294,12 +1597,9 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { throw new Error(`Void error: ${diff}.type not recognized`) } - console.log('REJECTION start, end:', diffArea.startLine, diffArea.endLine) // update the file this._writeText(uri, writeText, toRange, { shouldRealignDiffAreas: true }) - console.log('2REJECTION start, end:', diffArea.startLine, diffArea.endLine) - // originalCode does not change! // delete the diff @@ -1316,12 +1616,60 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { } + + + + // testDiffs(): DiffZone | undefined { + // const uri = this._getActiveEditorURI() + // if (!uri) return + + // const startLine = 1 + // const endLine = 4 + + // const currentFileStr = this._readURI(uri) + // if (currentFileStr === null) return + // const originalCode = currentFileStr.split('\n').slice((startLine - 1), (endLine - 1) + 1).join('\n') + + // const { onFinishEdit } = this._addToHistory(uri) + // const adding: Omit = { + // type: 'DiffZone', + // originalCode, + // startLine, + // endLine, + // _URI: uri, + // _streamState: { isStreaming: false, }, + // _diffOfId: {}, // added later + // _removeStylesFns: new Set(), + // } + // const diffZone = this._addDiffArea(adding) + // const endResult = `\ + // const x = 1; + // if (x > 0) { + // console.log('hi!') + // }` + // this._writeText(uri, endResult, + // { startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed + // { shouldRealignDiffAreas: true } + // ) + // diffZone._streamState = { isStreaming: false, } + // this._refreshStylesAndDiffsInURI(uri) + // onFinishEdit() + + // return diffZone + // } + } registerSingleton(IInlineDiffsService, InlineDiffsService, InstantiationType.Eager); - - +const acceptBg = '#1a7431' +const acceptAllBg = '#1e8538' +const acceptBorder = '1px solid #145626' +const rejectBg = '#b42331' +const rejectAllBg = '#cf2838' +const rejectBorder = '1px solid #8e1c27' +const buttonFontSize = '11px' +const buttonTextColor = 'white' class AcceptRejectWidget extends Widget implements IOverlayWidget { @@ -1334,13 +1682,16 @@ class AcceptRejectWidget extends Widget implements IOverlayWidget { private readonly ID private readonly startLine - constructor({ editor, onAccept, onReject, diffid, startLine }: { editor: ICodeEditor; onAccept: () => void; onReject: () => void; diffid: string, startLine: number }) { + constructor({ editor, onAccept, onReject, diffid, startLine, offsetLines }: { editor: ICodeEditor; onAccept: () => void; onReject: () => void; diffid: string, startLine: number, offsetLines: number }) { super() + this.ID = editor.getModel()?.uri.fsPath + diffid; this.editor = editor; this.startLine = startLine; + const lineHeight = editor.getOption(EditorOption.lineHeight); + // Create container div with buttons const { acceptButton, rejectButton, buttons } = dom.h('div@buttons', [ dom.h('button@acceptButton', []), @@ -1351,29 +1702,46 @@ class AcceptRejectWidget extends Widget implements IOverlayWidget { buttons.style.display = 'flex'; buttons.style.position = 'absolute'; buttons.style.gap = '4px'; - buttons.style.padding = '4px'; - buttons.style.zIndex = '1000'; + buttons.style.paddingRight = '4px'; + buttons.style.zIndex = '1'; + buttons.style.transform = `translateY(${offsetLines * lineHeight}px)`; // Style accept button acceptButton.onclick = onAccept; acceptButton.textContent = 'Accept'; - acceptButton.style.backgroundColor = '#28a745'; - acceptButton.style.color = 'white'; - acceptButton.style.border = 'none'; - acceptButton.style.padding = '4px 8px'; - acceptButton.style.borderRadius = '3px'; + acceptButton.style.backgroundColor = acceptBg; + acceptButton.style.border = acceptBorder; + acceptButton.style.color = buttonTextColor; + acceptButton.style.fontSize = buttonFontSize; + acceptButton.style.borderTop = 'none'; + acceptButton.style.padding = '1px 4px'; + acceptButton.style.borderBottomLeftRadius = '6px'; + acceptButton.style.borderBottomRightRadius = '6px'; + acceptButton.style.borderTopLeftRadius = '0'; + acceptButton.style.borderTopRightRadius = '0'; acceptButton.style.cursor = 'pointer'; + acceptButton.style.height = '100%'; + acceptButton.style.boxShadow = '0 2px 3px rgba(0,0,0,0.2)'; // Style reject button rejectButton.onclick = onReject; rejectButton.textContent = 'Reject'; - rejectButton.style.backgroundColor = '#dc3545'; - rejectButton.style.color = 'white'; - rejectButton.style.border = 'none'; - rejectButton.style.padding = '4px 8px'; - rejectButton.style.borderRadius = '3px'; + rejectButton.style.backgroundColor = rejectBg; + rejectButton.style.border = rejectBorder; + rejectButton.style.color = buttonTextColor; + rejectButton.style.fontSize = buttonFontSize; + rejectButton.style.borderTop = 'none'; + rejectButton.style.padding = '1px 4px'; + rejectButton.style.borderBottomLeftRadius = '6px'; + rejectButton.style.borderBottomRightRadius = '6px'; + rejectButton.style.borderTopLeftRadius = '0'; + rejectButton.style.borderTopRightRadius = '0'; rejectButton.style.cursor = 'pointer'; + rejectButton.style.height = '100%'; + rejectButton.style.boxShadow = '0 2px 3px rgba(0,0,0,0.2)'; + + this._domNode = buttons; @@ -1382,10 +1750,19 @@ class AcceptRejectWidget extends Widget implements IOverlayWidget { this._domNode.style.top = `${topPx}px` } const updateLeft = () => { - const leftPx = 0//editor.getScrollLeft() - editor.getScrollWidth() - this._domNode.style.left = `${leftPx}px` + const layoutInfo = editor.getLayoutInfo(); + const minimapWidth = layoutInfo.minimap.minimapWidth; + const verticalScrollbarWidth = layoutInfo.verticalScrollbarWidth; + const buttonWidth = this._domNode.offsetWidth; + + const leftPx = layoutInfo.width - minimapWidth - verticalScrollbarWidth - buttonWidth; + this._domNode.style.left = `${leftPx}px`; } + // Mount first, then update positions + editor.addOverlayWidget(this); + + updateTop() updateLeft() @@ -1407,3 +1784,97 @@ class AcceptRejectWidget extends Widget implements IOverlayWidget { } + + + + +class AcceptAllRejectAllWidget extends Widget implements IOverlayWidget { + private readonly _domNode: HTMLElement; + private readonly editor: ICodeEditor; + private readonly ID: string; + + constructor({ editor, onAcceptAll, onRejectAll }: { editor: ICodeEditor, onAcceptAll: () => void, onRejectAll: () => void }) { + super(); + + this.ID = editor.getModel()?.uri.fsPath + ''; + this.editor = editor; + + // Create container div with buttons + const { acceptButton, rejectButton, buttons } = dom.h('div@buttons', [ + dom.h('button@acceptButton', []), + dom.h('button@rejectButton', []) + ]); + + // Style the container + buttons.style.zIndex = '1'; + buttons.style.padding = '4px'; + buttons.style.display = 'flex'; + buttons.style.gap = '4px'; + buttons.style.alignItems = 'center'; + + // Style accept button + acceptButton.addEventListener('click', onAcceptAll) + acceptButton.textContent = 'Accept All'; + acceptButton.style.backgroundColor = acceptAllBg; + acceptButton.style.border = acceptBorder; + acceptButton.style.color = buttonTextColor; + acceptButton.style.fontSize = buttonFontSize; + acceptButton.style.padding = '4px 8px'; + acceptButton.style.borderRadius = '6px'; + acceptButton.style.cursor = 'pointer'; + + // Style reject button + rejectButton.addEventListener('click', onRejectAll) + rejectButton.textContent = 'Reject All'; + rejectButton.style.backgroundColor = rejectAllBg; + rejectButton.style.border = rejectBorder; + rejectButton.style.color = buttonTextColor; + rejectButton.style.fontSize = buttonFontSize; + rejectButton.style.color = 'white'; + rejectButton.style.padding = '4px 8px'; + rejectButton.style.borderRadius = '6px'; + rejectButton.style.cursor = 'pointer'; + + this._domNode = buttons; + + // Mount the widget + editor.addOverlayWidget(this); + } + + + public getId(): string { + return this.ID; + } + + public getDomNode(): HTMLElement { + return this._domNode; + } + + public getPosition() { + return { + preference: OverlayWidgetPositionPreference.BOTTOM_RIGHT_CORNER, + } + } + + public override dispose(): void { + this.editor.removeOverlayWidget(this); + super.dispose(); + } +} + + + +// registerAction2(class extends Action2 { +// constructor() { +// super({ +// id: 'void.testDiff', +// title: localize2('voidTestDiff', 'Void Test Diff'), +// f1: true, +// }); +// } +// async run(accessor: ServicesAccessor): Promise { +// const inlineDiffsService = accessor.get(IInlineDiffsService) +// // inlineDiffsService.testDiffs() + +// } +// }) diff --git a/src/vs/workbench/contrib/void/browser/media/void.css b/src/vs/workbench/contrib/void/browser/media/void.css index 34afe272..e5e9793e 100644 --- a/src/vs/workbench/contrib/void/browser/media/void.css +++ b/src/vs/workbench/contrib/void/browser/media/void.css @@ -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; } diff --git a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts index 15e433c2..a45bdf1c 100644 --- a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts @@ -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 @@ ... @@ -

-
    @@ -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 = ({ items, onItemSelect, onExtraButtonCli \`\`\` COMPLETION -\`\`\` +\`\`\` typescript
      {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} -<${sufTag}>${suffix} -<${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} \`\`\` -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. +Note also that the SELECTION has code that comes after it. This code is indicated with <${sufTag}>...after. -Complete the following: +Instructions: +1. Your OUTPUT should be a SINGLE PIECE OF CODE of the form <${midTag}>...new_selection. 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}>... or <${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} <${sufTag}>${suffix} -<${midTag}>` - } + +Return only the completion block of code (of the form \`\`\` ${language}\n <${midTag}>...new_selection\`\`\`):` }; diff --git a/src/vs/workbench/contrib/void/browser/quickEditActions.ts b/src/vs/workbench/contrib/void/browser/quickEditActions.ts index fbd8b931..125e983c 100644 --- a/src/vs/workbench/contrib/void/browser/quickEditActions.ts +++ b/src/vs/workbench/contrib/void/browser/quickEditActions.ts @@ -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 }) } diff --git a/src/vs/workbench/contrib/void/browser/react/build.js b/src/vs/workbench/contrib/void/browser/react/build.js index bfcbd7e3..436d10ce 100755 --- a/src/vs/workbench/contrib/void/browser/react/build.js +++ b/src/vs/workbench/contrib/void/browser/react/build.js @@ -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); diff --git a/src/vs/workbench/contrib/void/browser/react/src/ctrl-k-tsx/CtrlKChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/ctrl-k-tsx/CtrlKChat.tsx deleted file mode 100644 index 9f7c2042..00000000 --- a/src/vs/workbench/contrib/void/browser/react/src/ctrl-k-tsx/CtrlKChat.tsx +++ /dev/null @@ -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(null) - const inputBoxRef: React.MutableRefObject = 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(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
      -
      { - 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 */} -
      -
      -
      - { inlineDiffsService.removeCtrlKZone({ diffareaid }) }} - /> -
      - - {/* input */} -
      - {/* text input */} - { - inputBoxRef.current = instance; - onGetInputBox(instance); - instance.focus() - }, [onGetInputBox])} - multiline={true} - /> -
      - -
      - - - {/* bottom row */} -
      - {/* submit options */} -
      - -
      - - {/* submit / stop button */} - {isStreaming ? - // stop button - - : - // submit button (up arrow) - - } -
      -
      - - -
      -
      - - -} diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/BlockCode.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/BlockCode.tsx index ad8a76e8..fca06caa 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/BlockCode.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/BlockCode.tsx @@ -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 ( + <> +
      - // Other - 'sql': 'sql', - 'graphql': 'graphql', - 'gql': 'graphql', - 'dockerfile': 'dockerfile', - 'docker': 'dockerfile' -}; + {buttonsOnHover === null ? null : ( +
      +
      + {buttonsOnHover} +
      +
      + )} -export function getLanguageFromFileName(fileName: string): string { + - const ext = fileName.toLowerCase().split('.').pop(); - if (!ext) return 'plaintext'; - - return extensionMap[ext] || 'plaintext'; -} - -export const BlockCode = ({ text, buttonsOnHover, language }: { text: string, buttonsOnHover?: ReactNode, language?: string }) => { - - - const isSingleLine = !text.includes('\n') - - return (<> -
      - {buttonsOnHover === null ? null : ( -
      -
      {buttonsOnHover}
      -
      - )} - - -
      - +
      + ) } - diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index bfe938e8..79429e20 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -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 <>
    ) } if (t.type === "hr") { - return
    + return
    } if (t.type === "blockquote") { - return
    {t.text}
    + return
    {t.text}
    } if (t.type === "list") { @@ -130,14 +164,16 @@ const RenderToken = ({ token, nested = false }: { token: Token | string, nested? return ( {t.items.map((item, index) => ( -
  • +
  • {item.task && ( - + )} - + + +
  • ))}
    @@ -152,13 +188,12 @@ const RenderToken = ({ token, nested = false }: { token: Token | string, nested? if (nested) return contents - return

    {contents}

    + return

    {contents}

    } - // don't actually render tags, just render strings of them if (t.type === "html") { return ( -
    +			
     				{``}
     				{t.raw}
     				{``}
    @@ -176,30 +211,40 @@ const RenderToken = ({ token, nested = false }: { token: Token | string, nested?
     
     	if (t.type === "link") {
     		return (
    -			 { window.open(t.href) }} href={t.href} title={t.title ?? undefined}>
    +			 { window.open(t.href) }}
    +				href={t.href}
    +				title={t.title ?? undefined}
    +			>
     				{t.text}
     			
     		)
     	}
     
     	if (t.type === "image") {
    -		return {t.text}
    +		return {t.text}
     	}
     
     	if (t.type === "strong") {
    -		return {t.text}
    +		return {t.text}
     	}
     
     	if (t.type === "em") {
    -		return {t.text}
    +		return {t.text}
     	}
     
     	// inline code
     	if (t.type === "codespan") {
     		return (
    -			
    +			
     				{t.text}
    -			
    +			
     		)
     	}
     
    @@ -209,24 +254,24 @@ const RenderToken = ({ token, nested = false }: { token: Token | string, nested?
     
     	// strikethrough
     	if (t.type === "del") {
    -		return {t.text}
    +		return {t.text}
     	}
     
     	// default
     	return (
    -		
    - Unknown type: +
    + Unknown type: {t.raw}
    ) } -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) => ( - + ))} ) diff --git a/src/vs/workbench/contrib/void/browser/react/src/ctrl-k-tsx/CtrlK.tsx b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEdit.tsx similarity index 77% rename from src/vs/workbench/contrib/void/browser/react/src/ctrl-k-tsx/CtrlK.tsx rename to src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEdit.tsx index 02f2078d..53c68998 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/ctrl-k-tsx/CtrlK.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEdit.tsx @@ -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
    - +
    diff --git a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx new file mode 100644 index 00000000..80988038 --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx @@ -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(null) + const textAreaRef = useRef(null) + const textAreaFnsRef = 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 [instructionsAreEmpty, setInstructionsAreEmpty] = useState(!(initText ?? '')) // the user's instructions + const isDisabled = instructionsAreEmpty + + const [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone] = useRefState(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
    +
    { + textAreaRef.current?.focus() + }} + > + + {/* // this div is used to position the input box properly */} +
    +
    + + {/* input */} +
    + {/* text input */} + { + 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} + /> +
    + + {/* X button */} +
    + +
    +
    + + + {/* bottom row */} +
    + {/* submit options */} +
    + +
    + + {/* submit / stop button */} + {isStreaming ? + // stop button + + : + // submit button (up arrow) + + } +
    +
    + + +
    +
    + + +} diff --git a/src/vs/workbench/contrib/void/browser/react/src/ctrl-k-tsx/index.tsx b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/index.tsx similarity index 80% rename from src/vs/workbench/contrib/void/browser/react/src/ctrl-k-tsx/index.tsx rename to src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/index.tsx index 53f09fdf..301c0f24 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/ctrl-k-tsx/index.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/index.tsx @@ -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) diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorDisplay.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorDisplay.tsx index f46a67d0..84fe410a 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorDisplay.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorDisplay.tsx @@ -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 (
    {/* Header */} -
    -
    - -
    -

    +
    +
    + +
    +

    {/* eg Error */} Error

    -

    +

    {/* eg Something went wrong */} {message}

    -
    +
    {details && ( - )} {showDismiss && onDismiss && ( - )}
    @@ -75,10 +68,10 @@ export const ErrorDisplay = ({ {/* Expandable Details */} {isExpanded && details && ( -
    +
    - Full Error: -
    {details}
    + Full Error: +
    {details}
    )} diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/Sidebar.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/Sidebar.tsx index 12343f65..fe757a96 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/Sidebar.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/Sidebar.tsx @@ -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
    -
    + // const isDark = useIsDark() + return
    +
    {/* { 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} */} -
    + {/*
    -
    +
    */}
    diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx index 6b3f8d2d..c41c6702 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx @@ -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) => { +export const IconX = ({ size, className = '', ...props }: { size: number, className?: string } & React.SVGProps) => { return ( { return ( @@ -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" > - ); }; @@ -169,38 +172,40 @@ const useResizeObserver = () => { type ButtonProps = ButtonHTMLAttributes -const DEFAULT_BUTTON_SIZE = 20; +const DEFAULT_BUTTON_SIZE = 22; export const ButtonSubmit = ({ className, disabled, ...props }: ButtonProps & Required>) => { return } export const ButtonStop = ({ className, ...props }: ButtonHTMLAttributes) => { return } -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 }) => { const [isAtBottom, setIsAtBottom] = useState(true); // Start at bottom - const divRef = useRef(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 => { - 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 && (
    {selections.map((selection, i) => { const isThisSelectionOpened = !!(selection.selectionStr && selectionIsOpened[i]) + const isThisSelectionAFile = selection.selectionStr === null - return ( -
    + {/* selection summary */} +
    - {/* selection summary */} -
    { - 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 + }); + } }} > - + {/* file name */} {getBasename(selection.fileURI.fsPath)} {/* selection range */} - {selection.selectionStr !== null ? ` (${selection.range.startLineNumber}-${selection.range.endLineNumber})` : ''} + {!isThisSelectionAFile ? ` (${selection.range.startLineNumber}-${selection.range.endLineNumber})` : ''} {/* X button */} {type === 'staging' && { - 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)]) - }} - > - - - } - - {/* type of selection */} - {/* {selection.selectionStr !== null ? 'Selection' : 'File'} */} - {/* X button */} - {/* {type === 'staging' && // hoveredIdx === i - { - 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)]) }} > - - } */} + } +
    - {/* selection text */} - {isThisSelectionOpened && -
    - + + {/* clear all selections button */} + {type !== 'staging' || selections.length === 0 || i !== selections.length - 1 + ? null + :
    +
    setIsClearHovered(true)} + onMouseLeave={() => setIsClearHovered(false)} + > + { setStaging([]) }} + /> +
    }
    - ) + {/* selection text */} + {isThisSelectionOpened && +
    { + e.stopPropagation(); // don't focus input box + }} + > + +
    + } +
    + })} + +
    ) ) } + 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 = <> {chatMessage.displayContent} + + {/* {!isEditMode ? chatMessage.displayContent : <>} */} + {/* edit mode content */} + {/* TODO this should be the same input box as in the Sidebar */} + {/*