diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..541cd9f9 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,32 @@ + + + + + +## Jan. 12, 2025 - Entering beta + + +- Migrated away from VS Code extension API - Void now lives and interacts entirely within the VS Code codebase. + +- Added quick edits! Void handles FIM-prompting and output parsing, inline UI, and history management. + +- 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, stream interruptions, and past chats history. + +- 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. + +Many thanks to our contributors __, __, __ + +## Sept/Oct. 2024 - Early launch + +- Initialized Void's website and GitHub repo. + +- Started a waitlist. + diff --git a/LICENSE-VS-Code.txt b/LICENSE-VS-Code.txt index 6c39f17f..d34de290 100644 --- a/LICENSE-VS-Code.txt +++ b/LICENSE-VS-Code.txt @@ -1,5 +1,5 @@ Void is a fork of VS Code, which is licensed under the MIT License (below). -Void's additions and modifications are licensed under the MIT License (see LICENSE.txt). +Void's additions and modifications are licensed under the Apache 2.0 License (see LICENSE.txt). -------------------- diff --git a/LICENSE.txt b/LICENSE.txt index 432d2c90..7637992d 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,21 +1,201 @@ -MIT License + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ -Copyright (c) 2025 Glass Devtools, Inc. + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: + 1. Definitions. -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2025 Glass Devtools, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. 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.json b/package.json index ef863f0c..ebd91306 100644 --- a/package.json +++ b/package.json @@ -225,6 +225,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/browser/void.contribution.ts b/src/vs/platform/void/browser/void.contribution.ts index 90f09fec..1b3cddd2 100644 --- a/src/vs/platform/void/browser/void.contribution.ts +++ b/src/vs/platform/void/browser/void.contribution.ts @@ -1,7 +1,7 @@ -/*------------------------------------------------------------------------------------------ - * Copyright (c) 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the MIT License. See LICENSE.txt in the project root for more information. - *-----------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ // ---------- common ---------- diff --git a/src/vs/platform/void/common/llmMessageService.ts b/src/vs/platform/void/common/llmMessageService.ts index c33c90cd..9fd039c6 100644 --- a/src/vs/platform/void/common/llmMessageService.ts +++ b/src/vs/platform/void/common/llmMessageService.ts @@ -1,7 +1,7 @@ -/*------------------------------------------------------------------------------------------ - * Copyright (c) 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the MIT License. See LICENSE.txt in the project root for more information. - *-----------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, ServiceSendLLMMessageParams, MainLLMMessageParams, MainLLMMessageAbortParams, ServiceModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, MainModelListParams, OllamaModelResponse, OpenaiCompatibleModelResponse, } from './llmMessageTypes.js'; import { IChannel } from '../../../base/parts/ipc/common/ipc.js'; @@ -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 diff --git a/src/vs/platform/void/common/llmMessageTypes.ts b/src/vs/platform/void/common/llmMessageTypes.ts index 7544630b..db560ab9 100644 --- a/src/vs/platform/void/common/llmMessageTypes.ts +++ b/src/vs/platform/void/common/llmMessageTypes.ts @@ -1,12 +1,25 @@ -/*------------------------------------------------------------------------------------------ - * Copyright (c) 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the MIT License. See LICENSE.txt in the project root for more information. - *-----------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ import { IRange } from '../../../editor/common/core/range' import { ProviderName, SettingsOfProvider } from './voidSettingsTypes.js' +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 diff --git a/src/vs/platform/void/common/metricsService.ts b/src/vs/platform/void/common/metricsService.ts index c922db56..3d185669 100644 --- a/src/vs/platform/void/common/metricsService.ts +++ b/src/vs/platform/void/common/metricsService.ts @@ -1,7 +1,7 @@ -/*------------------------------------------------------------------------------------------ - * Copyright (c) 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the MIT License. See LICENSE.txt in the project root for more information. - *-----------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ import { createDecorator } from '../../instantiation/common/instantiation.js'; import { ProxyChannel } from '../../../base/parts/ipc/common/ipc.js'; diff --git a/src/vs/platform/void/common/refreshModelService.ts b/src/vs/platform/void/common/refreshModelService.ts index 8c170e8d..5a60fffd 100644 --- a/src/vs/platform/void/common/refreshModelService.ts +++ b/src/vs/platform/void/common/refreshModelService.ts @@ -1,7 +1,7 @@ -/*------------------------------------------------------------------------------------------ - * Copyright (c) 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the MIT License. See LICENSE.txt in the project root for more information. - *-----------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ import { createDecorator } from '../../instantiation/common/instantiation.js'; import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js'; @@ -36,7 +36,7 @@ export type RefreshModelStateOfProvider = Record refreshBasedOn[providerName].map(settingName => this.voidSettingsService.state.settingsOfProvider[providerName][settingName]) @@ -101,7 +101,7 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ // if it was just enabled, or there was a change and it wasn't to the enabled state, refresh if ((enabled && !prevEnabled) || (!enabled && !prevEnabled)) { // if user just clicked enable, refresh - this.refreshModels(providerName, !enabled, { isPolling: false, isInternal: true }) + this.refreshModels(providerName, !enabled, { isPolling: false, isInvisible: true }) } else { // else if user just clicked disable, don't refresh @@ -129,39 +129,51 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ state: RefreshModelStateOfProvider = { ollama: { state: 'init', timeoutId: null }, - openAICompatible: { 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 }) { + async refreshModels(providerName: RefreshableProviderName, enableProviderOnSuccess?: boolean, options?: { isPolling?: boolean, isInvisible?: boolean }) { - const { isPolling, isInternal } = options ?? {} + const { isPolling, isInvisible } = options ?? {} - console.log(`refreshModels, isInternal ${isInternal} isPolling ${isPolling}`) + console.log(`refreshModels, isInvisible ${isInvisible} isPolling ${isPolling}`) this._clearProviderTimeout(providerName) // start loading models - if (!isInternal) this._setRefreshState(providerName, 'refreshing') + if (!isInvisible) this._setRefreshState(providerName, 'refreshing') - const fn = providerName === 'ollama' ? this.llmMessageService.ollamaList + 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) - })) + // 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, isPolling, isInvisible } + ) + + // update state if (enableProviderOnSuccess) { this.voidSettingsService.setSettingOfProvider(providerName, '_enabled', true) } - if (!isInternal) this._setRefreshState(providerName, 'finished') + if (!isInvisible) { + this._setRefreshState(providerName, 'finished') + } else if (isInvisible) { + this._setRefreshState(providerName, 'finished_invisible') + } + }, onError: ({ error }) => { @@ -169,7 +181,6 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ } }) - 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 diff --git a/src/vs/platform/void/common/voidSettingsService.ts b/src/vs/platform/void/common/voidSettingsService.ts index fa92c587..f3d43e6b 100644 --- a/src/vs/platform/void/common/voidSettingsService.ts +++ b/src/vs/platform/void/common/voidSettingsService.ts @@ -1,7 +1,7 @@ -/*------------------------------------------------------------------------------------------ - * Copyright (c) 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the MIT License. See LICENSE.txt in the project root for more information. - *-----------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ import { Emitter, Event } from '../../../base/common/event.js'; import { Disposable } from '../../../base/common/lifecycle.js'; @@ -10,6 +10,7 @@ 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 { IMetricsService } from './metricsService.js'; import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, modelInfoOfDefaultNames, VoidModelInfo, FeatureFlagSettings, FeatureFlagName, defaultFeatureFlagSettings } from './voidSettingsTypes.js'; @@ -29,7 +30,7 @@ type SetModelSelectionOfFeatureFn = ( type SetFeatureFlagFn = (flagName: FeatureFlagName, newVal: boolean) => void; -export type ModelOption = { text: string, value: ModelSelection } +export type ModelOption = { name: string, selection: ModelSelection } @@ -55,7 +56,7 @@ export interface IVoidSettingsService { setModelSelectionOfFeature: SetModelSelectionOfFeatureFn; setFeatureFlag: SetFeatureFlagFn; - setDefaultModels(providerName: ProviderName, modelNames: string[]): void; + setAutodetectedModels(providerName: ProviderName, modelNames: string[], logging: { enableProviderOnSuccess?: boolean, isPolling?: boolean, isInvisible?: boolean }): void; toggleModelHidden(providerName: ProviderName, modelName: string): void; addModel(providerName: ProviderName, modelName: string): void; deleteModel(providerName: ProviderName, modelName: string): boolean; @@ -69,7 +70,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 @@ -100,6 +101,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, ) { @@ -169,11 +171,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 }) } @@ -220,25 +222,45 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { - setDefaultModels(providerName: ProviderName, newDefaultModelNames: string[]) { + setAutodetectedModels(providerName: ProviderName, newDefaultModelNames: string[], logging: { enableProviderOnSuccess?: boolean, isPolling?: boolean, isInvisible?: boolean }) { + 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 +271,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 +284,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 35a20c35..acc81f02 100644 --- a/src/vs/platform/void/common/voidSettingsTypes.ts +++ b/src/vs/platform/void/common/voidSettingsTypes.ts @@ -1,8 +1,8 @@ -/*------------------------------------------------------------------------------------------ - * Copyright (c) 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the MIT License. See LICENSE.txt in the project root for more information. - *-----------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ @@ -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, + })) } @@ -158,7 +165,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 @@ -191,6 +198,7 @@ export type SettingName = keyof SettingsForProvider type DisplayInfoForProviderName = { title: string, + desc?: string, } export const displayInfoOfProviderName = (providerName: ProviderName): DisplayInfoForProviderName => { @@ -217,7 +225,7 @@ export const displayInfoOfProviderName = (providerName: ProviderName): DisplayIn } else if (providerName === 'openAICompatible') { return { - title: 'Other', + title: 'OpenAI-Compatible', } } else if (providerName === 'gemini') { @@ -277,7 +285,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, } } diff --git a/src/vs/platform/void/electron-main/llmMessage/anthropic.ts b/src/vs/platform/void/electron-main/llmMessage/anthropic.ts index 7ae164c0..04dcaa21 100644 --- a/src/vs/platform/void/electron-main/llmMessage/anthropic.ts +++ b/src/vs/platform/void/electron-main/llmMessage/anthropic.ts @@ -1,7 +1,7 @@ -/*------------------------------------------------------------------------------------------ - * Copyright (c) 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the MIT License. See LICENSE.txt in the project root for more information. - *-----------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ import Anthropic from '@anthropic-ai/sdk'; import { _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js'; diff --git a/src/vs/platform/void/electron-main/llmMessage/gemini.ts b/src/vs/platform/void/electron-main/llmMessage/gemini.ts index 2eda09cb..557d28c7 100644 --- a/src/vs/platform/void/electron-main/llmMessage/gemini.ts +++ b/src/vs/platform/void/electron-main/llmMessage/gemini.ts @@ -1,7 +1,7 @@ -/*------------------------------------------------------------------------------------------ - * Copyright (c) 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the MIT License. See LICENSE.txt in the project root for more information. - *-----------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ import { Content, GoogleGenerativeAI, GoogleGenerativeAIFetchError } from '@google/generative-ai'; import { _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js'; diff --git a/src/vs/platform/void/electron-main/llmMessage/greptile.ts b/src/vs/platform/void/electron-main/llmMessage/greptile.ts index f3c27632..f61f87af 100644 --- a/src/vs/platform/void/electron-main/llmMessage/greptile.ts +++ b/src/vs/platform/void/electron-main/llmMessage/greptile.ts @@ -1,7 +1,7 @@ -/*------------------------------------------------------------------------------------------ - * Copyright (c) 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the MIT License. See LICENSE.txt in the project root for more information. - *-----------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ // // Greptile // // https://docs.greptile.com/api-reference/query diff --git a/src/vs/platform/void/electron-main/llmMessage/groq.ts b/src/vs/platform/void/electron-main/llmMessage/groq.ts index 6a62e643..1050e25c 100644 --- a/src/vs/platform/void/electron-main/llmMessage/groq.ts +++ b/src/vs/platform/void/electron-main/llmMessage/groq.ts @@ -1,7 +1,7 @@ -/*------------------------------------------------------------------------------------------ - * Copyright (c) 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the MIT License. See LICENSE.txt in the project root for more information. - *-----------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ import Groq from 'groq-sdk'; import { _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js'; diff --git a/src/vs/platform/void/electron-main/llmMessage/ollama.ts b/src/vs/platform/void/electron-main/llmMessage/ollama.ts index d48c0dca..95792700 100644 --- a/src/vs/platform/void/electron-main/llmMessage/ollama.ts +++ b/src/vs/platform/void/electron-main/llmMessage/ollama.ts @@ -1,7 +1,7 @@ -/*------------------------------------------------------------------------------------------ - * Copyright (c) 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the MIT License. See LICENSE.txt in the project root for more information. - *-----------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ import { Ollama } from 'ollama'; import { _InternalModelListFnType, _InternalSendLLMMessageFnType, OllamaModelResponse } from '../../common/llmMessageTypes.js'; diff --git a/src/vs/platform/void/electron-main/llmMessage/openai.ts b/src/vs/platform/void/electron-main/llmMessage/openai.ts index 90f9bf97..391764cb 100644 --- a/src/vs/platform/void/electron-main/llmMessage/openai.ts +++ b/src/vs/platform/void/electron-main/llmMessage/openai.ts @@ -1,7 +1,7 @@ -/*------------------------------------------------------------------------------------------ - * Copyright (c) 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the MIT License. See LICENSE.txt in the project root for more information. - *-----------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ import OpenAI from 'openai'; import { _InternalModelListFnType, _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js'; diff --git a/src/vs/platform/void/electron-main/llmMessage/sendLLMMessage.ts b/src/vs/platform/void/electron-main/llmMessage/sendLLMMessage.ts index 0d236e50..95716e81 100644 --- a/src/vs/platform/void/electron-main/llmMessage/sendLLMMessage.ts +++ b/src/vs/platform/void/electron-main/llmMessage/sendLLMMessage.ts @@ -1,7 +1,7 @@ -/*------------------------------------------------------------------------------------------ - * Copyright (c) 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the MIT License. See LICENSE.txt in the project root for more information. - *-----------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ import { LLMMMessageParams, OnText, OnFinalMessage, OnError } from '../../common/llmMessageTypes.js'; import { IMetricsService } from '../../common/metricsService.js'; @@ -35,6 +35,7 @@ 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', diff --git a/src/vs/platform/void/electron-main/llmMessageChannel.ts b/src/vs/platform/void/electron-main/llmMessageChannel.ts index 0ad52534..2430fce2 100644 --- a/src/vs/platform/void/electron-main/llmMessageChannel.ts +++ b/src/vs/platform/void/electron-main/llmMessageChannel.ts @@ -1,7 +1,7 @@ -/*------------------------------------------------------------------------------------------ - * Copyright (c) 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the MIT License. See LICENSE.txt in the project root for more information. - *-----------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ // registered in app.ts // code convention is to make a service responsible for this stuff, and not a channel, but having fewer files is simpler... diff --git a/src/vs/platform/void/electron-main/metricsMainService.ts b/src/vs/platform/void/electron-main/metricsMainService.ts index f638d6cc..31ca1252 100644 --- a/src/vs/platform/void/electron-main/metricsMainService.ts +++ b/src/vs/platform/void/electron-main/metricsMainService.ts @@ -1,7 +1,7 @@ -/*------------------------------------------------------------------------------------------ - * Copyright (c) 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the MIT License. See LICENSE.txt in the project root for more information. - *-----------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ import { Disposable } from '../../../base/common/lifecycle.js'; import { ITelemetryService } from '../../telemetry/common/telemetry.js'; 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 e3defc0b..1400d429 100644 --- a/src/vs/workbench/contrib/void/browser/autocompleteService.ts +++ b/src/vs/workbench/contrib/void/browser/autocompleteService.ts @@ -1,7 +1,7 @@ -/*------------------------------------------------------------------------------------------ - * Copyright (c) 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the MIT License. See LICENSE.txt in the project root for more information. - *-----------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ import { Disposable } from '../../../../base/common/lifecycle.js'; import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js'; @@ -17,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,7 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ // newAutocompletion.abortRef = { current: () => { } } newAutocompletion.status = 'finished' // newAutocompletion.promise = undefined - newAutocompletion.insertText = postprocessResult(extractCodeFromResult(fullText)) + newAutocompletion.insertText = postprocessResult(extractCodeFromRegular(fullText)) resolve(newAutocompletion.insertText) diff --git a/src/vs/workbench/contrib/void/browser/helperServices/consistentItemService.ts b/src/vs/workbench/contrib/void/browser/helperServices/consistentItemService.ts index bcaf12d7..f6787e4c 100644 --- a/src/vs/workbench/contrib/void/browser/helperServices/consistentItemService.ts +++ b/src/vs/workbench/contrib/void/browser/helperServices/consistentItemService.ts @@ -1,7 +1,7 @@ -/*------------------------------------------------------------------------------------------ - * Copyright (c) 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the MIT License. See LICENSE.txt in the project root for more information. - *-----------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ import { Disposable } from '../../../../../base/common/lifecycle.js'; import { URI } from '../../../../../base/common/uri.js'; 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 90003524..af883788 100644 --- a/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts +++ b/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts @@ -1,18 +1,163 @@ -/*------------------------------------------------------------------------------------------ - * Copyright (c) 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the MIT License. See LICENSE.txt in the project root for more information. - *-----------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ -export const extractCodeFromResult = (result: string) => { +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 + } + + +} + + + +export const extractCodeFromRegular = (text: 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() + return s } + + + + + +// Ollama has its own FIM, we should not use this if we use that +export const extractCodeFromFIM = ({ text, midTag }: { text: string, midTag: 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) + + console.log('ORIGIINAL CODE', text) + + pm.removeCodeBlock() + + console.log('D', pm.i, pm.j) + + + const foundMid = pm.removePrefix(`<${midTag}>`) + console.log('E', midTag, pm.i, pm.j) + + if (foundMid) { + pm.removeSuffix(``) + console.log('F', pm.i, pm.j) + + } + const s = pm.value() + return s + + + // // 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/findDiffs.ts b/src/vs/workbench/contrib/void/browser/helpers/findDiffs.ts index f89e04ed..c9235c14 100644 --- a/src/vs/workbench/contrib/void/browser/helpers/findDiffs.ts +++ b/src/vs/workbench/contrib/void/browser/helpers/findDiffs.ts @@ -1,7 +1,7 @@ -/*------------------------------------------------------------------------------------------ - * Copyright (c) 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the MIT License. See LICENSE.txt in the project root for more information. - *-----------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ import { diffLines } from '../react/out/diff/index.js' 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 b17f9bbf..00000000 --- a/src/vs/workbench/contrib/void/browser/helpers/getCmdKey.ts +++ /dev/null @@ -1,20 +0,0 @@ -/*------------------------------------------------------------------------------------------ - * Copyright (c) 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the MIT License. See LICENSE.txt in the project root for more information. - *-----------------------------------------------------------------------------------------*/ - -import { isMacintosh } from '../../../../../base/common/platform.js'; - -// import { OperatingSystem, OS } from '../../../../base/common/platform.js'; -// OS === OperatingSystem.Macintosh -export function getCmdKey(): string { - if (isMacintosh) { - return '⌘'; - } else { - return 'Ctrl'; - } -} - - - - diff --git a/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts b/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts index 013c6bb7..0422f5df 100644 --- a/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts +++ b/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts @@ -1,7 +1,7 @@ -/*------------------------------------------------------------------------------------------ - * Copyright (c) 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the MIT License. See LICENSE.txt in the project root for more information. - *-----------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * 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'; @@ -13,7 +13,7 @@ import { ICodeEditorService } from '../../../../editor/browser/services/codeEdit // import { throttle } from '../../../../base/common/decorators.js'; import { ComputedDiff, findDiffs } from './helpers/findDiffs.js'; import { EndOfLinePreference, IModelDecorationOptions, ITextModel } from '../../../../editor/common/model.js'; -import { IRange } from '../../../../editor/common/core/range.js'; +import { IRange, Range } from '../../../../editor/common/core/range.js'; import { registerColor } from '../../../../platform/theme/common/colorUtils.js'; import { Color, RGBA } from '../../../../base/common/color.js'; import { IModelService } from '../../../../editor/common/services/model.js'; @@ -27,20 +27,28 @@ 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 { InlineDecorationType } from '../../../../editor/common/viewModel.js'; +import { filenameToVscodeLanguage } from './helpers/detectLanguage.js'; +import { BaseEditorSimpleWorker } from '../../../../editor/common/services/editorSimpleWorker.js'; +import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js'; +import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js'; +import { localize2 } from '../../../../nls.js'; +import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; +import { isMacintosh } from '../../../../base/common/platform.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); @@ -117,7 +125,7 @@ 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; } @@ -172,6 +180,8 @@ export interface IInlineDiffsService { interruptStreaming(diffareaid: number): void; addCtrlKZone(opts: AddCtrlKOpts): number | undefined; removeCtrlKZone(opts: { diffareaid: number }): void; + + testDiffs(): void; } export const IInlineDiffsService = createDecorator('inlineDiffAreasService'); @@ -197,6 +207,8 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { @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(); @@ -243,7 +255,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) @@ -329,16 +341,18 @@ 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 itemId = this._consistentEditorItemService.addToEditor(editor, () => { const domNode = document.createElement('div'); domNode.style.zIndex = '1' + domNode.style.height = 'auto' const viewZone: IViewZone = { afterLineNumber: ctrlKZone.startLine - 1, domNode: domNode, - heightInPx: 52, + // heightInPx: 80, suppressMouseDown: false, + showInHiddenAreas: true, }; viewZone_ = viewZone @@ -347,29 +361,32 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { zoneId = accessor.addZone(viewZone) }) + // 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 + + 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 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 +399,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { }) return { - inputBoxRef, + textAreaRef, refresh: () => editor.changeViewZones(accessor => { if (zoneId && viewZone_) { viewZone_.afterLineNumber = ctrlKZone.startLine - 1 @@ -403,7 +420,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() @@ -446,7 +463,13 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { 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 result = renderLines(source, renderOptions, [ + { // add dummy so it doesn't highlight in red + range: Range.lift({ startLineNumber: 1, startColumn: 1, endLineNumber: Number.MAX_SAFE_INTEGER, endColumn: Number.MAX_SAFE_INTEGER }), + inlineClassName: '', + type: InlineDecorationType.Regular + } + ], domNode); const viewZone: IViewZone = { // afterLineNumber: computedDiff.startLine - 1, @@ -456,6 +479,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { domNode: domNode, marginDomNode: document.createElement('div'), // displayed to left suppressMouseDown: true, + showInHiddenAreas: true, }; let zoneId: string | null = null @@ -476,8 +500,14 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { fn: (editor) => { const buttonsWidget = new AcceptRejectWidget({ editor, - onAccept: () => { this.acceptDiff({ diffid }) }, - onReject: () => { this.rejectDiff({ diffid }) }, + onAccept: () => { + this.acceptDiff({ diffid }) + this._metricsService.capture('Accept Diff', { batch: false }) + }, + onReject: () => { + this.rejectDiff({ diffid }) + this._metricsService.capture('Reject Diff', { batch: false }) + }, diffid: diffid.toString(), startLine: diff.startLine, }) @@ -502,8 +532,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,13 +548,22 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { } weAreWriting = false - private _writeText(uri: URI, text: string, range: IRange, { shouldRealignDiffAreas }: { shouldRealignDiffAreas: boolean }) { + worker = new BaseEditorSimpleWorker() + 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) return - this.weAreWriting = true - model.applyEdits([{ range, text }]) // applies edits without adding them to undo/redo stack - this.weAreWriting = false + // minimal edits so not so flashy + // __TODO__ THIS IS NOT INSIDE A WORKER, SO IT MIGHT BE SLOW, we should instead just do an optimal write ourselves + const edits = this.worker.$Void_computeMoreMinimalEdits(uri.toString(), [{ range, text }], false) + + if (edits) { + this.weAreWriting = true + model.applyEdits(edits) + this.weAreWriting = false + } this._onInternalChangeContent(uri, { shouldRealign: shouldRealignDiffAreas && { newText: text, oldRange: range } }) } @@ -564,7 +604,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 @@ -596,13 +635,17 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { // 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 hasWriteChange = this._readURI(uri) !== entireModelCode // a heuristic check + if (hasWriteChange) + this._writeText(uri, entireModelCode, + { startColumn: 1, startLineNumber: 1, endLineNumber: numLines, endColumn: Number.MAX_SAFE_INTEGER }, + { shouldRealignDiffAreas: false } + ) + else { + // restore all the decorations + this._refreshStylesAndDiffsInURI(uri) + } } const beforeSnapshot: HistorySnapshot = getCurrentSnapshot() @@ -801,7 +844,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { // @throttle(100) - private _writeDiffZoneLLMText(diffZone: DiffZone, llmText: string, latestCurrentFileEnd: IPosition, newPosition: IPosition) { + private _writeDiffZoneLLMText(diffZone: DiffZone, llmText: string) { // ----------- 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 +865,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 @@ -841,7 +885,8 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { 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}` + // oriignalCode[1 + line...Infinity]. Must make sure 1 + line < originalCode.length. This is another way to check: + const newCode = (newCodeTop && oldFileBottom) ? `${newCodeTop}\n${oldFileBottom}` : (oldFileBottom || newCodeTop) this._writeText(uri, newCode, { startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER, }, // 1-indexed @@ -914,18 +959,17 @@ 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) { + (overlappingCtrlKZone as CtrlKZone)._mountInfo?.textAreaRef.current?.focus() + return } + const overlappingDiffZone = this._findOverlappingDiffArea({ startLine, endLine, uri, filter: (diffArea) => diffArea.type === 'DiffZone' }) + if (overlappingDiffZone) + return + const { onFinishEdit } = this._addToHistory(uri) const adding: Omit = { @@ -944,6 +988,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 +1011,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 +1041,8 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { if (!uri_) return uri = uri_ - // __TODO__ reject all diffs in the diff area + // reject all diffareas on this URI, adding to history (there can't possibly be overlap after this) + this.removeDiffAreas({ uri, behavior: 'reject' }) // in ctrl+L the start and end lines are the full document const numLines = this._getNumLines(uri) @@ -991,18 +1050,6 @@ 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') { @@ -1013,12 +1060,11 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { 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}`) @@ -1036,12 +1082,9 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { 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__ ctrl+K should use Ollama's FIM method. + const ollamaStyleFIM = false + const modelFimTags = defaultFimTags const adding: Omit = { type: 'DiffZone', @@ -1062,7 +1105,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { 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, }, @@ -1071,10 +1114,11 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { } 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) + const language = filenameToVscodeLanguage(uri.fsPath) ?? '' + const userContent = ctrlKStream_prompt({ selection: originalCode, userMessage, prefix, suffix, ollamaStyleFIM, fimTags: modelFimTags, language }) + // console.log('PREFIX:\n', prefix) + // console.log('SUFFIX:\n', suffix) + // console.log('USER CONTENT:\n', userContent) messages = [ // TODO include more context too (LSP, file history, etc) { role: 'system', content: ctrlKStream_systemMessage, }, @@ -1083,47 +1127,63 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { } 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, } - if (featureName === 'Ctrl+K') { const ctrlKZone = this.diffAreaOfId[opts.diffareaid] as CtrlKZone 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) => { + if (featureName === 'Ctrl+K') { + if (ollamaStyleFIM) return fullText + return extractCodeFromFIM({ text: fullText, midTag: modelFimTags.midTag }) + } + else if (featureName === 'Ctrl+L') { + return extractCodeFromRegular(fullText) + } + throw 1 + } + streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({ featureName, logging: { loggingName: `startApplying - ${featureName}` }, messages, onText: ({ newText, fullText }) => { - this._writeDiffZoneLLMText(diffZone, fullText, latestCurrentFileEnd, latestOriginalFileStart) + this._writeDiffZoneLLMText(diffZone, extractText(fullText)) 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, + this._writeText(uri, extractText(fullText), { 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 }, @@ -1134,6 +1194,45 @@ 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 + } + private _stopIfStreaming(diffZone: DiffZone) { @@ -1148,6 +1247,9 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { } + _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 +1260,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 +1269,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, behavior }: { uri: URI, 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') { + this._deleteCtrlKZone(diffArea) + } + } + + this._refreshStylesAndDiffsInURI(uri) + onFinishEdit() + } + + + // called on void.acceptDiff public async acceptDiff({ diffid }: { diffid: number }) { @@ -1278,8 +1429,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 +1455,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 @@ -1382,10 +1540,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 +1574,19 @@ class AcceptRejectWidget extends Widget implements IOverlayWidget { } + + +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 225925ad..e5e9793e 100644 --- a/src/vs/workbench/contrib/void/browser/media/void.css +++ b/src/vs/workbench/contrib/void/browser/media/void.css @@ -1,7 +1,7 @@ -/*------------------------------------------------------------------------------------------ - * Copyright (c) 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the MIT License. See LICENSE.txt in the project root for more information. - *-----------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ .monaco-editor .void-sweepIdxBG { background-color: var(--vscode-void-sweepIdxBG); @@ -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 cb278cc1..ca692569 100644 --- a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts @@ -1,9 +1,11 @@ -/*------------------------------------------------------------------------------------------ - * Copyright (c) 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the MIT License. See LICENSE.txt in the project root for more information. - *-----------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ +import { URI } from '../../../../../base/common/uri.js'; +import { filenameToVscodeLanguage } from '../helpers/detectLanguage.js'; import { CodeSelection } from '../threadHistoryService.js'; export const chat_systemMessage = `\ @@ -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,13 +329,28 @@ 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' +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, ollamaStyleFIM, language }: + { selection: string, prefix: string, suffix: string, userMessage: string, ollamaStyleFIM: boolean, fimTags: FimTagsType, language: string }) => { + const { preTag, sufTag, midTag } = fimTags + + + + if (ollamaStyleFIM) { + // const preTag = 'PRE' + // const sufTag = 'SUF' + // const midTag = 'MID' return `\ <${preTag}> /* Original Selection: @@ -341,32 +361,36 @@ ${prefix} <${sufTag}>${suffix} <${midTag}>` } - // prompt the model on how to do FIM + // prompt the model artifically on how to do FIM else { - const preTag = 'PRE' - const sufTag = 'SUF' - const midTag = 'MID' + // const preTag = 'BEFORE' + // const sufTag = 'AFTER' + // const midTag = 'SELECTION' return `\ -Here is the user's original selection: -\`\`\` +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<${preTag}/>. +Note also that the SELECTION has code that comes after it. This code is indicated with <${sufTag}>...after<${sufTag}/>. -Complete the following: +Instructions: +1. Your OUTPUT should be a SINGLE PIECE OF CODE of the form <${midTag}>...new_selection<${midTag}/>. Do not give any explanation before or after this. ONLY output this format, nothing more. +2. You may ONLY CHANGE the original SELECTION, and NOT the content in the <${preTag}>...<${preTag}/> or <${sufTag}>...<${sufTag}/> tags. +3. Make sure all brackets in the new selection are balanced the same as in the original selection. +4. Be careful not to duplicate or remove variables, comments, or other syntax by mistake. + +Given the code: <${preTag}>${prefix} <${sufTag}>${suffix} -<${midTag}>` + +Return only the completion block of code (of the form \`\`\` ${language}\n <${midTag}>...new_selection<${midTag}/>\`\`\`):` } }; diff --git a/src/vs/workbench/contrib/void/browser/quickEditActions.ts b/src/vs/workbench/contrib/void/browser/quickEditActions.ts index 754b939f..a4b8a16c 100644 --- a/src/vs/workbench/contrib/void/browser/quickEditActions.ts +++ b/src/vs/workbench/contrib/void/browser/quickEditActions.ts @@ -1,7 +1,7 @@ -/*------------------------------------------------------------------------------------------ - * Copyright (c) 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the MIT License. See LICENSE.txt in the project root for more information. - *-----------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js'; @@ -10,14 +10,15 @@ 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; + textAreaRef: (ref: HTMLTextAreaElement | null) => void; onChangeHeight: (height: number) => void; - onUserUpdateText: (text: string) => void; + onChangeText: (text: string) => void; initText: string | null; } @@ -30,7 +31,6 @@ export type QuickEdit = { } -export const VOID_CTRL_K_ACTION_ID = 'void.ctrlKAction' registerAction2(class extends Action2 { constructor( ) { @@ -48,13 +48,13 @@ 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; diff --git a/src/vs/workbench/contrib/void/browser/quickEditStateService.ts b/src/vs/workbench/contrib/void/browser/quickEditStateService.ts index 227e7e40..62f3823b 100644 --- a/src/vs/workbench/contrib/void/browser/quickEditStateService.ts +++ b/src/vs/workbench/contrib/void/browser/quickEditStateService.ts @@ -1,7 +1,7 @@ -/*------------------------------------------------------------------------------------------ - * Copyright (c) 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the MIT License. See LICENSE.txt in the project root for more information. - *-----------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ import { Emitter, Event } from '../../../../base/common/event.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; diff --git a/src/vs/workbench/contrib/void/browser/react/build.js b/src/vs/workbench/contrib/void/browser/react/build.js index 4360e859..436d10ce 100755 --- a/src/vs/workbench/contrib/void/browser/react/build.js +++ b/src/vs/workbench/contrib/void/browser/react/build.js @@ -1,7 +1,7 @@ -/*------------------------------------------------------------------------------------------ - * Copyright (c) 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the MIT License. See LICENSE.txt in the project root for more information. - *-----------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ import { spawn, execSync } from 'child_process'; // Added lines below @@ -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/index.tsx b/src/vs/workbench/contrib/void/browser/react/src/ctrl-k-tsx/index.tsx deleted file mode 100644 index 1b526325..00000000 --- a/src/vs/workbench/contrib/void/browser/react/src/ctrl-k-tsx/index.tsx +++ /dev/null @@ -1,12 +0,0 @@ -/*------------------------------------------------------------------------------------------ - * Copyright (c) 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the MIT License. See LICENSE.txt in the project root for more information. - *-----------------------------------------------------------------------------------------*/ - -import { mountFnGenerator } from '../util/mountFnGenerator.js' -import { CtrlK } from './CtrlK.js' - - -export const mountCtrlK = mountFnGenerator(CtrlK) - - diff --git a/src/vs/workbench/contrib/void/browser/react/src/diff/index.tsx b/src/vs/workbench/contrib/void/browser/react/src/diff/index.tsx index b308f819..31fee155 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/diff/index.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/diff/index.tsx @@ -1,7 +1,7 @@ -/*------------------------------------------------------------------------------------------ - * Copyright (c) 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the MIT License. See LICENSE.txt in the project root for more information. - *-----------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ import { diffLines, Change } from 'diff'; 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 50237589..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 @@ -1,87 +1,32 @@ -/*------------------------------------------------------------------------------------------ - * Copyright (c) 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the MIT License. See LICENSE.txt in the project root for more information. - *-----------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * 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 54f78df3..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 @@ -1,12 +1,13 @@ -/*------------------------------------------------------------------------------------------ - * Copyright (c) 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the MIT License. See LICENSE.txt in the project root for more information. - *-----------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ import React, { JSX, useCallback, useEffect, useState } from 'react' import { marked, MarkedToken, Token } from 'marked' import { BlockCode } from './BlockCode.js' import { 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 54% 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 f575e6cf..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 @@ -1,21 +1,21 @@ -/*------------------------------------------------------------------------------------------ - * Copyright (c) 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the MIT License. See LICENSE.txt in the project root for more information. - *-----------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ -import { useEffect, useState } from 'react' +import 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/ctrl-k-tsx/CtrlKChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx similarity index 52% rename from src/vs/workbench/contrib/void/browser/react/src/ctrl-k-tsx/CtrlKChat.tsx rename to src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx index 8724312a..755c3dec 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/ctrl-k-tsx/CtrlKChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx @@ -1,30 +1,33 @@ -/*------------------------------------------------------------------------------------------ - * Copyright (c) 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the MIT License. See LICENSE.txt in the project root for more information. - *-----------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * 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 { TextAreaFns, VoidInputBox2 } from '../util/inputs.js'; import { QuickEditPropsType } from '../../../quickEditActions.js'; -import { ButtonStop, ButtonSubmit } from '../sidebar-tsx/SidebarChat.js'; +import { ButtonStop, ButtonSubmit, IconX } from '../sidebar-tsx/SidebarChat.js'; import { ModelDropdown } from '../void-settings-tsx/ModelDropdown.js'; -import { X } from 'lucide-react'; +import { VOID_CTRL_K_ACTION_ID } from '../../../actionIDs.js'; -export const CtrlKChat = ({ diffareaid, onGetInputBox, onUserUpdateText, onChangeHeight, initText }: QuickEditPropsType) => { +export const QuickEditChat = ({ + diffareaid, + onChangeHeight, + onChangeText: onChangeText_, + textAreaRef: textAreaRef_, + initText +}: QuickEditPropsType) => { const accessor = useAccessor() const inlineDiffsService = accessor.get('IInlineDiffsService') const sizerRef = useRef(null) - const inputBoxRef: React.MutableRefObject = 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) => { @@ -32,77 +35,58 @@ export const CtrlKChat = ({ diffareaid, onGetInputBox, onUserUpdateText, onChang 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 [instructionsAreEmpty, setInstructionsAreEmpty] = useState(!(initText ?? '')) // the user's instructions + const isDisabled = instructionsAreEmpty const currentlyStreamingIdRef = useRef(undefined) const [isStreaming, setIsStreaming] = useState(false) const onSubmit = useCallback((e: FormEvent) => { + if (isDisabled) return if (currentlyStreamingIdRef.current !== undefined) return - inputBoxRef.current?.disable() + textAreaFnsRef.current?.disable() + const instructions = textAreaRef.current?.value ?? '' currentlyStreamingIdRef.current = inlineDiffsService.startApplying({ featureName: 'Ctrl+K', diffareaid: diffareaid, userMessage: instructions, }) setIsStreaming(true) - }, [inlineDiffsService, diffareaid, instructions]) + }, [isDisabled, inlineDiffsService, diffareaid]) const onInterrupt = useCallback(() => { if (currentlyStreamingIdRef.current !== undefined) inlineDiffsService.interruptStreaming(currentlyStreamingIdRef.current) - inputBoxRef.current?.enable() + textAreaFnsRef.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]) + const onX = useCallback(() => { + inlineDiffsService.removeCtrlKZone({ diffareaid }) + }, [inlineDiffsService, diffareaid]) + + + const keybindingString = accessor.get('IKeybindingService').lookupKeybinding(VOID_CTRL_K_ACTION_ID)?.getLabel() return
    { - if (e.key === 'Enter' && !e.shiftKey) { - onSubmit(e) - return - } - }} - onSubmit={(e) => { - if (isDisabled) { - // __TODO__ show disabled - return - } - console.log('submit!') - onSubmit(e) - }} + border border-void-border-3 focus-within:border-void-border-1 hover:border-void-border-1 + `} onClick={(e) => { - inputBoxRef.current?.focus() + textAreaRef.current?.focus() }} > @@ -115,30 +99,62 @@ export const CtrlKChat = ({ diffareaid, onGetInputBox, onUserUpdateText, onChang @@[&_div.monaco-inputbox]:!void-border-none @@[&_div.monaco-inputbox]:!void-outline-none`} > -
    -
    - { inlineDiffsService.removeCtrlKZone({ diffareaid }) }} - /> -
    +
    {/* input */}
    + @@[&_textarea]:!void-bg-transparent + @@[&_textarea]:!void-outline-none + @@[&_textarea]:!void-text-vscode-input-fg + @@[&_div.monaco-inputbox]:!void-outline-none + `} + > {/* text input */} - { - inputBoxRef.current = instance; - onGetInputBox(instance); - instance.focus() - }, [onGetInputBox])} + { + 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={`${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 */} +
    + +
    @@ -163,6 +179,7 @@ export const CtrlKChat = ({ diffareaid, onGetInputBox, onUserUpdateText, onChang : // submit button (up arrow) } diff --git a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/index.tsx b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/index.tsx new file mode 100644 index 00000000..301c0f24 --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/index.tsx @@ -0,0 +1,12 @@ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ + +import { mountFnGenerator } from '../util/mountFnGenerator.js' +import { QuickEdit } from './QuickEdit.js' + + +export const mountCtrlK = mountFnGenerator(QuickEdit) + + diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorBoundary.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorBoundary.tsx index b1985de9..2ccc54b2 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorBoundary.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorBoundary.tsx @@ -1,7 +1,7 @@ -/*------------------------------------------------------------------------------------------ - * Copyright (c) 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the MIT License. See LICENSE.txt in the project root for more information. - *-----------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ import React, { Component, ErrorInfo, ReactNode } from 'react'; import { ErrorDisplay } from './ErrorDisplay.js'; 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 6af510cd..a87e466e 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 @@ -1,10 +1,11 @@ -/*------------------------------------------------------------------------------------------ - * Copyright (c) 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the MIT License. See LICENSE.txt in the project root for more information. - *-----------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { AlertCircle, ChevronDown, ChevronUp, X } from 'lucide-react'; +import { errorDetails } from '../../../../../../../platform/void/common/llmMessageTypes.js'; export const ErrorDisplay = ({ @@ -20,54 +21,44 @@ export const ErrorDisplay = ({ }) => { const [isExpanded, setIsExpanded] = useState(false); - let details: string | null = null; - - if (fullError === null) { - details = null - } - else if (typeof fullError === 'object') { - details = JSON.stringify(fullError, null, 2) - } - else if (typeof fullError === 'string') { - details = null - } + const details = errorDetails(fullError) return (
    {/* Header */} -
    -
    - -
    -

    +
    +
    + +
    +

    {/* eg Error */} Error

    -

    +

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

    -
    +
    {details && ( - )} {showDismiss && onDismiss && ( - )}
    @@ -75,10 +66,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 61a85bd4..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 @@ -1,7 +1,7 @@ -/*------------------------------------------------------------------------------------------ - * Copyright (c) 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the MIT License. See LICENSE.txt in the project root for more information. - *-----------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ import React, { useEffect, useState } from 'react' import { mountFnGenerator } from '../util/mountFnGenerator.js' @@ -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 e7b8fe88..de424fe3 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 @@ -1,32 +1,37 @@ -/*------------------------------------------------------------------------------------------ - * Copyright (c) 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the MIT License. See LICENSE.txt in the project root for more information. - *-----------------------------------------------------------------------------------------*/ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ import React, { ButtonHTMLAttributes, FormEvent, FormHTMLAttributes, Fragment, useCallback, useEffect, useRef, useState } from 'react'; -import { useAccessor, useThreadsState } from '../util/services.js'; +import { useAccessor, useSidebarState, useThreadsState } from '../util/services.js'; import { ChatMessage, CodeSelection, CodeStagingSelection, IThreadHistoryService } from '../../../threadHistoryService.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) { @@ -269,97 +274,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,6 +407,9 @@ const ChatBubble = ({ chatMessage, isLoading }: { const role = chatMessage.role + // edit mode state + const [isEditMode, setIsEditMode] = useState(false) + if (!chatMessage.displayContent) return null @@ -376,30 +419,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 */} + {/*