mirror of
https://github.com/voideditor/void
synced 2026-05-23 09:28:23 +00:00
Merge branch 'model-selection' into feat-mistral-new
This commit is contained in:
commit
d067966b58
76 changed files with 3133 additions and 1231 deletions
32
CHANGELOG.md
Normal file
32
CHANGELOG.md
Normal file
|
|
@ -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.
|
||||
|
||||
|
|
@ -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).
|
||||
|
||||
--------------------
|
||||
|
||||
|
|
|
|||
214
LICENSE.txt
214
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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -92,21 +92,21 @@ async function main(buildDir?: string): Promise<void> {
|
|||
'-insert',
|
||||
'NSAppleEventsUsageDescription',
|
||||
'-string',
|
||||
'An application in Visual Studio Code wants to use AppleScript.',
|
||||
'An application in Void wants to use AppleScript.',
|
||||
`${infoPlistPath}`
|
||||
]);
|
||||
await spawn('plutil', [
|
||||
'-replace',
|
||||
'NSMicrophoneUsageDescription',
|
||||
'-string',
|
||||
'An application in Visual Studio Code wants to use the Microphone.',
|
||||
'An application in Void wants to use the Microphone.',
|
||||
`${infoPlistPath}`
|
||||
]);
|
||||
await spawn('plutil', [
|
||||
'-replace',
|
||||
'NSCameraUsageDescription',
|
||||
'-string',
|
||||
'An application in Visual Studio Code wants to use the Camera.',
|
||||
'An application in Void wants to use the Camera.',
|
||||
`${infoPlistPath}`
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }))
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -223,6 +223,11 @@ export class BaseEditorSimpleWorker implements IDisposable, IWorkerTextModelSync
|
|||
private static readonly _diffLimit = 100000;
|
||||
|
||||
public async $computeMoreMinimalEdits(modelUrl: string, edits: TextEdit[], pretty: boolean): Promise<TextEdit[]> {
|
||||
return this.$Void_computeMoreMinimalEdits(modelUrl, edits, pretty)
|
||||
}
|
||||
|
||||
// Void added this as non async
|
||||
public $Void_computeMoreMinimalEdits(modelUrl: string, edits: TextEdit[], pretty: boolean): TextEdit[] {
|
||||
const model = this._getModel(modelUrl);
|
||||
if (!model) {
|
||||
return edits;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -31,25 +31,29 @@ export class ServerTelemetryService extends TelemetryService implements IServerT
|
|||
}
|
||||
|
||||
override publicLog(eventName: string, data?: ITelemetryData) {
|
||||
if (this._injectedTelemetryLevel < TelemetryLevel.USAGE) {
|
||||
return;
|
||||
}
|
||||
return super.publicLog(eventName, data);
|
||||
// Void commented this out
|
||||
// if (this._injectedTelemetryLevel < TelemetryLevel.USAGE) {
|
||||
// return;
|
||||
// }
|
||||
// return super.publicLog(eventName, data);
|
||||
}
|
||||
|
||||
override publicLog2<E extends ClassifiedEvent<OmitMetadata<T>> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck<T, E>) {
|
||||
return this.publicLog(eventName, data as ITelemetryData | undefined);
|
||||
// Void commented this out
|
||||
// return this.publicLog(eventName, data as ITelemetryData | undefined);
|
||||
}
|
||||
|
||||
override publicLogError(errorEventName: string, data?: ITelemetryData) {
|
||||
if (this._injectedTelemetryLevel < TelemetryLevel.ERROR) {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
return super.publicLogError(errorEventName, data);
|
||||
// Void commented this out
|
||||
// if (this._injectedTelemetryLevel < TelemetryLevel.ERROR) {
|
||||
// return Promise.resolve(undefined);
|
||||
// }
|
||||
// return super.publicLogError(errorEventName, data);
|
||||
}
|
||||
|
||||
override publicLogError2<E extends ClassifiedEvent<OmitMetadata<T>> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck<T, E>) {
|
||||
return this.publicLogError(eventName, data as ITelemetryData | undefined);
|
||||
// Void commented this out
|
||||
// return this.publicLogError(eventName, data as ITelemetryData | undefined);
|
||||
}
|
||||
|
||||
async updateInjectedTelemetryLevel(telemetryLevel: TelemetryLevel): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { DisposableStore } from '../../../base/common/lifecycle.js';
|
||||
import { mixin } from '../../../base/common/objects.js';
|
||||
import { isWeb } from '../../../base/common/platform.js';
|
||||
import { escapeRegExpCharacters } from '../../../base/common/strings.js';
|
||||
import { localize } from '../../../nls.js';
|
||||
|
|
@ -15,7 +14,7 @@ import { IProductService } from '../../product/common/productService.js';
|
|||
import { Registry } from '../../registry/common/platform.js';
|
||||
import { ClassifiedEvent, IGDPRProperty, OmitMetadata, StrictPropertyCheck } from './gdprTypings.js';
|
||||
import { ITelemetryData, ITelemetryService, TelemetryConfiguration, TelemetryLevel, TELEMETRY_CRASH_REPORTER_SETTING_ID, TELEMETRY_OLD_SETTING_ID, TELEMETRY_SECTION_ID, TELEMETRY_SETTING_ID, ICommonProperties } from './telemetry.js';
|
||||
import { cleanData, getTelemetryLevel, ITelemetryAppender } from './telemetryUtils.js';
|
||||
import { getTelemetryLevel, ITelemetryAppender } from './telemetryUtils.js';
|
||||
|
||||
export interface ITelemetryServiceConfig {
|
||||
appenders: ITelemetryAppender[];
|
||||
|
|
@ -38,7 +37,7 @@ export class TelemetryService implements ITelemetryService {
|
|||
readonly firstSessionDate: string;
|
||||
readonly msftInternal: boolean | undefined;
|
||||
|
||||
private _appenders: ITelemetryAppender[];
|
||||
// private _appenders: ITelemetryAppender[];
|
||||
private _commonProperties: ICommonProperties;
|
||||
private _experimentProperties: { [name: string]: string } = {};
|
||||
private _piiPaths: string[];
|
||||
|
|
@ -53,7 +52,7 @@ export class TelemetryService implements ITelemetryService {
|
|||
@IConfigurationService private _configurationService: IConfigurationService,
|
||||
@IProductService private _productService: IProductService
|
||||
) {
|
||||
this._appenders = config.appenders;
|
||||
// this._appenders = config.appenders;
|
||||
this._commonProperties = config.commonProperties ?? Object.create(null);
|
||||
|
||||
this.sessionId = this._commonProperties['sessionID'] as string;
|
||||
|
|
@ -121,44 +120,47 @@ export class TelemetryService implements ITelemetryService {
|
|||
this._disposables.dispose();
|
||||
}
|
||||
|
||||
private _log(eventName: string, eventLevel: TelemetryLevel, data?: ITelemetryData) {
|
||||
// don't send events when the user is optout
|
||||
if (this._telemetryLevel < eventLevel) {
|
||||
return;
|
||||
}
|
||||
// Void commented this out
|
||||
// private _log(eventName: string, eventLevel: TelemetryLevel, data?: ITelemetryData) {
|
||||
// // don't send events when the user is optout
|
||||
// if (this._telemetryLevel < eventLevel) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// add experiment properties
|
||||
data = mixin(data, this._experimentProperties);
|
||||
// // add experiment properties
|
||||
// data = mixin(data, this._experimentProperties);
|
||||
|
||||
// remove all PII from data
|
||||
data = cleanData(data as Record<string, any>, this._cleanupPatterns);
|
||||
// // remove all PII from data
|
||||
// data = cleanData(data as Record<string, any>, this._cleanupPatterns);
|
||||
|
||||
// add common properties
|
||||
data = mixin(data, this._commonProperties);
|
||||
// // add common properties
|
||||
// data = mixin(data, this._commonProperties);
|
||||
|
||||
// Log to the appenders of sufficient level
|
||||
this._appenders.forEach(a => a.log(eventName, data));
|
||||
}
|
||||
// // Log to the appenders of sufficient level
|
||||
// this._appenders.forEach(a => a.log(eventName, data));
|
||||
// }
|
||||
|
||||
publicLog(eventName: string, data?: ITelemetryData) {
|
||||
this._log(eventName, TelemetryLevel.USAGE, data);
|
||||
// this._log(eventName, TelemetryLevel.USAGE, data);
|
||||
}
|
||||
|
||||
publicLog2<E extends ClassifiedEvent<OmitMetadata<T>> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck<T, E>) {
|
||||
this.publicLog(eventName, data as ITelemetryData);
|
||||
// this.publicLog(eventName, data as ITelemetryData);
|
||||
}
|
||||
|
||||
publicLogError(errorEventName: string, data?: ITelemetryData) {
|
||||
if (!this._sendErrorTelemetry) {
|
||||
return;
|
||||
}
|
||||
// Void commented this out
|
||||
// if (!this._sendErrorTelemetry) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// Send error event and anonymize paths
|
||||
this._log(errorEventName, TelemetryLevel.ERROR, data);
|
||||
// // Send error event and anonymize paths
|
||||
// this._log(errorEventName, TelemetryLevel.ERROR, data);
|
||||
}
|
||||
|
||||
publicLogError2<E extends ClassifiedEvent<OmitMetadata<T>> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck<T, E>) {
|
||||
this.publicLogError(eventName, data as ITelemetryData);
|
||||
// Void commented this out
|
||||
// this.publicLogError(eventName, data as ITelemetryData);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,10 @@ import { IRequestService } from '../../request/common/request.js';
|
|||
import { AvailableForDownload, DisablementReason, IUpdateService, State, StateType, UpdateType } from '../common/update.js';
|
||||
|
||||
export function createUpdateURL(platform: string, quality: string, productService: IProductService): string {
|
||||
return `${productService.updateUrl}/api/update/${platform}/${quality}/${productService.commit}`;
|
||||
// return `https://voideditor.dev/api/update/${platform}/stable`;
|
||||
// return `${productService.updateUrl}/api/update/${platform}/${quality}/${productService.commit}`;
|
||||
// https://github.com/VSCodium/update-api
|
||||
return `https://updates.voideditor.dev/api/update/${platform}/${quality}/${productService.commit}`;
|
||||
}
|
||||
|
||||
export type UpdateNotAvailableClassification = {
|
||||
|
|
@ -70,32 +73,38 @@ export abstract class AbstractUpdateService implements IUpdateService {
|
|||
*/
|
||||
protected async initialize(): Promise<void> {
|
||||
if (!this.environmentMainService.isBuilt) {
|
||||
console.log('is NOT built, canceling update service')
|
||||
this.setState(State.Disabled(DisablementReason.NotBuilt));
|
||||
return; // updates are never enabled when running out of sources
|
||||
}
|
||||
console.log('is built, continuing with update service')
|
||||
|
||||
if (this.environmentMainService.disableUpdates) {
|
||||
this.setState(State.Disabled(DisablementReason.DisabledByEnvironment));
|
||||
this.logService.info('update#ctor - updates are disabled by the environment');
|
||||
return;
|
||||
}
|
||||
// Void commented this
|
||||
// if (this.environmentMainService.disableUpdates) {
|
||||
// this.setState(State.Disabled(DisablementReason.DisabledByEnvironment));
|
||||
// this.logService.info('update#ctor - updates are disabled by the environment');
|
||||
// return;
|
||||
// }
|
||||
|
||||
if (!this.productService.updateUrl || !this.productService.commit) {
|
||||
this.setState(State.Disabled(DisablementReason.MissingConfiguration));
|
||||
this.logService.info('update#ctor - updates are disabled as there is no update URL');
|
||||
return;
|
||||
}
|
||||
// if (!this.productService.updateUrl || !this.productService.commit) {
|
||||
// this.setState(State.Disabled(DisablementReason.MissingConfiguration));
|
||||
// this.logService.info('update#ctor - updates are disabled as there is no update URL');
|
||||
// return;
|
||||
// }
|
||||
|
||||
// Void - for now, always update
|
||||
|
||||
const updateMode = 'default' //this.configurationService.getValue<'none' | 'manual' | 'start' | 'default'>('update.mode');
|
||||
|
||||
const updateMode = this.configurationService.getValue<'none' | 'manual' | 'start' | 'default'>('update.mode');
|
||||
const quality = this.getProductQuality(updateMode);
|
||||
|
||||
if (!quality) {
|
||||
this.setState(State.Disabled(DisablementReason.ManuallyDisabled));
|
||||
this.logService.info('update#ctor - updates are disabled by user preference');
|
||||
return;
|
||||
}
|
||||
|
||||
this.url = this.buildUpdateFeedUrl(quality);
|
||||
// const quality = 'stable'
|
||||
this.url = this.doBuildUpdateFeedUrl(quality);
|
||||
if (!this.url) {
|
||||
this.setState(State.Disabled(DisablementReason.InvalidConfiguration));
|
||||
this.logService.info('update#ctor - updates are disabled as the update URL is badly formed');
|
||||
|
|
@ -111,33 +120,30 @@ export abstract class AbstractUpdateService implements IUpdateService {
|
|||
|
||||
this.setState(State.Idle(this.getUpdateType()));
|
||||
|
||||
if (updateMode === 'manual') {
|
||||
this.logService.info('update#ctor - manual checks only; automatic updates are disabled by user preference');
|
||||
return;
|
||||
}
|
||||
// if (updateMode === 'manual') {
|
||||
// this.logService.info('update#ctor - manual checks only; automatic updates are disabled by user preference');
|
||||
// return;
|
||||
// }
|
||||
|
||||
if (updateMode === 'start') {
|
||||
this.logService.info('update#ctor - startup checks only; automatic updates are disabled by user preference');
|
||||
// if (updateMode === 'start') {
|
||||
// this.logService.info('update#ctor - startup checks only; automatic updates are disabled by user preference');
|
||||
|
||||
// Check for updates only once after 30 seconds
|
||||
setTimeout(() => this.checkForUpdates(false), 30 * 1000);
|
||||
} else {
|
||||
// Start checking for updates after 30 seconds
|
||||
this.scheduleCheckForUpdates(30 * 1000).then(undefined, err => this.logService.error(err));
|
||||
}
|
||||
// // Check for updates only once after 30 seconds
|
||||
// setTimeout(() => this.checkForUpdates(false), 30 * 1000);
|
||||
// } else {
|
||||
// Start checking for updates after 30 seconds
|
||||
this.scheduleCheckForUpdates(30 * 1000).then(undefined, err => this.logService.error(err));
|
||||
// }
|
||||
}
|
||||
|
||||
private getProductQuality(updateMode: string): string | undefined {
|
||||
return updateMode === 'none' ? undefined : this.productService.quality;
|
||||
}
|
||||
|
||||
private scheduleCheckForUpdates(delay = 60 * 60 * 1000): Promise<void> {
|
||||
return timeout(delay)
|
||||
.then(() => this.checkForUpdates(false))
|
||||
.then(() => {
|
||||
// Check again after 1 hour
|
||||
return this.scheduleCheckForUpdates(60 * 60 * 1000);
|
||||
});
|
||||
private async scheduleCheckForUpdates(delay = 60 * 60 * 1000): Promise<void> {
|
||||
await timeout(delay);
|
||||
await this.checkForUpdates(false);
|
||||
return await this.scheduleCheckForUpdates(60 * 60 * 1000);
|
||||
}
|
||||
|
||||
async checkForUpdates(explicit: boolean): Promise<void> {
|
||||
|
|
@ -160,6 +166,7 @@ export abstract class AbstractUpdateService implements IUpdateService {
|
|||
await this.doDownloadUpdate(this.state);
|
||||
}
|
||||
|
||||
// override implemented by windows and linux
|
||||
protected async doDownloadUpdate(state: AvailableForDownload): Promise<void> {
|
||||
// noop
|
||||
}
|
||||
|
|
@ -174,6 +181,7 @@ export abstract class AbstractUpdateService implements IUpdateService {
|
|||
await this.doApplyUpdate();
|
||||
}
|
||||
|
||||
// windows overrides this
|
||||
protected async doApplyUpdate(): Promise<void> {
|
||||
// noop
|
||||
}
|
||||
|
|
@ -236,6 +244,6 @@ export abstract class AbstractUpdateService implements IUpdateService {
|
|||
// noop
|
||||
}
|
||||
|
||||
protected abstract buildUpdateFeedUrl(quality: string): string | undefined;
|
||||
protected abstract doBuildUpdateFeedUrl(quality: string): string | undefined;
|
||||
protected abstract doCheckForUpdates(context: any): void;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 ----------
|
||||
|
||||
|
|
|
|||
|
|
@ -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<LLMMessageChannel>(mainProcessService.getChannel('void-channel-sendLLMMessage')); // lets you call it like a service
|
||||
// see llmMessageChannel.ts
|
||||
this.channel = this.mainProcessService.getChannel('void-channel-llmMessageService')
|
||||
|
||||
// .listen sets up an IPC channel and takes a few ms, so we set up listeners immediately and add hooks to them instead
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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<RefreshableProviderName, Refres
|
|||
|
||||
const refreshBasedOn: { [k in RefreshableProviderName]: (keyof SettingsOfProvider[k])[] } = {
|
||||
ollama: ['_enabled', 'endpoint'],
|
||||
openAICompatible: ['_enabled', 'endpoint', 'apiKey'],
|
||||
// openAICompatible: ['_enabled', 'endpoint', 'apiKey'],
|
||||
}
|
||||
const REFRESH_INTERVAL = 5_000
|
||||
// const COOLDOWN_TIMEOUT = 300
|
||||
|
|
@ -85,7 +85,7 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
|
|||
for (const providerName of refreshableProviderNames) {
|
||||
|
||||
const { _enabled: enabled } = this.voidSettingsService.state.settingsOfProvider[providerName]
|
||||
this.refreshModels(providerName, !enabled, { isPolling: true, isInternal: true })
|
||||
this.refreshModels(providerName, !enabled, { isPolling: true, isInvisible: true })
|
||||
|
||||
// every time providerName.enabled changes, refresh models too, like a useEffect
|
||||
let relevantVals = () => 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
|
||||
|
|
|
|||
|
|
@ -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 = <K extends FeatureName>(
|
|||
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<string, VoidModelInfo> = {}
|
||||
for (const em of existingModels) {
|
||||
existingModelsMap[em.modelName] = em
|
||||
for (const existingModel of existingModels) {
|
||||
existingModelsMap[existingModel.modelName] = existingModel
|
||||
}
|
||||
|
||||
return modelNames.map((modelName, i) => ({ modelName, isDefault, isAutodetected, isHidden: !!existingModelsMap[modelName]?.isHidden, }))
|
||||
return modelNames.map((modelName, i) => ({
|
||||
modelName,
|
||||
isDefault: true,
|
||||
isAutodetected: isAutodetected,
|
||||
isHidden: !!existingModelsMap[modelName]?.isHidden,
|
||||
}))
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -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<typeof defaultProviderSettings[ProviderName]>
|
||||
|
|
@ -191,6 +198,7 @@ export type SettingName = keyof SettingsForProvider<ProviderName>
|
|||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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...
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
6
src/vs/workbench/contrib/void/browser/actionIDs.ts
Normal file
6
src/vs/workbench/contrib/void/browser/actionIDs.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
// Normally you'd want to put these exports in the files that register them, but if you do that you'll get an import order error if you import them in certain cases.
|
||||
// (importing them runs the whole file to get the ID, causing an import error). I guess it's best practice to separate out IDs, pretty annoying...
|
||||
|
||||
export const VOID_CTRL_L_ACTION_ID = 'void.ctrlLAction'
|
||||
|
||||
export const VOID_CTRL_K_ACTION_ID = 'void.ctrlKAction'
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
170
src/vs/workbench/contrib/void/browser/helpers/detectLanguage.ts
Normal file
170
src/vs/workbench/contrib/void/browser/helpers/detectLanguage.ts
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
|
||||
// eg "bash" -> "shell"
|
||||
export const nameToVscodeLanguage: { [key: string]: string } = {
|
||||
// Web Technologies
|
||||
'html': 'html',
|
||||
'css': 'css',
|
||||
'scss': 'scss',
|
||||
'sass': 'scss',
|
||||
'less': 'less',
|
||||
'javascript': 'typescript',
|
||||
'js': 'typescript', // use more general renderer
|
||||
'jsx': 'typescript',
|
||||
'typescript': 'typescript',
|
||||
'ts': 'typescript',
|
||||
'tsx': 'typescript',
|
||||
'json': 'json',
|
||||
'jsonc': 'json',
|
||||
|
||||
// Programming Languages
|
||||
'python': 'python',
|
||||
'py': 'python',
|
||||
'java': 'java',
|
||||
'cpp': 'cpp',
|
||||
'c++': 'cpp',
|
||||
'c': 'c',
|
||||
'csharp': 'csharp',
|
||||
'cs': 'csharp',
|
||||
'c#': 'csharp',
|
||||
'go': 'go',
|
||||
'golang': 'go',
|
||||
'rust': 'rust',
|
||||
'rs': 'rust',
|
||||
'ruby': 'ruby',
|
||||
'rb': 'ruby',
|
||||
'php': 'php',
|
||||
'shell': 'shell',
|
||||
'bash': 'shell',
|
||||
'sh': 'shell',
|
||||
'zsh': 'shell',
|
||||
|
||||
// Markup and Config
|
||||
'markdown': 'markdown',
|
||||
'md': 'markdown',
|
||||
'xml': 'xml',
|
||||
'svg': 'xml',
|
||||
'yaml': 'yaml',
|
||||
'yml': 'yaml',
|
||||
'ini': 'ini',
|
||||
'toml': 'ini',
|
||||
|
||||
// Database and Query Languages
|
||||
'sql': 'sql',
|
||||
'mysql': 'sql',
|
||||
'postgresql': 'sql',
|
||||
'graphql': 'graphql',
|
||||
'gql': 'graphql',
|
||||
|
||||
// Others
|
||||
'dockerfile': 'dockerfile',
|
||||
'docker': 'dockerfile',
|
||||
'makefile': 'makefile',
|
||||
'plaintext': 'plaintext',
|
||||
'text': 'plaintext'
|
||||
};
|
||||
|
||||
|
||||
|
||||
// eg ".ts" -> "typescript"
|
||||
const fileExtensionToVscodeLanguage: { [key: string]: string } = {
|
||||
// Web
|
||||
'html': 'html',
|
||||
'htm': 'html',
|
||||
'css': 'css',
|
||||
'scss': 'scss',
|
||||
'less': 'less',
|
||||
'js': 'javascript',
|
||||
'jsx': 'javascript',
|
||||
'ts': 'typescript',
|
||||
'tsx': 'typescript',
|
||||
'json': 'json',
|
||||
'jsonc': 'json',
|
||||
|
||||
// Programming Languages
|
||||
'py': 'python',
|
||||
'java': 'java',
|
||||
'cpp': 'cpp',
|
||||
'cc': 'cpp',
|
||||
'c': 'c',
|
||||
'h': 'cpp',
|
||||
'hpp': 'cpp',
|
||||
'cs': 'csharp',
|
||||
'go': 'go',
|
||||
'rs': 'rust',
|
||||
'rb': 'ruby',
|
||||
'php': 'php',
|
||||
'sh': 'shell',
|
||||
'bash': 'shell',
|
||||
'zsh': 'shell',
|
||||
|
||||
// Markup/Config
|
||||
'md': 'markdown',
|
||||
'markdown': 'markdown',
|
||||
'xml': 'xml',
|
||||
'svg': 'xml',
|
||||
'yaml': 'yaml',
|
||||
'yml': 'yaml',
|
||||
'ini': 'ini',
|
||||
'toml': 'ini',
|
||||
|
||||
// Other
|
||||
'sql': 'sql',
|
||||
'graphql': 'graphql',
|
||||
'gql': 'graphql',
|
||||
'dockerfile': 'dockerfile',
|
||||
'docker': 'dockerfile',
|
||||
'mk': 'makefile',
|
||||
|
||||
// Config Files and Dot Files
|
||||
'npmrc': 'ini',
|
||||
'env': 'ini',
|
||||
'gitignore': 'ignore',
|
||||
'dockerignore': 'ignore',
|
||||
'eslintrc': 'json',
|
||||
'babelrc': 'json',
|
||||
'prettierrc': 'json',
|
||||
'stylelintrc': 'json',
|
||||
'editorconfig': 'ini',
|
||||
'htaccess': 'apacheconf',
|
||||
'conf': 'ini',
|
||||
'config': 'ini',
|
||||
|
||||
// Package Files
|
||||
'package': 'json',
|
||||
'package-lock': 'json',
|
||||
'gemfile': 'ruby',
|
||||
'podfile': 'ruby',
|
||||
'rakefile': 'ruby',
|
||||
|
||||
// Build Systems
|
||||
'cmake': 'cmake',
|
||||
'makefile': 'makefile',
|
||||
'gradle': 'groovy',
|
||||
|
||||
// Shell Scripts
|
||||
'bashrc': 'shell',
|
||||
'zshrc': 'shell',
|
||||
'fish': 'shell',
|
||||
|
||||
// Version Control
|
||||
'gitconfig': 'ini',
|
||||
'hgrc': 'ini',
|
||||
'svnconfig': 'ini',
|
||||
|
||||
// Web Server
|
||||
'nginx': 'nginx',
|
||||
|
||||
// Misc Config
|
||||
'properties': 'properties',
|
||||
'cfg': 'ini',
|
||||
'reg': 'ini'
|
||||
};
|
||||
|
||||
|
||||
export function filenameToVscodeLanguage(filename: string): string | undefined {
|
||||
|
||||
const ext = filename.toLowerCase().split('.').pop();
|
||||
if (!ext) return undefined;
|
||||
|
||||
return fileExtensionToVscodeLanguage[ext];
|
||||
}
|
||||
|
|
@ -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 = <PRE/>, the string is <PRE>hi<P
|
||||
const s = this.value()
|
||||
// for every possible prefix of `suffix`, check if string ends with it
|
||||
for (let len = Math.min(s.length, suffix.length); len >= 1; len -= 1) {
|
||||
if (s.endsWith(suffix.substring(0, len))) { // the end of the string equals a prefix
|
||||
this.j -= len
|
||||
return len === suffix.length
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
// removeSuffix = (suffix: string): boolean => {
|
||||
// let offset = 0
|
||||
|
||||
// while (this.j >= Math.max(this.i, 0)) {
|
||||
// if (this.originalS.charAt(this.j) !== suffix.charAt(suffix.length - 1 - offset))
|
||||
// break
|
||||
// offset += 1
|
||||
// this.j -= 1
|
||||
// }
|
||||
// return offset === suffix.length
|
||||
// }
|
||||
|
||||
removeFromStartUntil = (until: string, alsoRemoveUntilStr: boolean) => {
|
||||
const index = this.originalS.indexOf(until, this.i)
|
||||
|
||||
if (index === -1) {
|
||||
this.i = this.j + 1
|
||||
return false
|
||||
}
|
||||
console.log('index', index, until.length)
|
||||
|
||||
if (alsoRemoveUntilStr)
|
||||
this.i = index + until.length
|
||||
else
|
||||
this.i = index
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
removeCodeBlock = () => {
|
||||
const pm = this
|
||||
const foundCodeBlock = pm.removePrefix('```')
|
||||
if (!foundCodeBlock) return false
|
||||
|
||||
pm.removeFromStartUntil('\n', true) // language
|
||||
|
||||
const foundCodeBlockEnd = pm.removeSuffix('```')
|
||||
if (!foundCodeBlockEnd) return false
|
||||
|
||||
pm.removeSuffix('\n')
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const extractCodeFromRegular = (text: string): string => {
|
||||
// Match either:
|
||||
// 1. ```language\n<code>```
|
||||
// 2. ```<code>```
|
||||
const match = result.match(/```(?:\w+\n)?([\s\S]*?)```|```([\s\S]*?)```/);
|
||||
|
||||
if (!match) {
|
||||
return result;
|
||||
}
|
||||
const pm = new SurroundingsRemover(text)
|
||||
|
||||
// Return whichever group matched (non-empty)
|
||||
return match[1] ?? match[2] ?? result;
|
||||
pm.removeCodeBlock()
|
||||
|
||||
const s = pm.value()
|
||||
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 <MID> tag]
|
||||
(match the stuff between mid tags)
|
||||
[optional <MID/> 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(`</${midTag}>`)
|
||||
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]*?)?<MID>([\s\S]*?)(?:<\/MID>|`{1,3}|$)/;
|
||||
// const regex = new RegExp(
|
||||
// `[\\s\\S]*?(?:\`{1,3}\\s*([a-zA-Z_]+[\\w]*)?[\\s\\S]*?)?<${midTag}>([\\s\\S]*?)(?:</${midTag}>|\`{1,3}|$)`,
|
||||
// ''
|
||||
// );
|
||||
// const match = text.match(regex);
|
||||
// if (match) {
|
||||
// const [_, languageName, codeBetweenMidTags] = match;
|
||||
// return [languageName, codeBetweenMidTags] as const
|
||||
|
||||
// } else {
|
||||
// return [undefined, extractCodeFromRegular(text)] as const
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/*------------------------------------------------------------------------------------------
|
||||
* Copyright (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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -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<IInlineDiffsService>('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<CtrlKZone, 'diffareaid'> = {
|
||||
|
|
@ -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<DiffZone, 'diffareaid'> = {
|
||||
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<DiffZone, 'diffareaid'> = {
|
||||
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<void> {
|
||||
const inlineDiffsService = accessor.get(IInlineDiffsService)
|
||||
inlineDiffsService.testDiffs()
|
||||
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
@@ ... @@
|
||||
-<div className={styles.sidebar}>
|
||||
-<ul>
|
||||
|
|
@ -211,7 +213,7 @@ DIFF
|
|||
\`\`\`
|
||||
|
||||
NEW_FILE
|
||||
\`\`\`
|
||||
\`\`\` typescript
|
||||
import React from 'react';
|
||||
import styles from './Sidebar.module.css';
|
||||
|
||||
|
|
@ -226,7 +228,7 @@ const Sidebar: React.FC<SidebarProps> = ({ items, onItemSelect, onExtraButtonCli
|
|||
\`\`\`
|
||||
|
||||
COMPLETION
|
||||
\`\`\`
|
||||
\`\`\` typescript
|
||||
<div className={styles.sidebar}>
|
||||
<ul>
|
||||
{items.map((item, index) => (
|
||||
|
|
@ -253,10 +255,13 @@ export default Sidebar;\`\`\`
|
|||
|
||||
|
||||
|
||||
export const ctrlLStream_prompt = ({ originalCode, userMessage }: { originalCode: string, userMessage: string }) => {
|
||||
export const ctrlLStream_prompt = ({ originalCode, userMessage, uri }: { originalCode: string, userMessage: string, uri: URI }) => {
|
||||
|
||||
const language = filenameToVscodeLanguage(uri.fsPath) ?? ''
|
||||
|
||||
return `\
|
||||
ORIGINAL_CODE
|
||||
\`\`\`
|
||||
\`\`\` ${language}
|
||||
${originalCode}
|
||||
\`\`\`
|
||||
|
||||
|
|
@ -281,7 +286,7 @@ export const ctrlKStream_prefixAndSuffix = ({ fullFileStr, startLine, endLine }:
|
|||
const fullFileLines = fullFileStr.split('\n')
|
||||
|
||||
// we can optimize this later
|
||||
const MAX_CHARS = 1024
|
||||
const MAX_PREFIX_SUFFIX_CHARS = 20_000
|
||||
/*
|
||||
|
||||
a
|
||||
|
|
@ -302,7 +307,7 @@ export const ctrlKStream_prefixAndSuffix = ({ fullFileStr, startLine, endLine }:
|
|||
// we'll include fullFileLines[i...(startLine-1)-1].join('\n') in the prefix.
|
||||
while (i !== 0) {
|
||||
const newLine = fullFileLines[i - 1]
|
||||
if (newLine.length + 1 + prefix.length <= MAX_CHARS) { // +1 to include the \n
|
||||
if (newLine.length + 1 + prefix.length <= MAX_PREFIX_SUFFIX_CHARS) { // +1 to include the \n
|
||||
prefix = `${newLine}\n${prefix}`
|
||||
i -= 1
|
||||
}
|
||||
|
|
@ -313,7 +318,7 @@ export const ctrlKStream_prefixAndSuffix = ({ fullFileStr, startLine, endLine }:
|
|||
let j = endLine - 1
|
||||
while (j !== fullFileLines.length - 1) {
|
||||
const newLine = fullFileLines[j + 1]
|
||||
if (newLine.length + 1 + suffix.length <= MAX_CHARS) { // +1 to include the \n
|
||||
if (newLine.length + 1 + suffix.length <= MAX_PREFIX_SUFFIX_CHARS) { // +1 to include the \n
|
||||
suffix = `${suffix}\n${newLine}`
|
||||
j += 1
|
||||
}
|
||||
|
|
@ -324,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}</${preTag}>
|
|||
<${sufTag}>${suffix}</${sufTag}>
|
||||
<${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}</${midTag}>
|
||||
\`\`\`
|
||||
|
||||
The user wants to apply the following instructions to the selection:
|
||||
The user wants to apply the following INSTRUCTIONS to the SELECTION:
|
||||
${userMessage}
|
||||
|
||||
Please rewrite the selection following the user's instructions.
|
||||
Please edit the SELECTION following the user's INSTRUCTIONS, and return the edited selection.
|
||||
|
||||
Instructions to follow:
|
||||
1. Follow the user's instructions
|
||||
2. You may ONLY CHANGE the selection, and nothing else in the file
|
||||
3. Make sure all brackets in the new selection are balanced the same was as in the original selection
|
||||
3. Be careful not to duplicate or remove variables, comments, or other syntax by mistake
|
||||
Note that the SELECTION has code that comes before it. This code is indicated with <${preTag}>...before<${preTag}/>.
|
||||
Note also that the SELECTION has code that comes after it. This code is indicated with <${sufTag}>...after<${sufTag}/>.
|
||||
|
||||
Complete the following:
|
||||
Instructions:
|
||||
1. Your OUTPUT should be a SINGLE PIECE OF CODE of the form <${midTag}>...new_selection<${midTag}/>. Do not 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}</${preTag}>
|
||||
<${sufTag}>${suffix}</${sufTag}>
|
||||
<${midTag}>`
|
||||
|
||||
Return only the completion block of code (of the form \`\`\` ${language}\n <${midTag}>...new_selection<${midTag}/>\`\`\`):`
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<>
|
||||
<div className={`relative group w-full overflow-hidden`}>
|
||||
|
||||
// Other
|
||||
'sql': 'sql',
|
||||
'graphql': 'graphql',
|
||||
'gql': 'graphql',
|
||||
'dockerfile': 'dockerfile',
|
||||
'docker': 'dockerfile'
|
||||
};
|
||||
{buttonsOnHover === null ? null : (
|
||||
<div className="z-[1] absolute top-0 right-0 opacity-0 group-hover:opacity-100 duration-200">
|
||||
<div className={`flex space-x-2 ${isSingleLine ? '' : 'p-2'}`}>
|
||||
{buttonsOnHover}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
export function getLanguageFromFileName(fileName: string): string {
|
||||
<VoidCodeEditor {...codeEditorProps} />
|
||||
|
||||
const ext = fileName.toLowerCase().split('.').pop();
|
||||
if (!ext) return 'plaintext';
|
||||
|
||||
return extensionMap[ext] || 'plaintext';
|
||||
}
|
||||
|
||||
export const BlockCode = ({ text, buttonsOnHover, language }: { text: string, buttonsOnHover?: ReactNode, language?: string }) => {
|
||||
|
||||
|
||||
const isSingleLine = !text.includes('\n')
|
||||
|
||||
return (<>
|
||||
<div className={`relative group w-full bg-vscode-editor-bg overflow-hidden isolate`}>
|
||||
{buttonsOnHover === null ? null : (
|
||||
<div className="z-[1] absolute top-0 right-0 opacity-0 group-hover:opacity-100 duration-200">
|
||||
<div className={`flex space-x-2 ${isSingleLine ? '' : 'p-2'}`}>{buttonsOnHover}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<VoidCodeEditor
|
||||
initValue={text}
|
||||
language={language}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 <>
|
||||
<button
|
||||
className={`${isSingleLine ? '' : 'p-1'} text-xs hover:brightness-110 bg-vscode-input-bg border border-vscode-input-border rounded text-xs text-vscode-input-fg`}
|
||||
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-1 text-void-fg-1 hover:brightness-110 border border-vscode-input-border rounded`}
|
||||
onClick={onCopy}
|
||||
>
|
||||
{copyButtonState}
|
||||
</button>
|
||||
<button
|
||||
// btn btn-secondary btn-sm border text-xs text-vscode-input-fg border-vscode-input-border rounded
|
||||
className={`${isSingleLine ? '' : 'p-1'} text-xs hover:brightness-110 bg-vscode-input-bg border border-vscode-input-border rounded text-xs text-vscode-input-fg`}
|
||||
// btn btn-secondary btn-sm border text-sm border-vscode-input-border rounded
|
||||
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-1 text-void-fg-1 hover:brightness-110 border border-vscode-input-border rounded`}
|
||||
onClick={onApply}
|
||||
>
|
||||
Apply
|
||||
|
|
@ -64,8 +70,21 @@ const CodeButtonsOnHover = ({ text }: { text: string }) => {
|
|||
</>
|
||||
}
|
||||
|
||||
export const CodeSpan = ({ children, className }: { children: React.ReactNode, className?: string }) => {
|
||||
return <code className={`
|
||||
bg-void-bg-1
|
||||
px-1
|
||||
rounded-sm
|
||||
font-mono font-medium
|
||||
break-all
|
||||
${className}
|
||||
`}
|
||||
>
|
||||
{children}
|
||||
</code>
|
||||
}
|
||||
|
||||
const RenderToken = ({ token, nested = false }: { token: Token | string, nested?: boolean }): JSX.Element => {
|
||||
const RenderToken = ({ token, nested = false, noSpace = false }: { token: Token | string, nested?: boolean, noSpace?: boolean }): JSX.Element => {
|
||||
|
||||
// deal with built-in tokens first (assume marked token)
|
||||
const t = token as MarkedToken
|
||||
|
|
@ -76,53 +95,68 @@ const RenderToken = ({ token, nested = false }: { token: Token | string, nested?
|
|||
|
||||
if (t.type === "code") {
|
||||
return <BlockCode
|
||||
text={t.text}
|
||||
// language={t.lang} // instead use vscode to detect language
|
||||
initValue={t.text}
|
||||
language={t.lang === undefined ? undefined : nameToVscodeLanguage[t.lang]} // use vscode to detect language
|
||||
buttonsOnHover={<CodeButtonsOnHover text={t.text} />}
|
||||
/>
|
||||
}
|
||||
|
||||
if (t.type === "heading") {
|
||||
const HeadingTag = `h${t.depth}` as keyof JSX.IntrinsicElements
|
||||
return <HeadingTag>{t.text}</HeadingTag>
|
||||
const headingClasses: { [h: string]: string } = {
|
||||
h1: "text-4xl font-semibold mt-6 mb-4 pb-2 border-b border-void-bg-2",
|
||||
h2: "text-3xl font-semibold mt-6 mb-4 pb-2 border-b border-void-bg-2",
|
||||
h3: "text-2xl font-semibold mt-6 mb-4",
|
||||
h4: "text-xl font-semibold mt-6 mb-4",
|
||||
h5: "text-lg font-semibold mt-6 mb-4",
|
||||
h6: "text-base font-semibold mt-6 mb-4 text-gray-600"
|
||||
}
|
||||
return <HeadingTag className={headingClasses[HeadingTag]}>{t.text}</HeadingTag>
|
||||
}
|
||||
|
||||
if (t.type === "table") {
|
||||
return (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
{t.header.map((cell: any, index: number) => (
|
||||
<th key={index} style={{ textAlign: t.align[index] || "left" }}>
|
||||
{cell.raw}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{t.rows.map((row: any[], rowIndex: number) => (
|
||||
<tr key={rowIndex}>
|
||||
{row.map((cell: any, cellIndex: number) => (
|
||||
<td
|
||||
key={cellIndex}
|
||||
style={{ textAlign: t.align[cellIndex] || "left" }}
|
||||
<div className={`${noSpace ? '' : 'my-4'} overflow-x-auto`}>
|
||||
<table className="min-w-full border border-void-bg-2">
|
||||
<thead>
|
||||
<tr className="bg-void-bg-1">
|
||||
{t.header.map((cell: any, index: number) => (
|
||||
<th
|
||||
key={index}
|
||||
className="px-4 py-2 border border-void-bg-2 font-semibold"
|
||||
style={{ textAlign: t.align[index] || "left" }}
|
||||
>
|
||||
{cell.raw}
|
||||
</td>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
{t.rows.map((row: any[], rowIndex: number) => (
|
||||
<tr key={rowIndex} className={rowIndex % 2 === 0 ? 'bg-white' : 'bg-void-bg-1'}>
|
||||
{row.map((cell: any, cellIndex: number) => (
|
||||
<td
|
||||
key={cellIndex}
|
||||
className="px-4 py-2 border border-void-bg-2"
|
||||
style={{ textAlign: t.align[cellIndex] || "left" }}
|
||||
>
|
||||
{cell.raw}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (t.type === "hr") {
|
||||
return <hr />
|
||||
return <hr className="my-6 border-t border-void-bg-2" />
|
||||
}
|
||||
|
||||
if (t.type === "blockquote") {
|
||||
return <blockquote>{t.text}</blockquote>
|
||||
return <blockquote className={`pl-4 border-l-4 border-void-bg-2 italic ${noSpace ? '' : 'my-4'}`}>{t.text}</blockquote>
|
||||
}
|
||||
|
||||
if (t.type === "list") {
|
||||
|
|
@ -130,14 +164,16 @@ const RenderToken = ({ token, nested = false }: { token: Token | string, nested?
|
|||
return (
|
||||
<ListTag
|
||||
start={t.start ? t.start : undefined}
|
||||
className={`list-inside ${t.ordered ? "list-decimal" : "list-disc"}`}
|
||||
className={`list-inside pl-2 ${noSpace ? '' : 'my-4'} ${t.ordered ? "list-decimal" : "list-disc"}`}
|
||||
>
|
||||
{t.items.map((item, index) => (
|
||||
<li key={index}>
|
||||
<li key={index} className={`${noSpace ? '' : 'mb-4'}`}>
|
||||
{item.task && (
|
||||
<input type="checkbox" checked={item.checked} readOnly />
|
||||
<input type="checkbox" checked={item.checked} readOnly className="mr-2 form-checkbox" />
|
||||
)}
|
||||
<ChatMarkdownRender string={item.text} nested={true} />
|
||||
<span className="ml-1">
|
||||
<ChatMarkdownRender string={item.text} nested={true} />
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ListTag>
|
||||
|
|
@ -152,13 +188,12 @@ const RenderToken = ({ token, nested = false }: { token: Token | string, nested?
|
|||
</>
|
||||
if (nested)
|
||||
return contents
|
||||
return <p>{contents}</p>
|
||||
return <p className={`${noSpace ? '' : 'my-4'} leading`}>{contents}</p>
|
||||
}
|
||||
|
||||
// don't actually render <html> tags, just render strings of them
|
||||
if (t.type === "html") {
|
||||
return (
|
||||
<pre>
|
||||
<pre className={`bg-4oid-bg-2 p-4 rounded-lg ${noSpace ? '' : 'my-4'} font-mono text-sm`}>
|
||||
{`<html>`}
|
||||
{t.raw}
|
||||
{`</html>`}
|
||||
|
|
@ -176,30 +211,40 @@ const RenderToken = ({ token, nested = false }: { token: Token | string, nested?
|
|||
|
||||
if (t.type === "link") {
|
||||
return (
|
||||
<a className='underline' onClick={() => { window.open(t.href) }} href={t.href} title={t.title ?? undefined}>
|
||||
<a
|
||||
className='underline'
|
||||
onClick={() => { window.open(t.href) }}
|
||||
href={t.href}
|
||||
title={t.title ?? undefined}
|
||||
>
|
||||
{t.text}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
if (t.type === "image") {
|
||||
return <img src={t.href} alt={t.text} title={t.title ?? undefined} />
|
||||
return <img
|
||||
src={t.href}
|
||||
alt={t.text}
|
||||
title={t.title ?? undefined}
|
||||
className={`max4w-full h-auto rounded ${noSpace ? '' : 'my-4'}`}
|
||||
/>
|
||||
}
|
||||
|
||||
if (t.type === "strong") {
|
||||
return <strong>{t.text}</strong>
|
||||
return <strong className="font-semibold">{t.text}</strong>
|
||||
}
|
||||
|
||||
if (t.type === "em") {
|
||||
return <em>{t.text}</em>
|
||||
return <em className="italic">{t.text}</em>
|
||||
}
|
||||
|
||||
// inline code
|
||||
if (t.type === "codespan") {
|
||||
return (
|
||||
<code className="text-vscode-text-preformat-fg bg-vscode-text-preformat-bg px-1 rounded-sm font-mono">
|
||||
<CodeSpan>
|
||||
{t.text}
|
||||
</code>
|
||||
</CodeSpan>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -209,24 +254,24 @@ const RenderToken = ({ token, nested = false }: { token: Token | string, nested?
|
|||
|
||||
// strikethrough
|
||||
if (t.type === "del") {
|
||||
return <del>{t.text}</del>
|
||||
return <del className="line-through">{t.text}</del>
|
||||
}
|
||||
|
||||
// default
|
||||
return (
|
||||
<div className="bg-orange-50 rounded-sm overflow-hidden">
|
||||
<span className="text-xs text-orange-500">Unknown type:</span>
|
||||
<div className="bg-orange-50 rounded-sm overflow-hidden p-2">
|
||||
<span className="text-sm text-orange-500">Unknown type:</span>
|
||||
{t.raw}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const ChatMarkdownRender = ({ string, nested = false }: { string: string, nested?: boolean }) => {
|
||||
export const ChatMarkdownRender = ({ string, nested = false, noSpace }: { string: string, nested?: boolean, noSpace?: boolean }) => {
|
||||
const tokens = marked.lexer(string); // https://marked.js.org/using_pro#renderer
|
||||
return (
|
||||
<>
|
||||
{tokens.map((token, index) => (
|
||||
<RenderToken key={index} token={token} nested={nested} />
|
||||
<RenderToken key={index} token={token} nested={nested} noSpace={noSpace} />
|
||||
))}
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 <div className={`@@void-scope ${isDark ? 'dark' : ''}`}>
|
||||
<ErrorBoundary>
|
||||
<CtrlKChat {...props} />
|
||||
<QuickEditChat {...props} />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
|
|
@ -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<HTMLDivElement | null>(null)
|
||||
const inputBoxRef: React.MutableRefObject<InputBox | null> = useRef(null);
|
||||
const textAreaRef = useRef<HTMLTextAreaElement | null>(null)
|
||||
const textAreaFnsRef = useRef<TextAreaFns | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const inputContainer = sizerRef.current
|
||||
if (!inputContainer) return;
|
||||
|
||||
// only observing 1 element
|
||||
let resizeObserver: ResizeObserver | undefined
|
||||
resizeObserver = new ResizeObserver((entries) => {
|
||||
|
|
@ -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<number | undefined>(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 <div ref={sizerRef} className='py-2 w-full max-w-xl'>
|
||||
<form
|
||||
// copied from SidebarChat.tsx
|
||||
className={`
|
||||
flex flex-col gap-2 py-1 px-2 relative input text-left shrink-0
|
||||
flex flex-col gap-2 p-2 relative input text-left shrink-0
|
||||
transition-all duration-200
|
||||
rounded-md
|
||||
bg-vscode-input-bg
|
||||
border border-vscode-commandcenter-inactive-border focus-within:border-vscode-commandcenter-active-border hover:border-vscode-commandcenter-active-border
|
||||
`
|
||||
}
|
||||
onKeyDown={(e) => {
|
||||
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`}
|
||||
>
|
||||
<div className='flex flex-row justify-between items-end gap-1'>
|
||||
<div className='absolute size-0.5 top-0 right-4 z-[1]'>
|
||||
<X
|
||||
onClick={() => { inlineDiffsService.removeCtrlKZone({ diffareaid }) }}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex flex-row items-center justify-between items-end gap-1'>
|
||||
|
||||
{/* input */}
|
||||
<div // copied from SidebarChat.tsx
|
||||
className={`w-full
|
||||
@@[&_textarea]:!void-bg-transparent @@[&_textarea]:!void-outline-none @@[&_textarea]:!void-text-vscode-input-fg @@[&_div.monaco-inputbox]:!void-outline-none`}>
|
||||
@@[&_textarea]:!void-bg-transparent
|
||||
@@[&_textarea]:!void-outline-none
|
||||
@@[&_textarea]:!void-text-vscode-input-fg
|
||||
@@[&_div.monaco-inputbox]:!void-outline-none
|
||||
`}
|
||||
>
|
||||
{/* text input */}
|
||||
<VoidInputBox
|
||||
placeholder={`${getCmdKey()}+K to select`}
|
||||
onChangeText={onChangeText}
|
||||
onCreateInstance={useCallback((instance: InputBox) => {
|
||||
inputBoxRef.current = instance;
|
||||
onGetInputBox(instance);
|
||||
instance.focus()
|
||||
}, [onGetInputBox])}
|
||||
<VoidInputBox2
|
||||
|
||||
initValue={initText}
|
||||
|
||||
ref={useCallback((r: HTMLTextAreaElement | null) => {
|
||||
textAreaRef.current = r
|
||||
textAreaRef_(r)
|
||||
|
||||
// if presses the esc key, X
|
||||
r?.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape')
|
||||
onX()
|
||||
})
|
||||
|
||||
}, [textAreaRef_, onX])}
|
||||
|
||||
fnsRef={textAreaFnsRef}
|
||||
|
||||
placeholder={`${keybindingString} to select`}
|
||||
|
||||
onChangeText={useCallback((newStr: string) => {
|
||||
setInstructionsAreEmpty(!newStr)
|
||||
onChangeText_(newStr)
|
||||
}, [onChangeText_])}
|
||||
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
onSubmit(e)
|
||||
return
|
||||
}
|
||||
}}
|
||||
|
||||
multiline={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* X button */}
|
||||
<div className='absolute -top-1 -right-1 cursor-pointer z-1'>
|
||||
<IconX
|
||||
size={16}
|
||||
className="p-[1px] stroke-[2] opacity-80 text-void-fg-3 hover:brightness-95"
|
||||
onClick={onX}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
|
@ -163,6 +179,7 @@ export const CtrlKChat = ({ diffareaid, onGetInputBox, onUserUpdateText, onChang
|
|||
:
|
||||
// submit button (up arrow)
|
||||
<ButtonSubmit
|
||||
onClick={onSubmit}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className={`rounded-lg border border-red-200 bg-red-50 p-4 overflow-auto`}>
|
||||
{/* Header */}
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex gap-3">
|
||||
<AlertCircle className="h-5 w-5 text-red-600 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<h3 className="font-semibold text-red-800">
|
||||
<div className='flex items-start justify-between'>
|
||||
<div className='flex gap-3'>
|
||||
<AlertCircle className='h-5 w-5 text-red-600 mt-0.5' />
|
||||
<div className='flex-1'>
|
||||
<h3 className='font-semibold text-red-800'>
|
||||
{/* eg Error */}
|
||||
Error
|
||||
</h3>
|
||||
<p className="text-red-700 mt-1">
|
||||
<p className='text-red-700 mt-1'>
|
||||
{/* eg Something went wrong */}
|
||||
{message}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<div className='flex gap-2'>
|
||||
{details && (
|
||||
<button className="text-red-600 hover:text-red-800 p-1 rounded"
|
||||
<button className='text-red-600 hover:text-red-800 p-1 rounded'
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronUp className="h-5 w-5" />
|
||||
<ChevronUp className='h-5 w-5' />
|
||||
) : (
|
||||
<ChevronDown className="h-5 w-5" />
|
||||
<ChevronDown className='h-5 w-5' />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
{showDismiss && onDismiss && (
|
||||
<button className="text-red-600 hover:text-red-800 p-1 rounded"
|
||||
<button className='text-red-600 hover:text-red-800 p-1 rounded'
|
||||
onClick={onDismiss}
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
<X className='h-5 w-5' />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -75,10 +66,10 @@ export const ErrorDisplay = ({
|
|||
|
||||
{/* Expandable Details */}
|
||||
{isExpanded && details && (
|
||||
<div className="mt-4 space-y-3 border-t border-red-200 pt-3 overflow-auto">
|
||||
<div className='mt-4 space-y-3 border-t border-red-200 pt-3 overflow-auto'>
|
||||
<div>
|
||||
<span className="font-semibold text-red-800">Full Error: </span>
|
||||
<pre className="text-red-700">{details}</pre>
|
||||
<span className='font-semibold text-red-800'>Full Error: </span>
|
||||
<pre className='text-red-700'>{details}</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/*------------------------------------------------------------------------------------------
|
||||
* Copyright (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 <div className={`@@void-scope`} style={{ width: '100%', height: '100%' }}>
|
||||
<div className={`w-full h-full flex flex-col py-2 bg-vscode-sidebar-bg`}>
|
||||
// const isDark = useIsDark()
|
||||
return <div
|
||||
className={`@@void-scope`} // ${isDark ? 'dark' : ''}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
>
|
||||
<div
|
||||
// default background + text styles for sidebar
|
||||
className={`
|
||||
w-full h-full py-2
|
||||
bg-void-bg-2
|
||||
text-void-fg-1
|
||||
`}
|
||||
>
|
||||
|
||||
{/* <span onClick={() => {
|
||||
const tabs = ['chat', 'settings', 'threadSelector']
|
||||
|
|
@ -32,11 +40,11 @@ export const Sidebar = ({ className }: { className: string }) => {
|
|||
sidebarStateService.setState({ currentTab: tabs[(index + 1) % tabs.length] as any })
|
||||
}}>clickme {tab}</span> */}
|
||||
|
||||
<div className={`w-full h-auto mb-2 ${isHistoryOpen ? '' : 'hidden'} ring-2 ring-widget-shadow z-10`}>
|
||||
{/* <div className={`w-full h-auto mb-2 ${isHistoryOpen ? '' : 'hidden'} ring-2 ring-widget-shadow z-10`}>
|
||||
<ErrorBoundary>
|
||||
<SidebarThreadSelector />
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
<div className={`w-full h-full ${tab === 'chat' ? '' : 'hidden'}`}>
|
||||
<ErrorBoundary>
|
||||
|
|
|
|||
|
|
@ -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<SVGSVGElement>) => {
|
||||
export const IconX = ({ size, className = '', ...props }: { size: number, className?: string } & React.SVGProps<SVGSVGElement>) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
|
|
@ -47,14 +52,13 @@ const IconX = ({ size, className = '', ...props }: { size: number, className?: s
|
|||
);
|
||||
};
|
||||
|
||||
|
||||
const IconArrowUp = ({ size, className = '' }: { size: number, className?: string }) => {
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
className={className}
|
||||
viewBox="0 0 32 32"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
|
|
@ -62,10 +66,9 @@ const IconArrowUp = ({ size, className = '' }: { size: number, className?: strin
|
|||
fill="black"
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M15.1918 8.90615C15.6381 8.45983 16.3618 8.45983 16.8081 8.90615L21.9509 14.049C22.3972 14.4953 22.3972 15.2189 21.9509 15.6652C21.5046 16.1116 20.781 16.1116 20.3347 15.6652L17.1428 12.4734V22.2857C17.1428 22.9169 16.6311 23.4286 15.9999 23.4286C15.3688 23.4286 14.8571 22.9169 14.8571 22.2857V12.4734L11.6652 15.6652C11.2189 16.1116 10.4953 16.1116 10.049 15.6652C9.60265 15.2189 9.60265 14.4953 10.049 14.049L15.1918 8.90615Z"
|
||||
d="M5.293 9.707a1 1 0 010-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 01-1.414 1.414L11 7.414V15a1 1 0 11-2 0V7.414L6.707 9.707a1 1 0 01-1.414 0z"
|
||||
></path>
|
||||
</svg>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -169,38 +172,40 @@ const useResizeObserver = () => {
|
|||
|
||||
|
||||
type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement>
|
||||
const DEFAULT_BUTTON_SIZE = 20;
|
||||
const DEFAULT_BUTTON_SIZE = 22;
|
||||
export const ButtonSubmit = ({ className, disabled, ...props }: ButtonProps & Required<Pick<ButtonProps, 'disabled'>>) => {
|
||||
|
||||
return <button
|
||||
type='submit'
|
||||
className={`size-[20px] rounded-full shrink-0 grow-0 cursor-pointer
|
||||
type='button'
|
||||
className={`rounded-full flex-shrink-0 flex-grow-0 cursor-pointer flex items-center justify-center
|
||||
${disabled ? 'bg-vscode-disabled-fg' : 'bg-white'}
|
||||
${className}
|
||||
`}
|
||||
{...props}
|
||||
>
|
||||
<IconArrowUp size={DEFAULT_BUTTON_SIZE} className="stroke-[2]" />
|
||||
<IconArrowUp size={DEFAULT_BUTTON_SIZE} className="stroke-[2] p-[2px]" />
|
||||
</button>
|
||||
}
|
||||
|
||||
export const ButtonStop = ({ className, ...props }: ButtonHTMLAttributes<HTMLButtonElement>) => {
|
||||
|
||||
return <button
|
||||
className={`rounded-full bg-white shrink-0 grow-0 cursor-pointer flex items-center justify-center
|
||||
className={`rounded-full flex-shrink-0 flex-grow-0 cursor-pointer flex items-center justify-center
|
||||
bg-white
|
||||
${className}
|
||||
`}
|
||||
type='button'
|
||||
{...props}
|
||||
>
|
||||
<IconSquare size={DEFAULT_BUTTON_SIZE} className="stroke-[2] p-[6px]" />
|
||||
<IconSquare size={DEFAULT_BUTTON_SIZE} className="stroke-[3] p-[7px]" />
|
||||
</button>
|
||||
}
|
||||
|
||||
|
||||
const ScrollToBottomContainer = ({ children, className, style }: { children: React.ReactNode, className?: string, style?: React.CSSProperties }) => {
|
||||
const ScrollToBottomContainer = ({ children, className, style, scrollContainerRef }: { children: React.ReactNode, className?: string, style?: React.CSSProperties, scrollContainerRef: React.MutableRefObject<HTMLDivElement | null> }) => {
|
||||
const [isAtBottom, setIsAtBottom] = useState(true); // Start at bottom
|
||||
const divRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const divRef = scrollContainerRef
|
||||
|
||||
const scrollToBottom = () => {
|
||||
if (divRef.current) {
|
||||
|
|
@ -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 && (
|
||||
<div
|
||||
className='flex flex-wrap gap-2 text-left'
|
||||
className='flex items-center flex-wrap gap-0.5 text-left relative'
|
||||
>
|
||||
{selections.map((selection, i) => {
|
||||
|
||||
const isThisSelectionOpened = !!(selection.selectionStr && selectionIsOpened[i])
|
||||
const isThisSelectionAFile = selection.selectionStr === null
|
||||
|
||||
return (
|
||||
<div key={i} // container for `selectionSummary` and `selectionText`
|
||||
className={`${isThisSelectionOpened ? 'w-full' : ''}`}
|
||||
return <div key={i} // container for `selectionSummary` and `selectionText`
|
||||
className={`${isThisSelectionOpened ? 'w-full' : ''}`}
|
||||
>
|
||||
{/* selection summary */}
|
||||
<div // container for delete button
|
||||
className='flex items-center gap-0.5'
|
||||
>
|
||||
{/* selection summary */}
|
||||
<div
|
||||
// className="relative rounded rounded-e-2xl flex items-center space-x-2 mx-1 mb-1 disabled:cursor-default"
|
||||
className={`flex items-center gap-1 relative
|
||||
rounded-md p-1
|
||||
<div // styled summary box
|
||||
className={`flex items-center gap-0.5 relative
|
||||
rounded-md px-1
|
||||
w-fit h-fit
|
||||
select-none
|
||||
bg-vscode-editor-bg hover:brightness-95
|
||||
border border-vscode-commandcenter-border rounded-xs
|
||||
text-xs text-vscode-editor-fg text-nowrap
|
||||
`}
|
||||
bg-void-bg-3 hover:brightness-95
|
||||
text-void-fg-1 text-xs text-nowrap
|
||||
border rounded-xs ${isClearHovered ? 'border-void-border-1' : 'border-void-border-2'}
|
||||
transition-all duration-150`}
|
||||
onClick={() => {
|
||||
setSelectionIsOpened(s => {
|
||||
const newS = [...s]
|
||||
newS[i] = !newS[i]
|
||||
return newS
|
||||
});
|
||||
// open the file if it is a file
|
||||
if (isThisSelectionAFile) {
|
||||
commandService.executeCommand('vscode.open', selection.fileURI, {
|
||||
preview: true,
|
||||
// preserveFocus: false,
|
||||
});
|
||||
} else {
|
||||
// open the selection if it is a text-selection
|
||||
setSelectionIsOpened(s => {
|
||||
const newS = [...s]
|
||||
newS[i] = !newS[i]
|
||||
return newS
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className=''>
|
||||
<span>
|
||||
{/* file name */}
|
||||
{getBasename(selection.fileURI.fsPath)}
|
||||
{/* selection range */}
|
||||
{selection.selectionStr !== null ? ` (${selection.range.startLineNumber}-${selection.range.endLineNumber})` : ''}
|
||||
{!isThisSelectionAFile ? ` (${selection.range.startLineNumber}-${selection.range.endLineNumber})` : ''}
|
||||
</span>
|
||||
|
||||
{/* X button */}
|
||||
{type === 'staging' &&
|
||||
<span
|
||||
className='
|
||||
cursor-pointer
|
||||
bg-vscode-editorwidget-bg hover:bg-vscode-toolbar-hover-bg
|
||||
rounded-md
|
||||
z-1
|
||||
'
|
||||
className='cursor-pointer hover:brightness-95 rounded-md z-1'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (type !== 'staging') return;
|
||||
setStaging([...selections.slice(0, i), ...selections.slice(i + 1)])
|
||||
setSelectionIsOpened(o => [...o.slice(0, i), ...o.slice(i + 1)])
|
||||
}}
|
||||
>
|
||||
<IconX size={16} className="p-[2px] stroke-[3] text-vscode-toolbar-foreground" />
|
||||
</span>
|
||||
}
|
||||
|
||||
{/* type of selection */}
|
||||
{/* <span className='truncate'>{selection.selectionStr !== null ? 'Selection' : 'File'}</span> */}
|
||||
{/* X button */}
|
||||
{/* {type === 'staging' && // hoveredIdx === i
|
||||
<span className='absolute right-0 top-0 translate-x-[50%] translate-y-[-50%] cursor-pointer bg-white rounded-full border border-vscode-input-border z-1'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.stopPropagation(); // don't open/close selection
|
||||
if (type !== 'staging') return;
|
||||
setStaging([...selections.slice(0, i), ...selections.slice(i + 1)])
|
||||
setSelectionIsOpened(o => [...o.slice(0, i), ...o.slice(i + 1)])
|
||||
}}
|
||||
>
|
||||
<IconX size={16} className="p-[2px] stroke-[3]" />
|
||||
</span>
|
||||
} */}
|
||||
</span>}
|
||||
|
||||
|
||||
</div>
|
||||
{/* selection text */}
|
||||
{isThisSelectionOpened &&
|
||||
<div className='w-full p-1 rounded-sm border-vscode-editor-border'>
|
||||
<BlockCode text={selection.selectionStr!} language={getLanguageFromFileName(selection.fileURI.path)} />
|
||||
|
||||
{/* clear all selections button */}
|
||||
{type !== 'staging' || selections.length === 0 || i !== selections.length - 1
|
||||
? null
|
||||
: <div key={i} className={`flex items-center gap-0.5 ${isThisSelectionOpened ? 'w-full' : ''}`}>
|
||||
<div
|
||||
className='rounded-md'
|
||||
onMouseEnter={() => setIsClearHovered(true)}
|
||||
onMouseLeave={() => setIsClearHovered(false)}
|
||||
>
|
||||
<Delete
|
||||
size={16}
|
||||
className={`stroke-[1]
|
||||
stroke-void-fg-1
|
||||
fill-void-bg-3
|
||||
opacity-40
|
||||
hover:opacity-60
|
||||
transition-all duration-150
|
||||
cursor-pointer
|
||||
`}
|
||||
onClick={() => { setStaging([]) }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
{/* selection text */}
|
||||
{isThisSelectionOpened &&
|
||||
<div
|
||||
className='w-full px-1 rounded-sm border-vscode-editor-border'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation(); // don't focus input box
|
||||
}}
|
||||
>
|
||||
<BlockCode
|
||||
initValue={selection.selectionStr!}
|
||||
language={filenameToVscodeLanguage(selection.fileURI.path)}
|
||||
maxHeight={100}
|
||||
showScrollbars={true}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
})}
|
||||
|
||||
|
||||
</div>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
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 = <>
|
||||
<SelectedFiles type='past' selections={chatMessage.selections} />
|
||||
{chatMessage.displayContent}
|
||||
|
||||
{/* {!isEditMode ? chatMessage.displayContent : <></>} */}
|
||||
{/* edit mode content */}
|
||||
{/* TODO this should be the same input box as in the Sidebar */}
|
||||
{/* <textarea
|
||||
value={editModeText}
|
||||
className={`
|
||||
w-full max-w-full
|
||||
h-auto min-h-[81px] max-h-[500px]
|
||||
bg-void-bg-1 resize-none
|
||||
`}
|
||||
style={{ marginTop: 0 }}
|
||||
hidden={!isEditMode}
|
||||
/> */}
|
||||
|
||||
</>
|
||||
}
|
||||
else if (role === 'assistant') {
|
||||
chatbubbleContents = <ChatMarkdownRender string={chatMessage.displayContent} /> // sectionsHTML
|
||||
chatbubbleContents = <ChatMarkdownRender string={chatMessage.displayContent} />
|
||||
}
|
||||
|
||||
return <div
|
||||
// style + align chatbubble accoridng to role
|
||||
className={`p-2 mx-2 text-left space-y-2 rounded-lg max-w-full
|
||||
${role === 'user' ? 'self-end' : 'self-start'}
|
||||
${role === 'user' ? 'bg-vscode-input-bg text-vscode-input-fg' : ''}
|
||||
${role === 'assistant' ? 'w-full' : ''}
|
||||
`}
|
||||
// align chatbubble accoridng to role
|
||||
className={`
|
||||
relative
|
||||
${isEditMode ? 'px-2 w-full max-w-full'
|
||||
: role === 'user' ? `px-2 self-end w-fit max-w-full`
|
||||
: role === 'assistant' ? `px-2 self-start w-full max-w-full` : ''
|
||||
}
|
||||
`}
|
||||
>
|
||||
{chatbubbleContents}
|
||||
{isLoading && <IconLoading className='opacity-50 text-sm' />}
|
||||
<div
|
||||
// style chatbubble according to role
|
||||
className={`
|
||||
p-2 text-left space-y-2 rounded-lg
|
||||
overflow-x-auto max-w-full
|
||||
${role === 'user' ? 'bg-void-bg-1 text-void-fg-1' : ''}
|
||||
`}
|
||||
>
|
||||
{chatbubbleContents}
|
||||
{isLoading && <IconLoading className='opacity-50 text-sm' />}
|
||||
</div>
|
||||
|
||||
{/* edit button */}
|
||||
{/* {role === 'user' &&
|
||||
<Pencil
|
||||
size={16}
|
||||
className={`
|
||||
absolute top-0 right-2
|
||||
translate-x-0 -translate-y-0
|
||||
cursor-pointer z-1
|
||||
`}
|
||||
onClick={() => { setIsEditMode(v => !v); }}
|
||||
/>
|
||||
} */}
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const SidebarChat = () => {
|
||||
|
||||
const inputBoxRef: React.MutableRefObject<InputBox | null> = useRef(null);
|
||||
const textAreaRef = useRef<HTMLTextAreaElement | null>(null)
|
||||
const textAreaFnsRef = useRef<TextAreaFns | null>(null)
|
||||
|
||||
const accessor = useAccessor()
|
||||
const modelService = accessor.get('IModelService')
|
||||
|
|
@ -410,11 +492,13 @@ export const SidebarChat = () => {
|
|||
useEffect(() => {
|
||||
const disposables: IDisposable[] = []
|
||||
disposables.push(
|
||||
sidebarStateService.onDidFocusChat(() => { inputBoxRef.current?.focus() }),
|
||||
sidebarStateService.onDidBlurChat(() => { inputBoxRef.current?.blur() })
|
||||
sidebarStateService.onDidFocusChat(() => { textAreaRef.current?.focus() }),
|
||||
sidebarStateService.onDidBlurChat(() => { textAreaRef.current?.blur() })
|
||||
)
|
||||
return () => disposables.forEach(d => d.dispose())
|
||||
}, [sidebarStateService, inputBoxRef])
|
||||
}, [sidebarStateService, textAreaRef])
|
||||
|
||||
const { currentTab, isHistoryOpen } = useSidebarState()
|
||||
|
||||
// threads state
|
||||
const threadsState = useThreadsState()
|
||||
|
|
@ -433,25 +517,21 @@ export const SidebarChat = () => {
|
|||
|
||||
|
||||
// state of current message
|
||||
const [instructions, setInstructions] = useState('') // the user's instructions
|
||||
const isDisabled = !instructions.trim()
|
||||
const initVal = ''
|
||||
const [instructionsAreEmpty, setInstructionsAreEmpty] = useState(!initVal)
|
||||
const isDisabled = instructionsAreEmpty
|
||||
|
||||
const [sidebarRef, sidebarDimensions] = useResizeObserver()
|
||||
const [formRef, formDimensions] = useResizeObserver()
|
||||
const [historyRef, historyDimensions] = useResizeObserver()
|
||||
|
||||
// const [formHeight, setFormHeight] = useState(0) // TODO should use resize observer instead
|
||||
// const [sidebarHeight, setSidebarHeight] = useState(0)
|
||||
const onChangeText = useCallback((newStr: string) => { setInstructions(newStr) }, [setInstructions])
|
||||
useScrollbarStyles(sidebarRef)
|
||||
|
||||
const onSubmit = async () => {
|
||||
|
||||
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||
|
||||
e.preventDefault()
|
||||
if (isDisabled) return
|
||||
if (isLoading) return
|
||||
|
||||
|
||||
|
||||
const currSelns = threadsStateService.state._currentStagingSelections ?? []
|
||||
const selections = !currSelns ? null : await Promise.all(
|
||||
currSelns.map(async (sel) => ({ ...sel, content: await VSReadFile(modelService, sel.fileURI) }))
|
||||
|
|
@ -477,6 +557,7 @@ export const SidebarChat = () => {
|
|||
threadsStateService.addMessageToCurrentThread(systemPromptElt)
|
||||
|
||||
// add user's message to chat history
|
||||
const instructions = textAreaRef.current?.value ?? ''
|
||||
const userHistoryElt: ChatMessage = { role: 'user', content: chat_prompt(instructions, selections), displayContent: instructions, selections: selections }
|
||||
threadsStateService.addMessageToCurrentThread(userHistoryElt)
|
||||
|
||||
|
|
@ -485,9 +566,9 @@ export const SidebarChat = () => {
|
|||
// send message to LLM
|
||||
setIsLoading(true) // must come before message is sent so onError will work
|
||||
setLatestError(null)
|
||||
if (inputBoxRef.current) {
|
||||
inputBoxRef.current.value = ''; // this triggers onDidChangeText
|
||||
inputBoxRef.current.blur();
|
||||
if (textAreaRef.current) {
|
||||
textAreaFnsRef.current?.setValue('') // triggers onChange
|
||||
textAreaRef.current.blur();
|
||||
}
|
||||
|
||||
const object: ServiceSendLLMMessageParams = {
|
||||
|
|
@ -525,7 +606,7 @@ export const SidebarChat = () => {
|
|||
|
||||
threadsStateService.setStaging([]) // clear staging
|
||||
|
||||
inputBoxRef.current?.focus() // focus input after submit
|
||||
textAreaRef.current?.focus() // focus input after submit
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -552,31 +633,45 @@ export const SidebarChat = () => {
|
|||
|
||||
// const [_test_messages, _set_test_messages] = useState<string[]>([])
|
||||
|
||||
const keybindingString = accessor.get('IKeybindingService').lookupKeybinding(VOID_CTRL_L_ACTION_ID)?.getLabel()
|
||||
|
||||
// scroll to top on thread switch
|
||||
const scrollContainerRef = useRef<HTMLDivElement | null>(null)
|
||||
useEffect(() => {
|
||||
if (isHistoryOpen)
|
||||
scrollContainerRef.current?.scrollTo({ top: 0, left: 0 })
|
||||
}, [isHistoryOpen, currentThread?.id])
|
||||
|
||||
return <div
|
||||
ref={sidebarRef}
|
||||
className={`w-full h-full`}
|
||||
>
|
||||
{/* thread selector */}
|
||||
<div ref={historyRef}
|
||||
className={`w-full h-auto ${isHistoryOpen ? '' : 'hidden'} ring-2 ring-widget-shadow ring-inset z-10`}
|
||||
>
|
||||
<SidebarThreadSelector />
|
||||
</div>
|
||||
|
||||
{/* previous messages + current stream */}
|
||||
<ScrollToBottomContainer
|
||||
scrollContainerRef={scrollContainerRef}
|
||||
className={`
|
||||
w-full h-auto
|
||||
flex flex-col gap-0
|
||||
overflow-x-hidden
|
||||
overflow-y-auto
|
||||
`}
|
||||
style={{ maxHeight: sidebarDimensions.height - formDimensions.height - 30 }}
|
||||
style={{ maxHeight: sidebarDimensions.height - historyDimensions.height - formDimensions.height - 30 }} // the height of the previousMessages is determined by all other heights
|
||||
>
|
||||
{/* previous messages */}
|
||||
{previousMessages.map((message, i) => <ChatBubble key={i} chatMessage={message} />)}
|
||||
{previousMessages.map((message, i) =>
|
||||
<ChatBubble key={i} chatMessage={message} />
|
||||
)}
|
||||
|
||||
{/* message stream */}
|
||||
<ChatBubble chatMessage={{ role: 'assistant', content: messageStream, displayContent: messageStream || null }} isLoading={isLoading} />
|
||||
|
||||
{/* {_test_messages.map((_, i) => <div key={i}>div {i}</div>)}
|
||||
<div>{`totalHeight: ${sidebarHeight - formHeight - 30}`}</div>
|
||||
<div>{`sidebarHeight: ${sidebarHeight}`}</div>
|
||||
<div>{`formHeight: ${formHeight}`}</div>
|
||||
<button type='button' onClick={() => { _set_test_messages(d => [...d, 'asdasdsadasd']) }}>add div</button> */}
|
||||
|
||||
</ScrollToBottomContainer>
|
||||
|
||||
|
||||
|
|
@ -584,26 +679,18 @@ export const SidebarChat = () => {
|
|||
<div // this div is used to position the input box properly
|
||||
className={`right-0 left-0 m-2 z-[999] overflow-hidden ${previousMessages.length > 0 ? 'absolute bottom-0' : ''}`}
|
||||
>
|
||||
<form
|
||||
<div
|
||||
ref={formRef}
|
||||
className={`
|
||||
flex flex-col gap-2 p-2 relative input text-left shrink-0
|
||||
transition-all duration-200
|
||||
rounded-md
|
||||
bg-vscode-input-bg
|
||||
border border-vscode-commandcenter-inactive-border focus-within:border-vscode-commandcenter-active-border hover:border-vscode-commandcenter-active-border
|
||||
max-h-[80vh] overflow-y-auto
|
||||
border border-void-border-3 focus-within:border-void-border-1 hover:border-void-border-1
|
||||
`}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
onSubmit(e)
|
||||
}
|
||||
}}
|
||||
onSubmit={(e) => {
|
||||
console.log('submit!')
|
||||
onSubmit(e)
|
||||
}}
|
||||
onClick={(e) => {
|
||||
inputBoxRef.current?.focus()
|
||||
textAreaRef.current?.focus()
|
||||
}}
|
||||
>
|
||||
{/* top row */}
|
||||
|
|
@ -636,21 +723,27 @@ export const SidebarChat = () => {
|
|||
// .split(' ')
|
||||
// .map(style => `@@[&_div.monaco-inputbox]:!void-${style}`)
|
||||
// .join(' ');
|
||||
`@@[&_textarea]:!void-bg-transparent
|
||||
`
|
||||
@@[&_textarea]:!void-outline-none
|
||||
@@[&_textarea]:!void-text-vscode-input-fg
|
||||
@@[&_textarea]:!void-min-h-[81px]
|
||||
@@[&_textarea]:!void-max-h-[500px]
|
||||
@@[&_div.monaco-inputbox]:!void-border-none
|
||||
@@[&_div.monaco-inputbox]:!void-outline-none`
|
||||
@@[&_div.monaco-inputbox]:!void-outline-none
|
||||
`
|
||||
}
|
||||
>
|
||||
|
||||
{/* text input */}
|
||||
<VoidInputBox
|
||||
placeholder={`${getCmdKey()}+L to select`}
|
||||
onChangeText={onChangeText}
|
||||
inputBoxRef={inputBoxRef}
|
||||
<VoidInputBox2
|
||||
placeholder={`${keybindingString} to select`}
|
||||
onChangeText={useCallback((newStr: string) => { setInstructionsAreEmpty(!newStr) }, [setInstructionsAreEmpty])}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
onSubmit()
|
||||
}
|
||||
}}
|
||||
ref={textAreaRef}
|
||||
fnsRef={textAreaFnsRef}
|
||||
multiline={true}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -662,7 +755,9 @@ export const SidebarChat = () => {
|
|||
{/* submit options */}
|
||||
<div className='max-w-[150px]
|
||||
@@[&_select]:!void-border-none
|
||||
@@[&_select]:!void-outline-none'
|
||||
@@[&_select]:!void-outline-none
|
||||
flex-grow
|
||||
'
|
||||
>
|
||||
<ModelDropdown featureName='Ctrl+L' />
|
||||
</div>
|
||||
|
|
@ -676,13 +771,14 @@ export const SidebarChat = () => {
|
|||
:
|
||||
// submit button (up arrow)
|
||||
<ButtonSubmit
|
||||
onClick={onSubmit}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div >
|
||||
</div >
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 from "react";
|
||||
import { useAccessor, useThreadsState } from '../util/services.js';
|
||||
import { IThreadHistoryService } from '../../../threadHistoryService.js';
|
||||
import { ISidebarStateService } from '../../../sidebarStateService.js';
|
||||
import { IconX } from './SidebarChat.js';
|
||||
|
||||
|
||||
const truncate = (s: string) => {
|
||||
|
|
@ -31,63 +32,87 @@ export const SidebarThreadSelector = () => {
|
|||
const sortedThreadIds = Object.keys(allThreads ?? {}).sort((threadId1, threadId2) => allThreads![threadId1].lastModified > allThreads![threadId2].lastModified ? -1 : 1)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-1 max-h-[400px] overflow-y-auto">
|
||||
<div className="flex p-2 flex-col mb-2 gap-y-1 max-h-[400px] overflow-y-auto">
|
||||
|
||||
{/* X button at top right */}
|
||||
<div className="text-right">
|
||||
<button className="btn btn-sm" onClick={() => sidebarStateService.setState({ isHistoryOpen: false })}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
className="size-4"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M6 18 18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
<div className="w-full relative flex justify-center items-center">
|
||||
{/* title */}
|
||||
<h2 className='font-bold text-lg'>{`History`}</h2>
|
||||
{/* X button at top right */}
|
||||
<button
|
||||
type='button'
|
||||
className='absolute top-0 right-0'
|
||||
onClick={() => sidebarStateService.setState({ isHistoryOpen: false })}
|
||||
>
|
||||
<IconX
|
||||
size={16}
|
||||
className="p-[1px] stroke-[2] opacity-80 text-void-fg-3 hover:brightness-95"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* a list of all the past threads */}
|
||||
<div className='flex flex-col gap-y-1 overflow-y-auto'>
|
||||
{sortedThreadIds.map((threadId) => {
|
||||
if (!allThreads)
|
||||
return <>Error: Threads not found.</>
|
||||
const pastThread = allThreads[threadId]
|
||||
<div className="px-1">
|
||||
<ul className="flex flex-col gap-y-0.5 overflow-y-auto list-disc">
|
||||
|
||||
let btnStringArr: string[] = []
|
||||
{sortedThreadIds.length === 0
|
||||
|
||||
const firstMsgIdx = allThreads[threadId].messages.findIndex(msg => msg.role !== 'system' && !!msg.displayContent) ?? ''
|
||||
if (firstMsgIdx !== -1)
|
||||
btnStringArr.push(truncate(allThreads[threadId].messages[firstMsgIdx].displayContent ?? ''))
|
||||
else
|
||||
btnStringArr.push('""')
|
||||
? <div key="nothreads" className="text-center text-void-fg-3 brightness-90 text-sm">{`No history found`}</div>
|
||||
|
||||
const secondMsgIdx = allThreads[threadId].messages.findIndex((msg, i) => msg.role !== 'system' && !!msg.displayContent && i > firstMsgIdx) ?? ''
|
||||
if (secondMsgIdx !== -1)
|
||||
btnStringArr.push(truncate(allThreads[threadId].messages[secondMsgIdx].displayContent ?? ''))
|
||||
: sortedThreadIds.map((threadId) => {
|
||||
if (!allThreads) {
|
||||
return <li key="error" className="text-void-warning">{`No history found`}</li>;
|
||||
}
|
||||
|
||||
const numMessagesRemaining = allThreads[threadId].messages.filter((msg, i) => msg.role !== 'system' && !!msg.displayContent && i > secondMsgIdx).length
|
||||
if (numMessagesRemaining > 0)
|
||||
btnStringArr.push(numMessagesRemaining + '')
|
||||
const pastThread = allThreads[threadId];
|
||||
let firstMsg = null;
|
||||
// let secondMsg = null;
|
||||
|
||||
const btnString = btnStringArr.join(' / ')
|
||||
const firstMsgIdx = pastThread.messages.findIndex(
|
||||
(msg) => msg.role !== 'system' && !!msg.displayContent
|
||||
);
|
||||
|
||||
return (
|
||||
<button
|
||||
key={pastThread.id}
|
||||
className={`rounded-sm`}
|
||||
onClick={() => threadsStateService.switchToThread(pastThread.id)}
|
||||
title={new Date(pastThread.createdAt).toLocaleString()}
|
||||
>
|
||||
{btnString}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
if (firstMsgIdx !== -1) {
|
||||
// firstMsg = truncate(pastThread.messages[firstMsgIdx].displayContent ?? '');
|
||||
firstMsg = pastThread.messages[firstMsgIdx].displayContent ?? '';
|
||||
} else {
|
||||
firstMsg = '""';
|
||||
}
|
||||
|
||||
// const secondMsgIdx = pastThread.messages.findIndex(
|
||||
// (msg, i) => msg.role !== 'system' && !!msg.displayContent && i > firstMsgIdx
|
||||
// );
|
||||
|
||||
// if (secondMsgIdx !== -1) {
|
||||
// secondMsg = truncate(pastThread.messages[secondMsgIdx].displayContent ?? '');
|
||||
// }
|
||||
|
||||
const numMessages = pastThread.messages.filter(
|
||||
(msg) => msg.role !== 'system'
|
||||
).length;
|
||||
|
||||
return (
|
||||
<li key={pastThread.id}>
|
||||
<button
|
||||
type='button'
|
||||
className={`
|
||||
hover:bg-void-bg-1
|
||||
${threadsState._currentThreadId === pastThread.id ? 'bg-void-bg-1' : ''}
|
||||
rounded-sm px-2 py-1
|
||||
w-full
|
||||
text-left
|
||||
flex items-center
|
||||
`}
|
||||
onClick={() => threadsStateService.switchToThread(pastThread.id)}
|
||||
title={new Date(pastThread.createdAt).toLocaleString()}
|
||||
>
|
||||
<div className='truncate'>{`${firstMsg}`}</div>
|
||||
<div>{`\u00A0(${numMessages})`}</div>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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 { mountFnGenerator } from '../util/mountFnGenerator.js'
|
||||
import { Sidebar } from './Sidebar.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.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
|
|
@ -20,6 +20,16 @@
|
|||
|
||||
|
||||
|
||||
.inherit-bg-all-restyle > * {
|
||||
background-color: inherit !important;
|
||||
}
|
||||
|
||||
|
||||
.bg-editor-style-override {
|
||||
--vscode-sideBar-background: var(--vscode-editor-background);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* html {
|
||||
font-size: var(--vscode-font-size);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
/*------------------------------------------------------------------------------------------
|
||||
* 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, { useCallback, useEffect, useRef } from 'react';
|
||||
import React, { forwardRef, MutableRefObject, useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
|
||||
import { IInputBoxStyles, InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js';
|
||||
import { defaultCheckboxStyles, defaultInputBoxStyles, defaultSelectBoxStyles } from '../../../../../../../platform/theme/browser/defaultStyles.js';
|
||||
import { SelectBox } from '../../../../../../../base/browser/ui/selectBox/selectBox.js';
|
||||
|
|
@ -12,6 +12,9 @@ import { Checkbox } from '../../../../../../../base/browser/ui/toggle/toggle.js'
|
|||
|
||||
import { CodeEditorWidget } from '../../../../../../../editor/browser/widget/codeEditor/codeEditorWidget.js'
|
||||
import { useAccessor } from './services.js';
|
||||
import { ITextModel } from '../../../../../../../editor/common/model.js';
|
||||
import { asCssVariable } from '../../../../../../../platform/theme/common/colorUtils.js';
|
||||
import { inputBackground, inputForeground } from '../../../../../../../platform/theme/common/colorRegistry.js';
|
||||
|
||||
|
||||
// type guard
|
||||
|
|
@ -45,8 +48,104 @@ export const WidgetComponent = <CtorParams extends any[], Instance>({ ctor, prop
|
|||
}
|
||||
|
||||
|
||||
export type TextAreaFns = { setValue: (v: string) => void, enable: () => void, disable: () => void }
|
||||
type InputBox2Props = {
|
||||
initValue?: string | null;
|
||||
placeholder: string;
|
||||
multiline: boolean;
|
||||
fnsRef?: { current: null | TextAreaFns };
|
||||
onChangeText?: (value: string) => void;
|
||||
onKeyDown?: (e: React.KeyboardEvent<HTMLTextAreaElement>) => void;
|
||||
onChangeHeight?: (newHeight: number) => void;
|
||||
}
|
||||
export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(function X({ initValue, placeholder, multiline, fnsRef, onKeyDown, onChangeText }, ref) {
|
||||
|
||||
export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, placeholder, multiline, styles }: {
|
||||
// mirrors whatever is in ref
|
||||
const textAreaRef = useRef<HTMLTextAreaElement | null>(null)
|
||||
const [isEnabled, setEnabled] = useState(true)
|
||||
|
||||
const adjustHeight = useCallback(() => {
|
||||
const r = textAreaRef.current
|
||||
if (!r) return
|
||||
|
||||
r.style.height = 'auto' // set to auto to reset height, then set to new height
|
||||
|
||||
if (r.scrollHeight === 0) return requestAnimationFrame(adjustHeight)
|
||||
const h = r.scrollHeight
|
||||
const newHeight = Math.min(h, 500)
|
||||
r.style.height = `${newHeight}px`
|
||||
}, []);
|
||||
|
||||
|
||||
|
||||
const fns: TextAreaFns = useMemo(() => ({
|
||||
setValue: (val) => {
|
||||
const r = textAreaRef.current
|
||||
if (!r) return
|
||||
r.value = val
|
||||
onChangeText?.(r.value)
|
||||
adjustHeight()
|
||||
},
|
||||
enable: () => { setEnabled(true) },
|
||||
disable: () => { setEnabled(false) },
|
||||
}), [onChangeText, adjustHeight])
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (initValue)
|
||||
fns.setValue(initValue)
|
||||
}, [initValue])
|
||||
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<textarea
|
||||
ref={useCallback((r: HTMLTextAreaElement | null) => {
|
||||
if (fnsRef)
|
||||
fnsRef.current = fns
|
||||
|
||||
textAreaRef.current = r
|
||||
if (typeof ref === 'function') ref(r)
|
||||
else if (ref) ref.current = r
|
||||
adjustHeight()
|
||||
}, [fnsRef, fns, setEnabled, adjustHeight, ref])}
|
||||
|
||||
disabled={!isEnabled}
|
||||
|
||||
className="w-full resize-none max-h-[500px] overflow-y-auto"
|
||||
style={{
|
||||
// defaultInputBoxStyles
|
||||
background: asCssVariable(inputBackground),
|
||||
color: asCssVariable(inputForeground)
|
||||
// inputBorder: asCssVariable(inputBorder),
|
||||
}}
|
||||
|
||||
onChange={useCallback(() => {
|
||||
const r = textAreaRef.current
|
||||
if (!r) return
|
||||
onChangeText?.(r.value)
|
||||
adjustHeight()
|
||||
}, [onChangeText, adjustHeight])}
|
||||
|
||||
onKeyDown={useCallback((e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
// Shift + Enter when multiline = newline
|
||||
const shouldAddNewline = e.shiftKey && multiline
|
||||
if (!shouldAddNewline) e.preventDefault(); // prevent newline from being created
|
||||
}
|
||||
onKeyDown?.(e)
|
||||
}, [onKeyDown, multiline])}
|
||||
|
||||
rows={1}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
)
|
||||
|
||||
})
|
||||
|
||||
export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, placeholder, multiline }: {
|
||||
onChangeText: (value: string) => void;
|
||||
styles?: Partial<IInputBoxStyles>,
|
||||
onCreateInstance?: (instance: InputBox) => void | IDisposable[];
|
||||
|
|
@ -60,6 +159,10 @@ export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, plac
|
|||
const contextViewProvider = accessor.get('IContextViewService')
|
||||
return <WidgetComponent
|
||||
ctor={InputBox}
|
||||
className='
|
||||
bg-void-bg-1
|
||||
@@[&_::placeholder]:!void-text-void-fg-3
|
||||
'
|
||||
propsFn={useCallback((container) => [
|
||||
container,
|
||||
contextViewProvider,
|
||||
|
|
@ -69,7 +172,6 @@ export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, plac
|
|||
inputForeground: "var(--vscode-foreground)",
|
||||
// inputBackground: 'transparent',
|
||||
// inputBorder: 'none',
|
||||
...styles,
|
||||
},
|
||||
placeholder,
|
||||
tooltip: '',
|
||||
|
|
@ -193,11 +295,223 @@ export const VoidCheckBox = ({ label, value, onClick, className }: { label: stri
|
|||
}
|
||||
|
||||
|
||||
export const VoidSelectBox = <T,>({ onChangeSelection, onCreateInstance, selectBoxRef, options }: {
|
||||
export const VoidCustomSelectBox = <T extends any>({
|
||||
options,
|
||||
selectedOption: selectedOption_,
|
||||
onChangeOption,
|
||||
getOptionName,
|
||||
getOptionsEqual,
|
||||
className,
|
||||
arrowTouchesText = true,
|
||||
matchInputWidth = false,
|
||||
isMenuPositionFixed = true,
|
||||
gap = 0,
|
||||
}: {
|
||||
options: T[];
|
||||
selectedOption?: T;
|
||||
onChangeOption: (newValue: T) => void;
|
||||
getOptionName: (option: T) => string;
|
||||
getOptionsEqual: (a: T, b: T) => boolean;
|
||||
className?: string;
|
||||
arrowTouchesText?: boolean;
|
||||
matchInputWidth?: boolean;
|
||||
isMenuPositionFixed?: boolean;
|
||||
gap?: number;
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [readyToShow, setReadyToShow] = useState(false);
|
||||
const [position, setPosition] = useState({ top: 0, left: 0, width: 0 });
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const buttonRef = useRef<HTMLButtonElement | null>(null);
|
||||
const measureRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
|
||||
// if the selected option is null, use the 0th option as the selected, and set the option to options[0]
|
||||
useEffect(() => {
|
||||
if (!options[0]) return
|
||||
if (!selectedOption_) {
|
||||
onChangeOption(options[0]);
|
||||
}
|
||||
}, [selectedOption_, options])
|
||||
const selectedOption = !selectedOption_ ? options[0] : selectedOption_
|
||||
|
||||
|
||||
const updatePosition = useCallback(() => {
|
||||
if (!buttonRef.current || !containerRef.current || !measureRef.current) return;
|
||||
|
||||
const buttonRect = buttonRef.current.getBoundingClientRect();
|
||||
const containerRect = containerRef.current.getBoundingClientRect();
|
||||
const containerWidth = containerRef.current.offsetWidth;
|
||||
const viewportHeight = window.innerHeight;
|
||||
const spaceBelow = viewportHeight - buttonRect.bottom;
|
||||
const spaceNeeded = options.length * 28;
|
||||
const showAbove = spaceBelow < spaceNeeded && buttonRect.top > spaceBelow;
|
||||
|
||||
// Calculate the menu width
|
||||
let menuWidth = matchInputWidth ? containerWidth : buttonRect.width;
|
||||
|
||||
// If not matchInputWidth, calculate content width from measurement div
|
||||
if (!matchInputWidth) {
|
||||
const contentWidth = measureRef.current.offsetWidth;
|
||||
menuWidth = Math.max(buttonRect.width, contentWidth);
|
||||
}
|
||||
|
||||
if (isMenuPositionFixed) {
|
||||
// Fixed positioning (relative to viewport)
|
||||
setPosition({
|
||||
top: showAbove
|
||||
? buttonRect.top - spaceNeeded
|
||||
: buttonRect.bottom + gap,
|
||||
left: buttonRect.left,
|
||||
width: menuWidth,
|
||||
});
|
||||
} else {
|
||||
// Absolute positioning (relative to parent container)
|
||||
setPosition({
|
||||
top: showAbove
|
||||
? -(spaceNeeded + gap)
|
||||
: buttonRect.height + gap,
|
||||
left: 0,
|
||||
width: menuWidth,
|
||||
});
|
||||
}
|
||||
|
||||
setReadyToShow(true);
|
||||
}, [gap, matchInputWidth, options.length, isMenuPositionFixed]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setReadyToShow(false);
|
||||
updatePosition();
|
||||
window.addEventListener('scroll', updatePosition, true);
|
||||
window.addEventListener('resize', updatePosition);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('scroll', updatePosition, true);
|
||||
window.removeEventListener('resize', updatePosition);
|
||||
};
|
||||
} else {
|
||||
setReadyToShow(false);
|
||||
}
|
||||
}, [isOpen, updatePosition]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (isOpen) {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={`inline-block relative ${className}`}
|
||||
>
|
||||
{/* Hidden measurement div */}
|
||||
<div
|
||||
ref={measureRef}
|
||||
className="opacity-0 pointer-events-none absolute -left-[999999px] -top-[999999px] flex flex-col"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{options.map((option) => (
|
||||
<div key={getOptionName(option)} className="flex items-center whitespace-nowrap">
|
||||
<div className="w-4" />
|
||||
<span className="px-2">{getOptionName(option)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Select Button */}
|
||||
<button
|
||||
type='button'
|
||||
ref={buttonRef}
|
||||
className="flex items-center h-4 bg-transparent whitespace-nowrap hover:brightness-90 w-full"
|
||||
onClick={() => {
|
||||
setIsOpen(!isOpen);
|
||||
}}
|
||||
>
|
||||
<span className={`max-w-[120px] truncate ${arrowTouchesText ? 'mr-1' : ''}`}>
|
||||
{getOptionName(selectedOption)}
|
||||
</span>
|
||||
<svg
|
||||
className={`size-3 flex-shrink-0 ${arrowTouchesText ? '' : 'ml-auto'}`}
|
||||
viewBox="0 0 12 12"
|
||||
fill="none"
|
||||
>
|
||||
<path
|
||||
d="M2.5 4.5L6 8L9.5 4.5"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Dropdown Menu */}
|
||||
{isOpen && readyToShow && (
|
||||
<div
|
||||
className={`${isMenuPositionFixed ? 'fixed' : 'absolute'} z-10 bg-void-bg-1 border-void-border-1 border overflow-hidden rounded shadow-lg`}
|
||||
style={{
|
||||
top: position.top,
|
||||
left: position.left,
|
||||
width: position.width,
|
||||
}}
|
||||
>
|
||||
{options.map((option) => {
|
||||
const thisOptionIsSelected = getOptionsEqual(option, selectedOption);
|
||||
const optionName = getOptionName(option);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={optionName}
|
||||
className={`flex items-center px-2 py-1 cursor-pointer whitespace-nowrap
|
||||
transition-all duration-100
|
||||
bg-void-bg-1
|
||||
${thisOptionIsSelected ? 'bg-void-bg-2' : 'hover:bg-void-bg-2'}
|
||||
`}
|
||||
onClick={() => {
|
||||
onChangeOption(option);
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
<div className="w-4 flex justify-center flex-shrink-0">
|
||||
{thisOptionIsSelected && (
|
||||
<svg className="size-3" viewBox="0 0 12 12" fill="none">
|
||||
<path
|
||||
d="M10 3L4.5 8.5L2 6"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<span>{optionName}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
export const _VoidSelectBox = <T,>({ onChangeSelection, onCreateInstance, selectBoxRef, options, className }: {
|
||||
onChangeSelection: (value: T) => void;
|
||||
onCreateInstance?: ((instance: SelectBox) => void | IDisposable[]);
|
||||
selectBoxRef?: React.MutableRefObject<SelectBox | null>;
|
||||
options: readonly { text: string, value: T }[];
|
||||
className?: string;
|
||||
}) => {
|
||||
const accessor = useAccessor()
|
||||
const contextViewProvider = accessor.get('IContextViewService')
|
||||
|
|
@ -205,7 +519,13 @@ export const VoidSelectBox = <T,>({ onChangeSelection, onCreateInstance, selectB
|
|||
let containerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
return <WidgetComponent
|
||||
className='@@select-child-restyle'
|
||||
className={`
|
||||
@@select-child-restyle
|
||||
@@[&_select]:!void-text-void-fg-3
|
||||
@@[&_select]:!void-text-xs
|
||||
!text-void-fg-3
|
||||
${className ?? ''}
|
||||
`}
|
||||
ctor={SelectBox}
|
||||
propsFn={useCallback((container) => {
|
||||
containerRef.current = container
|
||||
|
|
@ -214,14 +534,15 @@ export const VoidSelectBox = <T,>({ onChangeSelection, onCreateInstance, selectB
|
|||
options.map(opt => ({ text: opt.text })),
|
||||
defaultIndex,
|
||||
contextViewProvider,
|
||||
defaultSelectBoxStyles
|
||||
defaultSelectBoxStyles,
|
||||
] as const;
|
||||
}, [containerRef, options, contextViewProvider])}
|
||||
}, [containerRef, options])}
|
||||
|
||||
dispose={useCallback((instance: SelectBox) => {
|
||||
instance.dispose();
|
||||
for (let child of containerRef.current?.childNodes ?? [])
|
||||
containerRef.current?.childNodes.forEach(child => {
|
||||
containerRef.current?.removeChild(child)
|
||||
})
|
||||
}, [containerRef])}
|
||||
|
||||
onCreateInstance={useCallback((instance: SelectBox) => {
|
||||
|
|
@ -286,24 +607,48 @@ const normalizeIndentation = (code: string): string => {
|
|||
|
||||
}
|
||||
|
||||
export const VoidCodeEditor = ({ initValue, language }: { initValue: string, language: string | undefined }) => {
|
||||
|
||||
const MAX_HEIGHT = Infinity;
|
||||
const modelOfEditorId: { [id: string]: ITextModel | undefined } = {}
|
||||
export type VoidCodeEditorProps = { initValue: string, language?: string, maxHeight?: number, showScrollbars?: boolean }
|
||||
export const VoidCodeEditor = ({ initValue, language, maxHeight, showScrollbars }: VoidCodeEditorProps) => {
|
||||
|
||||
initValue = normalizeIndentation(initValue)
|
||||
|
||||
// default settings
|
||||
const MAX_HEIGHT = maxHeight ?? Infinity;
|
||||
const SHOW_SCROLLBARS = showScrollbars ?? false;
|
||||
|
||||
const divRef = useRef<HTMLDivElement | null>(null)
|
||||
|
||||
const accessor = useAccessor()
|
||||
const instantiationService = accessor.get('IInstantiationService')
|
||||
// const languageDetectionService = accessor.get('ILanguageDetectionService')
|
||||
const modelService = accessor.get('IModelService')
|
||||
const languageDetectionService = accessor.get('ILanguageDetectionService')
|
||||
|
||||
initValue = normalizeIndentation(initValue)
|
||||
|
||||
return <div ref={divRef}>
|
||||
const id = useId()
|
||||
|
||||
// these are used to pass to the model creation of modelRef
|
||||
const initValueRef = useRef(initValue)
|
||||
const languageRef = useRef(language)
|
||||
|
||||
const modelRef = useRef<ITextModel | null>(null)
|
||||
|
||||
// if we change the initial value, don't re-render the whole thing, just set it here. same for language
|
||||
useEffect(() => {
|
||||
initValueRef.current = initValue
|
||||
modelRef.current?.setValue(initValue)
|
||||
}, [initValue])
|
||||
useEffect(() => {
|
||||
languageRef.current = language
|
||||
if (language) modelRef.current?.setLanguage(language)
|
||||
}, [language])
|
||||
|
||||
return <div ref={divRef} className='relative z-0 px-2 py-1 bg-void-bg-3'>
|
||||
<WidgetComponent
|
||||
className='relative z-0 text-sm bg-vscode-editor-bg'
|
||||
ctor={useCallback((container) =>
|
||||
instantiationService.createInstance(
|
||||
className='@@bg-editor-style-override' // text-sm
|
||||
ctor={useCallback((container) => {
|
||||
return instantiationService.createInstance(
|
||||
CodeEditorWidget,
|
||||
container,
|
||||
{
|
||||
|
|
@ -312,10 +657,19 @@ export const VoidCodeEditor = ({ initValue, language }: { initValue: string, lan
|
|||
|
||||
scrollbar: {
|
||||
alwaysConsumeMouseWheel: false,
|
||||
vertical: 'hidden',
|
||||
horizontal: 'hidden',
|
||||
verticalScrollbarSize: 0,
|
||||
horizontalScrollbarSize: 0,
|
||||
...SHOW_SCROLLBARS ? {
|
||||
vertical: 'auto',
|
||||
verticalScrollbarSize: 8,
|
||||
horizontal: 'auto',
|
||||
horizontalScrollbarSize: 8,
|
||||
} : {
|
||||
vertical: 'hidden',
|
||||
verticalScrollbarSize: 0,
|
||||
horizontal: 'auto',
|
||||
horizontalScrollbarSize: 8,
|
||||
ignoreHorizontalScrollbarInContentHeight: true,
|
||||
|
||||
},
|
||||
},
|
||||
scrollBeyondLastLine: false,
|
||||
|
||||
|
|
@ -347,26 +701,23 @@ export const VoidCodeEditor = ({ initValue, language }: { initValue: string, lan
|
|||
{
|
||||
isSimpleWidget: true,
|
||||
})
|
||||
, [instantiationService])
|
||||
}
|
||||
}, [instantiationService])}
|
||||
|
||||
onCreateInstance={useCallback((editor: CodeEditorWidget) => {
|
||||
const model = modelService.createModel(
|
||||
initValue,
|
||||
language ? {
|
||||
languageId: language,
|
||||
onDidChange: () => ({
|
||||
dispose: () => { }
|
||||
})
|
||||
} : null
|
||||
);
|
||||
const model = modelOfEditorId[id] ?? modelService.createModel(
|
||||
initValueRef.current, {
|
||||
languageId: languageRef.current ? languageRef.current : '',
|
||||
onDidChange: (e) => { return { dispose: () => { } } } // no idea why they'd require this
|
||||
})
|
||||
modelRef.current = model
|
||||
editor.setModel(model);
|
||||
|
||||
const container = editor.getDomNode()
|
||||
const parentNode = container?.parentElement
|
||||
const resize = () => {
|
||||
const height = editor.getScrollHeight() + 1
|
||||
if (parentNode) {
|
||||
const height = Math.min(editor.getScrollHeight() + 1, MAX_HEIGHT);
|
||||
// const height = Math.min(, MAX_HEIGHT);
|
||||
parentNode.style.height = `${height}px`;
|
||||
editor.layout();
|
||||
}
|
||||
|
|
@ -375,12 +726,12 @@ export const VoidCodeEditor = ({ initValue, language }: { initValue: string, lan
|
|||
resize()
|
||||
const disposable = editor.onDidContentSizeChange(() => { resize() });
|
||||
|
||||
return [disposable]
|
||||
}, [modelService, initValue, language])}
|
||||
return [disposable, model]
|
||||
}, [modelService])}
|
||||
|
||||
dispose={useCallback((editor: CodeEditorWidget) => {
|
||||
editor.dispose();
|
||||
}, [modelService, languageDetectionService])}
|
||||
}, [modelService])}
|
||||
|
||||
propsFn={useCallback(() => { return [] }, [])}
|
||||
/>
|
||||
|
|
@ -389,6 +740,13 @@ export const VoidCodeEditor = ({ initValue, language }: { initValue: string, lan
|
|||
}
|
||||
|
||||
|
||||
export const VoidButton = ({ children, disabled, onClick }: { children: React.ReactNode; disabled?: boolean; onClick: () => void }) => {
|
||||
return <button disabled={disabled}
|
||||
className='px-3 py-1 bg-black/10 dark:bg-gray-200/10 rounded-sm overflow-hidden'
|
||||
onClick={onClick}
|
||||
>{children}</button>
|
||||
}
|
||||
|
||||
// export const VoidScrollableElt = ({ options, children }: { options: ScrollableElementCreationOptions, children: React.ReactNode }) => {
|
||||
// const instanceRef = useRef<DomScrollableElement | null>(null);
|
||||
// const [childrenPortal, setChildrenPortal] = useState<React.ReactNode | null>(null)
|
||||
|
|
|
|||
|
|
@ -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 * as ReactDOM from 'react-dom/client'
|
||||
|
|
|
|||
|
|
@ -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, { useState, useEffect } from 'react'
|
||||
import { ThreadsState } from '../../../threadHistoryService.js'
|
||||
|
|
@ -40,7 +40,11 @@ import { IAccessibilityService } from '../../../../../../../platform/accessibili
|
|||
import { ILanguageConfigurationService } from '../../../../../../../editor/common/languages/languageConfigurationRegistry.js'
|
||||
import { ILanguageFeaturesService } from '../../../../../../../editor/common/services/languageFeatures.js'
|
||||
import { ILanguageDetectionService } from '../../../../../../services/languageDetection/common/languageDetectionWorkerService.js'
|
||||
|
||||
import { IKeybindingService } from '../../../../../../../platform/keybinding/common/keybinding.js'
|
||||
import { IEnvironmentService } from '../../../../../../../platform/environment/common/environment.js'
|
||||
import { IConfigurationService } from '../../../../../../../platform/configuration/common/configuration.js'
|
||||
import { IPathService } from '../../../../../../../workbench/services/path/common/pathService.js'
|
||||
import { IMetricsService } from '../../../../../../../platform/void/common/metricsService.js'
|
||||
|
||||
|
||||
// normally to do this you'd use a useEffect that calls .onDidChangeState(), but useEffect mounts too late and misses initial state changes
|
||||
|
|
@ -173,6 +177,12 @@ const getReactAccessor = (accessor: ServicesAccessor) => {
|
|||
ILanguageConfigurationService: accessor.get(ILanguageConfigurationService),
|
||||
ILanguageDetectionService: accessor.get(ILanguageDetectionService),
|
||||
ILanguageFeaturesService: accessor.get(ILanguageFeaturesService),
|
||||
IKeybindingService: accessor.get(IKeybindingService),
|
||||
|
||||
IEnvironmentService: accessor.get(IEnvironmentService),
|
||||
IConfigurationService: accessor.get(IConfigurationService),
|
||||
IPathService: accessor.get(IPathService),
|
||||
IMetricsService: accessor.get(IMetricsService),
|
||||
|
||||
} as const
|
||||
return reactAccessor
|
||||
|
|
|
|||
|
|
@ -0,0 +1,101 @@
|
|||
import { useEffect } from 'react';
|
||||
|
||||
export const useScrollbarStyles = (containerRef: React.MutableRefObject<HTMLDivElement | null>) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
// Create selector for specific overflow classes
|
||||
const overflowSelector = [
|
||||
'[class*="overflow-auto"]',
|
||||
'[class*="overflow-x-auto"]',
|
||||
'[class*="overflow-y-auto"]'
|
||||
].join(',');
|
||||
|
||||
// Get all matching elements within the container, including the container itself
|
||||
const scrollElements = [
|
||||
...(containerRef.current.matches(overflowSelector) ? [containerRef.current] : []),
|
||||
...Array.from(containerRef.current.querySelectorAll(overflowSelector))
|
||||
];
|
||||
|
||||
// Apply styles and listeners to each scroll element
|
||||
scrollElements.forEach(element => {
|
||||
// Add the scrollable class directly to the overflow element
|
||||
element.classList.add('void-scrollable-element');
|
||||
|
||||
let fadeTimeout: NodeJS.Timeout | null = null;
|
||||
let fadeInterval: NodeJS.Timeout | null = null;
|
||||
|
||||
const fadeIn = () => {
|
||||
if (fadeInterval) clearInterval(fadeInterval);
|
||||
|
||||
let step = 0;
|
||||
fadeInterval = setInterval(() => {
|
||||
if (step <= 10) {
|
||||
element.classList.remove(`show-scrollbar-${step - 1}`);
|
||||
element.classList.add(`show-scrollbar-${step}`);
|
||||
step++;
|
||||
} else {
|
||||
clearInterval(fadeInterval!);
|
||||
}
|
||||
}, 10);
|
||||
};
|
||||
|
||||
const fadeOut = () => {
|
||||
if (fadeInterval) clearInterval(fadeInterval);
|
||||
|
||||
let step = 10;
|
||||
fadeInterval = setInterval(() => {
|
||||
if (step >= 0) {
|
||||
element.classList.remove(`show-scrollbar-${step + 1}`);
|
||||
element.classList.add(`show-scrollbar-${step}`);
|
||||
step--;
|
||||
} else {
|
||||
clearInterval(fadeInterval!);
|
||||
}
|
||||
}, 60);
|
||||
};
|
||||
|
||||
const onMouseEnter = () => {
|
||||
if (fadeTimeout) clearTimeout(fadeTimeout);
|
||||
if (fadeInterval) clearInterval(fadeInterval);
|
||||
fadeIn();
|
||||
};
|
||||
|
||||
const onMouseLeave = () => {
|
||||
if (fadeTimeout) clearTimeout(fadeTimeout);
|
||||
fadeTimeout = setTimeout(() => {
|
||||
fadeOut();
|
||||
}, 10);
|
||||
};
|
||||
|
||||
element.addEventListener('mouseenter', onMouseEnter);
|
||||
element.addEventListener('mouseleave', onMouseLeave);
|
||||
|
||||
// Store cleanup function
|
||||
const cleanup = () => {
|
||||
element.removeEventListener('mouseenter', onMouseEnter);
|
||||
element.removeEventListener('mouseleave', onMouseLeave);
|
||||
if (fadeTimeout) clearTimeout(fadeTimeout);
|
||||
if (fadeInterval) clearInterval(fadeInterval);
|
||||
element.classList.remove('void-scrollable-element');
|
||||
// Remove any remaining show-scrollbar classes
|
||||
for (let i = 0; i <= 10; i++) {
|
||||
element.classList.remove(`show-scrollbar-${i}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Store the cleanup function on the element for later use
|
||||
(element as any).__scrollbarCleanup = cleanup;
|
||||
});
|
||||
|
||||
return () => {
|
||||
// Clean up all scroll elements
|
||||
scrollElements.forEach(element => {
|
||||
if ((element as any).__scrollbarCleanup) {
|
||||
(element as any).__scrollbarCleanup();
|
||||
}
|
||||
});
|
||||
};
|
||||
}, [containerRef]);
|
||||
};
|
||||
|
|
@ -1,15 +1,15 @@
|
|||
/*------------------------------------------------------------------------------------------
|
||||
* 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 { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { FeatureName, featureNames, ModelSelection, modelSelectionsEqual, ProviderName, providerNames } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
|
||||
import { useSettingsState, useRefreshModelState, useAccessor } from '../util/services.js'
|
||||
import { VoidSelectBox } from '../util/inputs.js'
|
||||
import { _VoidSelectBox, VoidCustomSelectBox } from '../util/inputs.js'
|
||||
import { SelectBox } from '../../../../../../../base/browser/ui/selectBox/selectBox.js'
|
||||
import { IconWarning } from '../sidebar-tsx/SidebarChat.js'
|
||||
import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js'
|
||||
import { VOID_OPEN_SETTINGS_ACTION_ID, VOID_TOGGLE_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js'
|
||||
import { ModelOption } from '../../../../../../../platform/void/common/voidSettingsService.js'
|
||||
|
||||
|
||||
|
|
@ -17,42 +17,63 @@ import { ModelOption } from '../../../../../../../platform/void/common/voidSetti
|
|||
const optionsEqual = (m1: ModelOption[], m2: ModelOption[]) => {
|
||||
if (m1.length !== m2.length) return false
|
||||
for (let i = 0; i < m1.length; i++) {
|
||||
if (!modelSelectionsEqual(m1[i].value, m2[i].value)) return false
|
||||
if (!modelSelectionsEqual(m1[i].selection, m2[i].selection)) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
|
||||
const ModelSelectBox = ({ options, featureName }: { options: ModelOption[], featureName: FeatureName }) => {
|
||||
const accessor = useAccessor()
|
||||
|
||||
const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
|
||||
let weChangedText = false
|
||||
const selection = voidSettingsService.state.modelSelectionOfFeature[featureName]
|
||||
const selectedOption = selection ? voidSettingsService.state._modelOptions.find(v => modelSelectionsEqual(v.selection, selection)) : options[0]
|
||||
|
||||
return <VoidSelectBox
|
||||
const onChangeOption = useCallback((newOption: ModelOption) => {
|
||||
voidSettingsService.setModelSelectionOfFeature(featureName, newOption.selection)
|
||||
}, [voidSettingsService, featureName])
|
||||
|
||||
return <VoidCustomSelectBox
|
||||
options={options}
|
||||
onChangeSelection={useCallback((newVal: ModelSelection) => {
|
||||
if (weChangedText) return
|
||||
voidSettingsService.setModelSelectionOfFeature(featureName, newVal)
|
||||
}, [voidSettingsService, featureName])}
|
||||
// we are responsible for setting the initial state here. always sync instance when state changes.
|
||||
onCreateInstance={useCallback((instance: SelectBox) => {
|
||||
const syncInstance = () => {
|
||||
const modelsListRef = voidSettingsService.state._modelOptions // as a ref
|
||||
const settingsAtProvider = voidSettingsService.state.modelSelectionOfFeature[featureName]
|
||||
const selectionIdx = settingsAtProvider === null ? -1 : modelsListRef.findIndex(v => modelSelectionsEqual(v.value, settingsAtProvider))
|
||||
weChangedText = true
|
||||
instance.select(selectionIdx === -1 ? 0 : selectionIdx)
|
||||
weChangedText = false
|
||||
}
|
||||
syncInstance()
|
||||
const disposable = voidSettingsService.onDidChangeState(syncInstance)
|
||||
return [disposable]
|
||||
}, [voidSettingsService, featureName])}
|
||||
selectedOption={selectedOption}
|
||||
onChangeOption={onChangeOption}
|
||||
getOptionName={(option) => option.name}
|
||||
getOptionsEqual={(a, b) => optionsEqual([a], [b])}
|
||||
className={`text-xs text-void-fg-3 px-1`}
|
||||
matchInputWidth={false}
|
||||
isMenuPositionFixed={featureName === 'Ctrl+K' ? false : true}
|
||||
/>
|
||||
}
|
||||
// const ModelSelectBox = ({ options, featureName }: { options: ModelOption[], featureName: FeatureName }) => {
|
||||
// const accessor = useAccessor()
|
||||
|
||||
// const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
|
||||
// let weChangedText = false
|
||||
|
||||
// return <VoidSelectBox
|
||||
// className='@@[&_select]:!void-text-xs text-void-fg-3'
|
||||
// options={options}
|
||||
// onChangeSelection={useCallback((newVal: ModelSelection) => {
|
||||
// if (weChangedText) return
|
||||
// voidSettingsService.setModelSelectionOfFeature(featureName, newVal)
|
||||
// }, [voidSettingsService, featureName])}
|
||||
// // we are responsible for setting the initial state here. always sync instance when state changes.
|
||||
// onCreateInstance={useCallback((instance: SelectBox) => {
|
||||
// const syncInstance = () => {
|
||||
// const modelsListRef = voidSettingsService.state._modelOptions // as a ref
|
||||
// const settingsAtProvider = voidSettingsService.state.modelSelectionOfFeature[featureName]
|
||||
// const selectionIdx = settingsAtProvider === null ? -1 : modelsListRef.findIndex(v => modelSelectionsEqual(v.value, settingsAtProvider))
|
||||
// weChangedText = true
|
||||
// instance.select(selectionIdx === -1 ? 0 : selectionIdx)
|
||||
// weChangedText = false
|
||||
// }
|
||||
// syncInstance()
|
||||
// const disposable = voidSettingsService.onDidChangeState(syncInstance)
|
||||
// return [disposable]
|
||||
// }, [voidSettingsService, featureName])}
|
||||
// />
|
||||
// }
|
||||
|
||||
const MemoizedModelSelectBox = ({ featureName }: { featureName: FeatureName }) => {
|
||||
const settingsState = useSettingsState()
|
||||
|
|
@ -71,30 +92,23 @@ const MemoizedModelSelectBox = ({ featureName }: { featureName: FeatureName }) =
|
|||
|
||||
}
|
||||
|
||||
const DummySelectBox = () => {
|
||||
|
||||
const accessor = useAccessor()
|
||||
const comandService = accessor.get('ICommandService')
|
||||
|
||||
const openSettings = () => {
|
||||
comandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID);
|
||||
};
|
||||
export const WarningBox = ({ text, onClick, className }: { text: string; onClick?: () => void; className?: string }) => {
|
||||
|
||||
return <div
|
||||
className={`
|
||||
flex items-center
|
||||
flex-nowrap text-ellipsis
|
||||
text-vscode-charts-yellow
|
||||
hover:brightness-90 transition-all duration-200
|
||||
cursor-pointer
|
||||
text-void-warning brightness-90 opacity-90
|
||||
text-xs text-ellipsis
|
||||
${onClick ? `hover:brightness-75 transition-all duration-200 cursor-pointer` : ''}
|
||||
flex items-center flex-nowrap
|
||||
${className}
|
||||
`}
|
||||
onClick={openSettings}
|
||||
onClick={onClick}
|
||||
>
|
||||
<IconWarning
|
||||
size={20}
|
||||
className='mr-1 brightness-90'
|
||||
size={14}
|
||||
className='mr-1'
|
||||
/>
|
||||
<span>Model required</span>
|
||||
<span>{text}</span>
|
||||
</div>
|
||||
// return <VoidSelectBox
|
||||
// options={[{ text: 'Please add a model!', value: null }]}
|
||||
|
|
@ -104,7 +118,16 @@ const DummySelectBox = () => {
|
|||
|
||||
export const ModelDropdown = ({ featureName }: { featureName: FeatureName }) => {
|
||||
const settingsState = useSettingsState()
|
||||
|
||||
const accessor = useAccessor()
|
||||
const commandService = accessor.get('ICommandService')
|
||||
|
||||
const openSettings = () => { commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID); };
|
||||
|
||||
return <>
|
||||
{settingsState._modelOptions.length === 0 ? <DummySelectBox /> : <MemoizedModelSelectBox featureName={featureName} />}
|
||||
{settingsState._modelOptions.length === 0 ?
|
||||
<WarningBox onClick={openSettings} text='Provider required' />
|
||||
: <MemoizedModelSelectBox featureName={featureName} />
|
||||
}
|
||||
</>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,29 @@
|
|||
/*------------------------------------------------------------------------------------------
|
||||
* 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, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js'
|
||||
import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidModelInfo, featureFlagNames, displayInfoOfFeatureFlag, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, defaultProviderSettings, nonlocalProviderNames, localProviderNames } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
|
||||
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
|
||||
import { VoidCheckBox, VoidInputBox, VoidSelectBox, VoidSwitch } from '../util/inputs.js'
|
||||
import { VoidButton, VoidCheckBox, VoidCustomSelectBox, VoidInputBox, VoidInputBox2, VoidSwitch } from '../util/inputs.js'
|
||||
import { useAccessor, useIsDark, useRefreshModelListener, useRefreshModelState, useSettingsState } from '../util/services.js'
|
||||
import { X, RefreshCw, Loader2, Check } from 'lucide-react'
|
||||
import { X, RefreshCw, Loader2, Check, MoveRight } from 'lucide-react'
|
||||
import { useScrollbarStyles } from '../util/useScrollbarStyles.js'
|
||||
import { isWindows, isLinux, isMacintosh } from '../../../../../../../base/common/platform.js'
|
||||
import { URI } from '../../../../../../../base/common/uri.js'
|
||||
import { env } from '../../../../../../../base/common/process.js'
|
||||
import { WarningBox } from './ModelDropdown.js'
|
||||
import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js'
|
||||
|
||||
const SubtleButton = ({ onClick, text, icon, disabled }: { onClick: () => void, text: string, icon: React.ReactNode, disabled: boolean }) => {
|
||||
|
||||
return <div className='flex items-center px-3 rounded-sm overflow-hidden gap-2 hover:bg-black/10 dark:hover:bg-gray-300/10'>
|
||||
return <div className='flex items-center text-void-fg-3 mb-1 px-3 rounded-sm overflow-hidden gap-2 hover:bg-black/10 dark:hover:bg-gray-300/10'>
|
||||
<button className='flex items-center' disabled={disabled} onClick={onClick}>
|
||||
{icon}
|
||||
</button>
|
||||
<span className='opacity-50'>
|
||||
<span>
|
||||
{text}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -31,6 +36,7 @@ const RefreshModelButton = ({ providerName }: { providerName: RefreshableProvide
|
|||
|
||||
const accessor = useAccessor()
|
||||
const refreshModelService = accessor.get('IRefreshModelService')
|
||||
const metricsService = accessor.get('IMetricsService')
|
||||
|
||||
const [justFinished, setJustFinished] = useState(false)
|
||||
|
||||
|
|
@ -51,8 +57,11 @@ const RefreshModelButton = ({ providerName }: { providerName: RefreshableProvide
|
|||
|
||||
const { title: providerTitle } = displayInfoOfProviderName(providerName)
|
||||
return <SubtleButton
|
||||
onClick={() => { refreshModelService.refreshModels(providerName) }}
|
||||
text={justFinished ? `${providerTitle} Models are up-to-date!` : `Refresh Models List for ${providerTitle}.`}
|
||||
onClick={() => {
|
||||
refreshModelService.refreshModels(providerName)
|
||||
metricsService.capture('Click', { providerName, action: 'Refresh Models' })
|
||||
}}
|
||||
text={justFinished ? `${providerTitle} Models are up-to-date!` : `Manually refresh models list for ${providerTitle}.`}
|
||||
icon={isRefreshing ? <Loader2 className='size-3 animate-spin' /> : (justFinished ? <Check className='stroke-green-500 size-3' /> : <RefreshCw className='size-3' />)}
|
||||
disabled={isRefreshing || justFinished}
|
||||
/>
|
||||
|
|
@ -64,7 +73,7 @@ const RefreshableModels = () => {
|
|||
|
||||
const buttons = refreshableProviderNames.map(providerName => {
|
||||
if (!settingsState.settingsOfProvider[providerName]._enabled) return null
|
||||
return <div key={providerName} className='pb-4' >
|
||||
return <div key={providerName} className='pb-4'>
|
||||
<RefreshModelButton providerName={providerName} />
|
||||
</div>
|
||||
})
|
||||
|
|
@ -84,68 +93,73 @@ const AddModelMenu = ({ onSubmit }: { onSubmit: () => void }) => {
|
|||
|
||||
const settingsState = useSettingsState()
|
||||
|
||||
const providerNameRef = useRef<ProviderName | null>(null)
|
||||
const modelNameRef = useRef<string | null>(null)
|
||||
// const providerNameRef = useRef<ProviderName | null>(null)
|
||||
const [providerName, setProviderName] = useState<ProviderName | null>(null)
|
||||
|
||||
const modelNameRef = useRef<HTMLTextAreaElement | null>(null)
|
||||
|
||||
const [errorString, setErrorString] = useState('')
|
||||
|
||||
|
||||
const providerOptions = useMemo(() => providerNames.map(providerName => ({ text: displayInfoOfProviderName(providerName).title, value: providerName })), [providerNames])
|
||||
|
||||
return <>
|
||||
<div className='flex items-center gap-4'>
|
||||
|
||||
{/* provider */}
|
||||
<div className='max-w-40 w-full border border-vscode-editorwidget-border'>
|
||||
<VoidSelectBox
|
||||
<VoidCustomSelectBox
|
||||
options={providerNames}
|
||||
selectedOption={providerName}
|
||||
onChangeOption={(pn) => setProviderName(pn)}
|
||||
getOptionName={(pn) => pn ? displayInfoOfProviderName(pn).title : '(null)'}
|
||||
getOptionsEqual={(a, b) => a === b}
|
||||
className={`max-w-44 w-full border border-void-border-2 bg-void-bg-1 text-void-fg-3 text-root
|
||||
py-[4px] px-[6px]
|
||||
`}
|
||||
arrowTouchesText={false}
|
||||
/>
|
||||
{/* <_VoidSelectBox
|
||||
onCreateInstance={useCallback(() => { providerNameRef.current = providerOptions[0].value }, [providerOptions])} // initialize state
|
||||
onChangeSelection={useCallback((providerName: ProviderName) => { providerNameRef.current = providerName }, [])}
|
||||
options={providerOptions}
|
||||
/>
|
||||
</div>
|
||||
/> */}
|
||||
|
||||
{/* model */}
|
||||
<div className='max-w-40 w-full border border-vscode-editorwidget-border'>
|
||||
<VoidInputBox
|
||||
<div className='max-w-44 w-full border border-void-border-2 bg-void-bg-1 text-void-fg-3 text-root'>
|
||||
<VoidInputBox2
|
||||
placeholder='Model Name'
|
||||
onChangeText={useCallback((modelName) => { modelNameRef.current = modelName }, [])}
|
||||
ref={modelNameRef}
|
||||
multiline={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* button */}
|
||||
<div className='max-w-40'>
|
||||
<button
|
||||
className='px-3 py-1 bg-black/10 dark:bg-gray-200/10 rounded-sm overflow-hidden'
|
||||
onClick={() => {
|
||||
const providerName = providerNameRef.current
|
||||
const modelName = modelNameRef.current
|
||||
<VoidButton onClick={() => {
|
||||
const modelName = modelNameRef.current?.value
|
||||
|
||||
if (providerName === null) {
|
||||
setErrorString('Please select a provider.')
|
||||
return
|
||||
}
|
||||
if (!modelName) {
|
||||
setErrorString('Please enter a model name.')
|
||||
return
|
||||
}
|
||||
// if model already exists here
|
||||
if (settingsState.settingsOfProvider[providerName].models.find(m => m.modelName === modelName)) {
|
||||
setErrorString(`This model already exists under ${providerName}.`)
|
||||
return
|
||||
}
|
||||
if (providerName === null) {
|
||||
setErrorString('Please select a provider.')
|
||||
return
|
||||
}
|
||||
if (!modelName) {
|
||||
setErrorString('Please enter a model name.')
|
||||
return
|
||||
}
|
||||
// if model already exists here
|
||||
if (settingsState.settingsOfProvider[providerName].models.find(m => m.modelName === modelName)) {
|
||||
setErrorString(`This model already exists under ${providerName}.`)
|
||||
return
|
||||
}
|
||||
|
||||
settingsStateService.addModel(providerName, modelName)
|
||||
onSubmit()
|
||||
settingsStateService.addModel(providerName, modelName)
|
||||
onSubmit()
|
||||
|
||||
}}>Add model</button>
|
||||
}}
|
||||
>Add model</VoidButton>
|
||||
</div>
|
||||
|
||||
{!errorString ? null : <div className='text-red-500 truncate whitespace-nowrap'>
|
||||
{errorString}
|
||||
</div>}
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</>
|
||||
|
|
@ -158,10 +172,7 @@ const AddModelMenuFull = () => {
|
|||
return <div className='hover:bg-black/10 dark:hover:bg-gray-300/10 py-1 my-4 pb-1 px-3 rounded-sm overflow-hidden '>
|
||||
{open ?
|
||||
<AddModelMenu onSubmit={() => { setOpen(false) }} />
|
||||
: <button
|
||||
className='px-3 py-1 bg-black/10 dark:bg-gray-200/10 rounded-sm overflow-hidden'
|
||||
onClick={() => setOpen(true)}
|
||||
>Add Model</button>
|
||||
: <VoidButton onClick={() => setOpen(true)}>Add Model</VoidButton>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
|
@ -195,20 +206,25 @@ export const ModelDump = () => {
|
|||
|
||||
const disabled = !providerEnabled
|
||||
|
||||
return <div key={`${modelName}${providerName}`} className={`flex items-center justify-between gap-4 hover:bg-black/10 dark:hover:bg-gray-300/10 py-1 px-3 rounded-sm overflow-hidden cursor-default truncate ${isNewProviderName ? 'mt-4' : ''}`}>
|
||||
return <div key={`${modelName}${providerName}`}
|
||||
className={`flex items-center justify-between gap-4 hover:bg-black/10 dark:hover:bg-gray-300/10 py-1 px-3 rounded-sm overflow-hidden cursor-default truncate
|
||||
`}
|
||||
>
|
||||
{/* left part is width:full */}
|
||||
<div className={`w-full flex items-center gap-4`}>
|
||||
<span className='min-w-40'>{isNewProviderName ? displayInfoOfProviderName(providerName).title : ''}</span>
|
||||
<span>{modelName}</span>
|
||||
<div className={`flex-grow flex items-center gap-4`}>
|
||||
<span className='w-full max-w-32'>{isNewProviderName ? displayInfoOfProviderName(providerName).title : ''}</span>
|
||||
<span className='w-fit truncate'>{modelName}</span>
|
||||
{/* <span>{`${modelName} (${providerName})`}</span> */}
|
||||
</div>
|
||||
{/* right part is anything that fits */}
|
||||
<div className='w-fit flex items-center gap-4'>
|
||||
<span className='opacity-50'>{isAutodetected ? '(detected locally)' : isDefault ? '' : '(custom model)'}</span>
|
||||
<div className='flex items-center gap-4'>
|
||||
<span className='opacity-50 truncate'>{isAutodetected ? '(detected locally)' : isDefault ? '' : '(custom model)'}</span>
|
||||
|
||||
<VoidSwitch
|
||||
value={disabled ? false : !isHidden}
|
||||
onChange={() => { settingsStateService.toggleModelHidden(providerName, modelName) }}
|
||||
onChange={() => {
|
||||
settingsStateService.toggleModelHidden(providerName, modelName)
|
||||
}}
|
||||
disabled={disabled}
|
||||
size='sm'
|
||||
/>
|
||||
|
|
@ -235,14 +251,15 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider
|
|||
|
||||
const accessor = useAccessor()
|
||||
const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
const voidMetricsService = accessor.get('IMetricsService')
|
||||
|
||||
let weChangedTextRef = false
|
||||
|
||||
return <ErrorBoundary>
|
||||
<div className='my-1'>
|
||||
<VoidInputBox
|
||||
// placeholder={`${providerTitle} ${settingTitle} (${placeholder}).`}
|
||||
placeholder={`${settingTitle} (${placeholder}).`}
|
||||
// placeholder={`${providerTitle} ${settingTitle} (${placeholder})`}
|
||||
placeholder={`${settingTitle} (${placeholder})`}
|
||||
onChangeText={useCallback((newVal) => {
|
||||
if (weChangedTextRef) return
|
||||
voidSettingsService.setSettingOfProvider(providerName, settingName, newVal)
|
||||
|
|
@ -268,10 +285,12 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider
|
|||
|
||||
if (shouldEnable) {
|
||||
voidSettingsService.setSettingOfProvider(providerName, '_enabled', true)
|
||||
voidMetricsService.capture('Enable Provider', { providerName })
|
||||
}
|
||||
|
||||
if (shouldDisable) {
|
||||
voidSettingsService.setSettingOfProvider(providerName, '_enabled', false)
|
||||
voidMetricsService.capture('Disable Provider', { providerName })
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -281,8 +300,8 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider
|
|||
}, [voidSettingsService, providerName, settingName])}
|
||||
multiline={false}
|
||||
/>
|
||||
{subTextMd === undefined ? null : <div className='py-1 px-3 opacity-50 text-xs'>
|
||||
<ChatMarkdownRender string={subTextMd} />
|
||||
{subTextMd === undefined ? null : <div className='py-1 px-3 opacity-50 text-sm'>
|
||||
<ChatMarkdownRender noSpace string={subTextMd} />
|
||||
</div>}
|
||||
|
||||
</div>
|
||||
|
|
@ -357,10 +376,12 @@ export const VoidProviderSettings = ({ providerNames }: { providerNames: Provide
|
|||
// })}
|
||||
// </>
|
||||
// }
|
||||
type TabName = 'models' | 'general'
|
||||
export const VoidFeatureFlagSettings = () => {
|
||||
|
||||
const accessor = useAccessor()
|
||||
const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
const metricsService = accessor.get('IMetricsService')
|
||||
|
||||
const voidSettingsState = useSettingsState()
|
||||
|
||||
|
|
@ -371,7 +392,10 @@ export const VoidFeatureFlagSettings = () => {
|
|||
const { description } = displayInfoOfFeatureFlag(flagName)
|
||||
|
||||
return <SubtleButton key={flagName}
|
||||
onClick={() => { voidSettingsService.setFeatureFlag(flagName, !enabled) }}
|
||||
onClick={() => {
|
||||
voidSettingsService.setFeatureFlag(flagName, !enabled)
|
||||
metricsService.capture('Click', { action: 'Autorefresh Toggle', flagName, enabled: !enabled })
|
||||
}}
|
||||
text={description}
|
||||
icon={enabled ? <Check className='stroke-green-500 size-3' /> : <X className='stroke-red-500 size-3' />}
|
||||
disabled={false}
|
||||
|
|
@ -379,16 +403,218 @@ export const VoidFeatureFlagSettings = () => {
|
|||
})
|
||||
}
|
||||
|
||||
export const FeaturesTab = () => {
|
||||
return <>
|
||||
<h2 className={`text-3xl mb-2`}>Local Providers</h2>
|
||||
{/* <h3 className={`opacity-50 mb-2`}>{`Keep your data private by hosting AI locally on your computer.`}</h3> */}
|
||||
{/* <h3 className={`opacity-50 mb-2`}>{`Instructions:`}</h3> */}
|
||||
{/* <h3 className={`mb-2`}>{`Void can access any model that you host locally. We automatically detect your local models by default.`}</h3> */}
|
||||
<h3 className={`text-void-fg-3 mb-2`}>{`Void can access any model that you host locally. We automatically detect your local models by default.`}</h3>
|
||||
<div className='pl-4 opacity-50'>
|
||||
<span className={`text-sm mb-2`}><ChatMarkdownRender noSpace string={`1. Download [Ollama](https://ollama.com/download).`} /></span>
|
||||
<span className={`text-sm mb-2`}><ChatMarkdownRender noSpace string={`2. Open your terminal.`} /></span>
|
||||
<span className={`text-sm mb-2 select-text`}><ChatMarkdownRender noSpace string={`3. Run \`ollama run llama3.1\`. This installs Meta's llama3.1 model which is best for chat and inline edits. Requires 5GB of memory.`} /></span>
|
||||
<span className={`text-sm mb-2 select-text`}><ChatMarkdownRender noSpace string={`4. Run \`ollama run qwen2.5-coder:1.5b\`. This installs a faster autocomplete model. Requires 1GB of memory.`} /></span>
|
||||
<span className={`text-sm mb-2`}><ChatMarkdownRender noSpace string={`Void automatically detects locally running models and enables them.`} /></span>
|
||||
{/* TODO we should create UI for downloading models without user going into terminal */}
|
||||
</div>
|
||||
|
||||
<ErrorBoundary>
|
||||
<VoidProviderSettings providerNames={localProviderNames} />
|
||||
</ErrorBoundary>
|
||||
|
||||
<h2 className={`text-3xl mb-2 mt-16`}>Providers</h2>
|
||||
<h3 className={`text-void-fg-3 mb-2`}>{`Void can access models from Anthropic, OpenAI, OpenRouter, and more.`}</h3>
|
||||
{/* <h3 className={`opacity-50 mb-2`}>{`Access models like ChatGPT and Claude. We recommend using Anthropic or OpenAI as providers, or Groq as a faster alternative.`}</h3> */}
|
||||
<ErrorBoundary>
|
||||
<VoidProviderSettings providerNames={nonlocalProviderNames} />
|
||||
</ErrorBoundary>
|
||||
|
||||
<h2 className={`text-3xl mb-2 mt-16`}>Models</h2>
|
||||
<ErrorBoundary>
|
||||
<VoidFeatureFlagSettings />
|
||||
<RefreshableModels />
|
||||
<ModelDump />
|
||||
<AddModelMenuFull />
|
||||
</ErrorBoundary>
|
||||
</>
|
||||
}
|
||||
|
||||
|
||||
|
||||
// https://github.com/VSCodium/vscodium/blob/master/docs/index.md#migrating-from-visual-studio-code-to-vscodium
|
||||
// https://code.visualstudio.com/docs/editor/extension-marketplace#_where-are-extensions-installed
|
||||
type TransferFilesInfo = { from: URI, to: URI }[]
|
||||
const transferTheseFilesOfOS = (os: 'mac' | 'windows' | 'linux' | null): TransferFilesInfo => {
|
||||
if (os === null)
|
||||
throw new Error(`One-click switch is not possible in this environment.`)
|
||||
if (os === 'mac') {
|
||||
const homeDir = env['HOME']
|
||||
if (!homeDir) throw new Error(`$HOME not found`)
|
||||
return [{
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Code', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'settings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Code', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'keybindings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.vscode', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'),
|
||||
}]
|
||||
}
|
||||
|
||||
if (os === 'linux') {
|
||||
const homeDir = env['HOME']
|
||||
if (!homeDir) throw new Error(`variable for $HOME location not found`)
|
||||
return [{
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Code', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'settings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Code', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'keybindings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.vscode', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'),
|
||||
}]
|
||||
}
|
||||
|
||||
if (os === 'windows') {
|
||||
const appdata = env['APPDATA']
|
||||
if (!appdata) throw new Error(`variable for %APPDATA% location not found`)
|
||||
const userprofile = env['USERPROFILE']
|
||||
if (!userprofile) throw new Error(`variable for %USERPROFILE% location not found`)
|
||||
|
||||
return [{
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Code', 'User', 'settings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'settings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Code', 'User', 'keybindings.json'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'keybindings.json'),
|
||||
}, {
|
||||
from: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.vscode', 'extensions'),
|
||||
to: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.void-editor', 'extensions'),
|
||||
}]
|
||||
}
|
||||
|
||||
throw new Error(`os '${os}' not recognized`)
|
||||
}
|
||||
|
||||
const os = isWindows ? 'windows' : isMacintosh ? 'mac' : isLinux ? 'linux' : null
|
||||
let transferTheseFiles: TransferFilesInfo = []
|
||||
let transferError: string | null = null
|
||||
|
||||
try { transferTheseFiles = transferTheseFilesOfOS(os) }
|
||||
catch (e) { transferError = e + '' }
|
||||
|
||||
const OneClickSwitchButton = () => {
|
||||
const accessor = useAccessor()
|
||||
const fileService = accessor.get('IFileService')
|
||||
|
||||
const [state, setState] = useState<{ type: 'done', error?: string } | { type: | 'loading' | 'justfinished' }>({ type: 'done' })
|
||||
|
||||
if (transferTheseFiles.length === 0)
|
||||
return <>
|
||||
<WarningBox text={transferError ?? `One-click-switch not available.`} />
|
||||
</>
|
||||
|
||||
|
||||
|
||||
const onClick = async () => {
|
||||
|
||||
if (state.type !== 'done') return
|
||||
|
||||
setState({ type: 'loading' })
|
||||
|
||||
let errAcc = ''
|
||||
for (let { from, to } of transferTheseFiles) {
|
||||
console.log('transferring', from, to)
|
||||
// not sure if this can fail, just wrapping it with try/catch for now
|
||||
try { await fileService.copy(from, to, true) }
|
||||
catch (e) { errAcc += e + '\n' }
|
||||
}
|
||||
const hadError = !!errAcc
|
||||
if (hadError) {
|
||||
setState({ type: 'done', error: errAcc })
|
||||
}
|
||||
else {
|
||||
setState({ type: 'justfinished' })
|
||||
setTimeout(() => { setState({ type: 'done' }); }, 3000)
|
||||
}
|
||||
}
|
||||
|
||||
return <>
|
||||
<VoidButton disabled={state.type !== 'done'} onClick={onClick}>
|
||||
{state.type === 'done' ? 'Transfer my Settings'
|
||||
: state.type === 'loading' ? 'Transferring...'
|
||||
: state.type === 'justfinished' ? 'Success!'
|
||||
: null
|
||||
}
|
||||
</VoidButton>
|
||||
{state.type === 'done' && state.error ? <WarningBox text={state.error} /> : null}
|
||||
</>
|
||||
}
|
||||
|
||||
|
||||
const GeneralTab = () => {
|
||||
const accessor = useAccessor()
|
||||
const commandService = accessor.get('ICommandService')
|
||||
|
||||
return <>
|
||||
|
||||
|
||||
<div className=''>
|
||||
<h2 className={`text-3xl mb-2`}>One-Click Switch</h2>
|
||||
<h4 className={`text-void-fg-3 mb-2`}>{`Transfer your settings from VS Code to Void in one click.`}</h4>
|
||||
<OneClickSwitchButton />
|
||||
</div>
|
||||
|
||||
|
||||
{/* <div className='my-4'>
|
||||
<h3 className={`text-xl mb-2 mt-4`}>Rules for AI</h3>
|
||||
{`placeholder: "Do not add ;'s. Do not change or delete spacing, formatting, or comments. Respond to queries in French when applicable. "`}
|
||||
</div> */}
|
||||
|
||||
|
||||
|
||||
|
||||
<div className='mt-16'>
|
||||
<h2 className={`text-3xl mb-2`}>Built-in Settings</h2>
|
||||
<h4 className={`text-void-fg-3 mb-2`}>{`IDE settings, keyboard settings, and theme customization.`}</h4>
|
||||
|
||||
<div className='my-4'>
|
||||
<VoidButton onClick={() => { commandService.executeCommand('workbench.action.openSettings') }}>
|
||||
General Settings
|
||||
</VoidButton>
|
||||
</div>
|
||||
<div className='my-4'>
|
||||
<VoidButton onClick={() => { commandService.executeCommand('workbench.action.openGlobalKeybindings') }}>
|
||||
Keyboard Settings
|
||||
</VoidButton>
|
||||
</div>
|
||||
<div className='my-4'>
|
||||
<VoidButton onClick={() => { commandService.executeCommand('workbench.action.selectTheme') }}>
|
||||
Theme Settings
|
||||
</VoidButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* <VoidFeatureFlagSettings /> */}
|
||||
|
||||
</>
|
||||
}
|
||||
|
||||
// full settings
|
||||
|
||||
export const Settings = () => {
|
||||
const isDark = useIsDark()
|
||||
|
||||
const [tab, setTab] = useState<'models' | 'features'>('models')
|
||||
const [tab, setTab] = useState<TabName>('models')
|
||||
|
||||
return <div className={`@@void-scope ${isDark ? 'dark' : ''}`}>
|
||||
<div className='w-full h-full px-10 py-10 select-none'>
|
||||
const containerRef = useRef<HTMLDivElement | null>(null)
|
||||
useScrollbarStyles(containerRef)
|
||||
|
||||
return <div className={`@@void-scope ${isDark ? 'dark' : ''}`} style={{ height: '100%', width: '100%' }}>
|
||||
<div ref={containerRef} className='overflow-y-auto w-full h-full px-10 py-10 select-none'>
|
||||
|
||||
<div className='max-w-5xl mx-auto'>
|
||||
|
||||
|
|
@ -404,9 +630,9 @@ export const Settings = () => {
|
|||
<button className={`text-left p-1 px-3 my-0.5 rounded-sm overflow-hidden ${tab === 'models' ? 'bg-black/10 dark:bg-gray-200/10' : ''} hover:bg-black/10 hover:dark:bg-gray-200/10 active:bg-black/10 active:dark:bg-gray-200/10 `}
|
||||
onClick={() => { setTab('models') }}
|
||||
>Models</button>
|
||||
{/* <button className={`text-left p-1 px-3 my-0.5 rounded-sm overflow-hidden ${tab === 'features' ? 'bg-black/10 dark:bg-gray-200/10' : ''} hover:bg-black/10 hover:dark:bg-gray-200/10 active:bg-black/10 active:dark:bg-gray-200/10 `}
|
||||
onClick={() => { setTab('features') }}
|
||||
>Features</button> */}
|
||||
<button className={`text-left p-1 px-3 my-0.5 rounded-sm overflow-hidden ${tab === 'general' ? 'bg-black/10 dark:bg-gray-200/10' : ''} hover:bg-black/10 hover:dark:bg-gray-200/10 active:bg-black/10 active:dark:bg-gray-200/10 `}
|
||||
onClick={() => { setTab('general') }}
|
||||
>General</button>
|
||||
</div>
|
||||
|
||||
{/* separator */}
|
||||
|
|
@ -414,44 +640,14 @@ export const Settings = () => {
|
|||
|
||||
|
||||
{/* content */}
|
||||
<div className='w-full overflow-y-auto'>
|
||||
<div className='w-full min-w-[600px] overflow-auto'>
|
||||
|
||||
<div className={`${tab !== 'models' ? 'hidden' : ''}`}>
|
||||
<h2 className={`text-3xl mb-2`}>Local Providers</h2>
|
||||
{/* <h3 className={`text-md opacity-50 mb-2`}>{`Keep your data private by hosting AI locally on your computer.`}</h3> */}
|
||||
{/* <h3 className={`text-md opacity-50 mb-2`}>{`Instructions:`}</h3> */}
|
||||
<h3 className={`text-md mb-2`}>{`Void can access any model that you host locally. We automatically detect your local models by default.`}</h3>
|
||||
<div className='pl-4 select-text opacity-50'>
|
||||
<h4 className={`text-xs mb-2`}><ChatMarkdownRender string={`1. Download [Ollama](https://ollama.com/download).`} /></h4>
|
||||
<h4 className={`text-xs mb-2`}><ChatMarkdownRender string={`2. Open your terminal.`} /></h4>
|
||||
<h4 className={`text-xs mb-2`}><ChatMarkdownRender string={`3. Run \`ollama run llama3.1\`. This installs Meta's llama model which is competitive with GPT-series models. It requires 5GB of memory.`} /></h4>
|
||||
<h4 className={`text-xs mb-2`}><ChatMarkdownRender string={`4. Run \`ollama run qwen2.5-coder:1.5b\`. This is a faster autocomplete model and requires 1GB of memory.`} /></h4>
|
||||
{/* TODO we should create UI for downloading models without user going into terminal */}
|
||||
</div>
|
||||
|
||||
<ErrorBoundary>
|
||||
<VoidProviderSettings providerNames={localProviderNames} />
|
||||
</ErrorBoundary>
|
||||
|
||||
<h2 className={`text-3xl mb-2 mt-16`}>More Providers</h2>
|
||||
<h3 className={`text-md mb-2`}>{`Void can also access models like ChatGPT and Claude. We recommend using Anthropic or OpenAI.`}</h3>
|
||||
{/* <h3 className={`text-md opacity-50 mb-2`}>{`Access models like ChatGPT and Claude. We recommend using Anthropic or OpenAI as providers, or Groq as a faster alternative.`}</h3> */}
|
||||
<ErrorBoundary>
|
||||
<VoidProviderSettings providerNames={nonlocalProviderNames} />
|
||||
</ErrorBoundary>
|
||||
|
||||
<h2 className={`text-3xl mb-2 mt-16`}>Models</h2>
|
||||
<ErrorBoundary>
|
||||
<VoidFeatureFlagSettings />
|
||||
<RefreshableModels />
|
||||
<ModelDump />
|
||||
<AddModelMenuFull />
|
||||
</ErrorBoundary>
|
||||
<FeaturesTab />
|
||||
</div>
|
||||
|
||||
<div className={`${tab !== 'features' ? 'hidden' : ''}`}>
|
||||
<h2 className={`text-3xl mb-2`} onClick={() => { setTab('features') }}>Features</h2>
|
||||
{/* <VoidFeatureFlagSettings /> */}
|
||||
<div className={`${tab !== 'general' ? 'hidden' : ''}`}>
|
||||
<GeneralTab />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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 { mountFnGenerator } from '../util/mountFnGenerator.js'
|
||||
import { Settings } from './Settings.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.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
|
|
@ -9,7 +9,38 @@ module.exports = {
|
|||
content: ['./src2/**/*.{jsx,tsx}'], // uses these files to decide how to transform the css file
|
||||
theme: {
|
||||
extend: {
|
||||
fontSize: {
|
||||
xs: '10px',
|
||||
sm: '11px',
|
||||
root: '13px',
|
||||
lg: '14px',
|
||||
xl: '16px',
|
||||
'2xl': '18px',
|
||||
'3xl': '20px',
|
||||
'4xl': '24px',
|
||||
'5xl': '30px',
|
||||
'6xl': '36px',
|
||||
'7xl': '48px',
|
||||
'8xl': '64px',
|
||||
'9xl': '72px',
|
||||
},
|
||||
// common colors to use, ordered light to dark
|
||||
|
||||
colors: {
|
||||
"void-bg-1": "var(--vscode-input-background)",
|
||||
"void-bg-2": "var(--vscode-sideBar-background)",
|
||||
"void-bg-3": "var(--vscode-editor-background)",
|
||||
|
||||
"void-fg-1": "var(--vscode-editor-foreground)",
|
||||
"void-fg-2": "var(--vscode-input-foreground)",
|
||||
"void-fg-3": "var(--vscode-input-placeholderForeground)",
|
||||
"void-warning": "var(--vscode-charts-yellow)",
|
||||
|
||||
"void-border-1": "var(--vscode-commandCenter-activeBorder)",
|
||||
"void-border-2": "var(--vscode-commandCenter-border)",
|
||||
"void-border-3": "var(--vscode-commandCenter-inactiveBorder)",
|
||||
|
||||
|
||||
vscode: {
|
||||
// see: https://code.visualstudio.com/api/extension-guides/webview#theming-webview-content
|
||||
|
||||
|
|
@ -39,8 +70,8 @@ module.exports = {
|
|||
"input-bg": "var(--vscode-input-background)",
|
||||
"input-border": "var(--vscode-input-border)",
|
||||
"input-fg": "var(--vscode-input-foreground)",
|
||||
"input-placeholder-fg": "var(--vscode-placeholderForeground)",
|
||||
"input-active-bg": "var(--vscode-activeBackground)",
|
||||
"input-placeholder-fg": "var(--vscode-input-placeholderForeground)",
|
||||
"input-active-bg": "var(--vscode-input-activeBackground)",
|
||||
"input-option-active-border": "var(--vscode-inputOption-activeBorder)",
|
||||
"input-option-active-fg": "var(--vscode-inputOption-activeForeground)",
|
||||
"input-option-hover-bg": "var(--vscode-inputOption-hoverBackground)",
|
||||
|
|
|
|||
|
|
@ -1,9 +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.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"exactOptionalPropertyTypes": false,
|
||||
|
|
@ -17,5 +16,10 @@
|
|||
// this is just for type checking, so src/ is the correct dir
|
||||
"./src/**/*.ts",
|
||||
"./src/**/*.tsx"
|
||||
]
|
||||
],
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 { defineConfig } from 'tsup'
|
||||
|
||||
|
|
@ -9,7 +9,7 @@ export default defineConfig({
|
|||
entry: [
|
||||
'./src2/sidebar-tsx/index.tsx',
|
||||
'./src2/void-settings-tsx/index.tsx',
|
||||
'./src2/ctrl-k-tsx/index.tsx',
|
||||
'./src2/quick-edit-tsx/index.tsx',
|
||||
'./src2/diff/index.tsx',
|
||||
],
|
||||
outDir: './out',
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
@ -16,19 +16,36 @@ import { CodeStagingSelection, IThreadHistoryService } from './threadHistoryServ
|
|||
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
|
||||
import { IRange } from '../../../../editor/common/core/range.js';
|
||||
import { ITextModel } from '../../../../editor/common/model.js';
|
||||
import { VOID_VIEW_ID } from './sidebarPane.js';
|
||||
import { VOID_VIEW_CONTAINER_ID, VOID_VIEW_ID } from './sidebarPane.js';
|
||||
import { IMetricsService } from '../../../../platform/void/common/metricsService.js';
|
||||
import { ISidebarStateService } from './sidebarStateService.js';
|
||||
import { ICommandService } from '../../../../platform/commands/common/commands.js';
|
||||
import { VOID_OPEN_SETTINGS_ACTION_ID } from './voidSettingsPane.js';
|
||||
import { VOID_TOGGLE_SETTINGS_ACTION_ID } from './voidSettingsPane.js';
|
||||
import { VOID_CTRL_L_ACTION_ID } from './actionIDs.js';
|
||||
import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js';
|
||||
import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js';
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { localize2 } from '../../../../nls.js';
|
||||
import { IViewsService } from '../../../services/views/common/viewsService.js';
|
||||
|
||||
|
||||
// ---------- Register commands and keybindings ----------
|
||||
|
||||
|
||||
const roundRangeToLines = (range: IRange | null | undefined) => {
|
||||
export const roundRangeToLines = (range: IRange | null | undefined, options: { emptySelectionBehavior: 'null' | 'line' }) => {
|
||||
if (!range)
|
||||
return null
|
||||
|
||||
// treat as no selection if selection is empty
|
||||
if (range.endColumn === range.startColumn && range.endLineNumber === range.startLineNumber) {
|
||||
if (options.emptySelectionBehavior === 'null')
|
||||
return null
|
||||
else if (options.emptySelectionBehavior === 'line')
|
||||
return { startLineNumber: range.startLineNumber, startColumn: 1, endLineNumber: range.startLineNumber, endColumn: 1 }
|
||||
}
|
||||
|
||||
// IRange is 1-indexed
|
||||
const endLine = range.endColumn === 1 ? range.endLineNumber - 1 : range.endLineNumber // e.g. if the user triple clicks, it selects column=0, line=line -> column=0, line=line+1
|
||||
const newRange: IRange = {
|
||||
|
|
@ -50,11 +67,28 @@ const getContentInRange = (model: ITextModel, range: IRange | null) => {
|
|||
return trimmedContent
|
||||
}
|
||||
|
||||
// Action: when press ctrl+L, show the sidebar chat and add to the selection
|
||||
export const VOID_CTRL_L_ACTION_ID = 'void.ctrlLAction'
|
||||
|
||||
|
||||
const VOID_OPEN_SIDEBAR_ACTION_ID = 'void.sidebar.open'
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({ id: VOID_CTRL_L_ACTION_ID, title: 'Void: Show Sidebar', keybinding: { primary: KeyMod.CtrlCmd | KeyCode.KeyL, weight: KeybindingWeight.BuiltinExtension } });
|
||||
super({ id: VOID_OPEN_SIDEBAR_ACTION_ID, title: localize2('voidOpenSidebar', 'Void: Open Sidebar'), f1: true });
|
||||
}
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const stateService = accessor.get(ISidebarStateService)
|
||||
stateService.setState({ isHistoryOpen: false, currentTab: 'chat' })
|
||||
stateService.fireFocusChat()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
// Action: when press ctrl+L, show the sidebar chat and add to the selection
|
||||
const VOID_ADD_SELECTION_TO_SIDEBAR_ACTION_ID = 'void.sidebar.select'
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({ id: VOID_ADD_SELECTION_TO_SIDEBAR_ACTION_ID, title: localize2('voidAddToSidebar', 'Void: Add Selection to Sidebar'), f1: true });
|
||||
}
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
|
||||
|
|
@ -62,67 +96,76 @@ registerAction2(class extends Action2 {
|
|||
if (!model)
|
||||
return
|
||||
|
||||
const stateService = accessor.get(ISidebarStateService)
|
||||
const metricsService = accessor.get(IMetricsService)
|
||||
const editorService = accessor.get(ICodeEditorService)
|
||||
|
||||
metricsService.capture('User Action', { type: 'Ctrl+L' })
|
||||
|
||||
stateService.setState({ isHistoryOpen: false, currentTab: 'chat' })
|
||||
stateService.fireFocusChat()
|
||||
metricsService.capture('Ctrl+L', {})
|
||||
|
||||
const editor = editorService.getActiveCodeEditor()
|
||||
const selectionRange = roundRangeToLines(
|
||||
// accessor.get(IEditorService).activeTextEditorControl?.getSelection()
|
||||
editor?.getSelection()
|
||||
// accessor.get(IEditorService).activeTextEditorControl?.getSelection()
|
||||
const selectionRange = roundRangeToLines(editor?.getSelection(), { emptySelectionBehavior: 'null' })
|
||||
|
||||
|
||||
// select whole lines
|
||||
if (selectionRange) {
|
||||
editor?.setSelection({ startLineNumber: selectionRange.startLineNumber, endLineNumber: selectionRange.endLineNumber, startColumn: 1, endColumn: Number.MAX_SAFE_INTEGER })
|
||||
}
|
||||
|
||||
const selectionStr = getContentInRange(model, selectionRange)
|
||||
|
||||
const selection: CodeStagingSelection = !selectionRange || !selectionStr || (selectionRange.startLineNumber > selectionRange.endLineNumber) ? {
|
||||
type: 'File',
|
||||
fileURI: model.uri,
|
||||
selectionStr: null,
|
||||
range: null,
|
||||
} : {
|
||||
type: 'Selection',
|
||||
fileURI: model.uri,
|
||||
selectionStr: selectionStr,
|
||||
range: selectionRange,
|
||||
}
|
||||
|
||||
// add selection to staging
|
||||
const threadHistoryService = accessor.get(IThreadHistoryService)
|
||||
const currentStaging = threadHistoryService.state._currentStagingSelections
|
||||
const currentStagingEltIdx = currentStaging?.findIndex(s =>
|
||||
s.fileURI.fsPath === model.uri.fsPath
|
||||
&& s.range?.startLineNumber === selection.range?.startLineNumber
|
||||
&& s.range?.endLineNumber === selection.range?.endLineNumber
|
||||
)
|
||||
|
||||
|
||||
if (selectionRange) {
|
||||
// select whole lines
|
||||
editor?.setSelection({ startLineNumber: selectionRange.startLineNumber, endLineNumber: selectionRange.endLineNumber, startColumn: 1, endColumn: Number.MAX_SAFE_INTEGER })
|
||||
|
||||
const selectionStr = getContentInRange(model, selectionRange)
|
||||
|
||||
const selection: CodeStagingSelection = selectionStr === null || selectionRange.startLineNumber > selectionRange.endLineNumber ? {
|
||||
type: 'File',
|
||||
fileURI: model.uri,
|
||||
selectionStr: null,
|
||||
range: null,
|
||||
} : {
|
||||
type: 'Selection',
|
||||
fileURI: model.uri,
|
||||
selectionStr: selectionStr,
|
||||
range: selectionRange,
|
||||
}
|
||||
|
||||
// add selection to staging
|
||||
const threadHistoryService = accessor.get(IThreadHistoryService)
|
||||
const currentStaging = threadHistoryService.state._currentStagingSelections
|
||||
const currentStagingEltIdx = currentStaging?.findIndex(s =>
|
||||
s.fileURI.fsPath === model.uri.fsPath
|
||||
&& s.range?.startLineNumber === selection.range?.startLineNumber
|
||||
&& s.range?.endLineNumber === selection.range?.endLineNumber
|
||||
)
|
||||
|
||||
// if matches with existing selection, overwrite
|
||||
if (currentStagingEltIdx !== undefined && currentStagingEltIdx !== -1) {
|
||||
threadHistoryService.setStaging([
|
||||
...currentStaging!.slice(0, currentStagingEltIdx),
|
||||
selection,
|
||||
...currentStaging!.slice(currentStagingEltIdx + 1, Infinity)
|
||||
])
|
||||
}
|
||||
// if no match, add
|
||||
else {
|
||||
threadHistoryService.setStaging([...(currentStaging ?? []), selection])
|
||||
}
|
||||
// if matches with existing selection, overwrite
|
||||
if (currentStagingEltIdx !== undefined && currentStagingEltIdx !== -1) {
|
||||
threadHistoryService.setStaging([
|
||||
...currentStaging!.slice(0, currentStagingEltIdx),
|
||||
selection,
|
||||
...currentStaging!.slice(currentStagingEltIdx + 1, Infinity)
|
||||
])
|
||||
}
|
||||
// if no match, add
|
||||
else {
|
||||
threadHistoryService.setStaging([...(currentStaging ?? []), selection])
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({ id: VOID_CTRL_L_ACTION_ID, title: 'Void: Press Ctrl+L', keybinding: { primary: KeyMod.CtrlCmd | KeyCode.KeyL, weight: KeybindingWeight.BuiltinExtension } });
|
||||
}
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const commandService = accessor.get(ICommandService)
|
||||
await commandService.executeCommand(VOID_OPEN_SIDEBAR_ACTION_ID)
|
||||
await commandService.executeCommand(VOID_ADD_SELECTION_TO_SIDEBAR_ACTION_ID)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// New chat menu button
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
|
|
@ -180,6 +223,70 @@ registerAction2(class extends Action2 {
|
|||
}
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const commandService = accessor.get(ICommandService)
|
||||
commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID)
|
||||
commandService.executeCommand(VOID_TOGGLE_SETTINGS_ACTION_ID)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
export class TabSwitchListener extends Disposable {
|
||||
|
||||
constructor(
|
||||
onSwitchTab: (uri: URI) => void,
|
||||
@ICodeEditorService private readonly _editorService: ICodeEditorService,
|
||||
) {
|
||||
super()
|
||||
|
||||
// when editor switches tabs (models)
|
||||
const addTabSwitchListeners = (editor: ICodeEditor) => {
|
||||
this._register(editor.onDidChangeModel(e => {
|
||||
if (e.newModelUrl && e.newModelUrl.scheme === 'file') {
|
||||
onSwitchTab(e.newModelUrl)
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
const initializeEditor = (editor: ICodeEditor) => {
|
||||
addTabSwitchListeners(editor)
|
||||
}
|
||||
|
||||
// initialize current editors + any new editors
|
||||
for (let editor of this._editorService.listCodeEditors()) initializeEditor(editor)
|
||||
this._register(this._editorService.onCodeEditorAdd(editor => { initializeEditor(editor) }))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class TabSwitchContribution extends Disposable implements IWorkbenchContribution {
|
||||
static readonly ID = 'workbench.contrib.void.tabswitch'
|
||||
|
||||
constructor(
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@ICommandService private readonly commandService: ICommandService,
|
||||
@IViewsService private readonly viewsService: IViewsService,
|
||||
) {
|
||||
super()
|
||||
|
||||
// sidebarIsVisible state
|
||||
let sidebarIsVisible = this.viewsService.isViewContainerVisible(VOID_VIEW_CONTAINER_ID)
|
||||
this._register(this.viewsService.onDidChangeViewVisibility(e => {
|
||||
sidebarIsVisible = e.visible
|
||||
}))
|
||||
|
||||
const addCurrentFileIfVisible = () => {
|
||||
if (sidebarIsVisible)
|
||||
this.commandService.executeCommand(VOID_ADD_SELECTION_TO_SIDEBAR_ACTION_ID)
|
||||
}
|
||||
|
||||
// when sidebar becomes visible, add current file
|
||||
this._register(this.viewsService.onDidChangeViewVisibility(e => { sidebarIsVisible = e.visible }))
|
||||
|
||||
// run on current tab if it exists, and listen for tab switches and visibility changes
|
||||
addCurrentFileIfVisible()
|
||||
this._register(this.viewsService.onDidChangeViewVisibility(() => { addCurrentFileIfVisible() }))
|
||||
this._register(this.instantiationService.createInstance(TabSwitchListener, () => { addCurrentFileIfVisible() }))
|
||||
}
|
||||
}
|
||||
|
||||
registerWorkbenchContribution2(TabSwitchContribution.ID, TabSwitchContribution, WorkbenchPhase.BlockRestore);
|
||||
|
|
|
|||
|
|
@ -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 { Registry } from '../../../../platform/registry/common/platform.js';
|
||||
import {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
@ -60,6 +60,10 @@ export type ChatThreads = {
|
|||
createdAt: string; // ISO string
|
||||
lastModified: string; // ISO string
|
||||
messages: ChatMessage[];
|
||||
|
||||
// editing state
|
||||
isBeingEdited: boolean;
|
||||
_currentStagingSelections: CodeStagingSelection[] | null;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -77,6 +81,8 @@ const newThreadObject = () => {
|
|||
createdAt: now,
|
||||
lastModified: now,
|
||||
messages: [],
|
||||
isBeingEdited: false,
|
||||
_currentStagingSelections: null,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
// register inline diffs
|
||||
|
|
|
|||
|
|
@ -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 { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { EditorInput } from '../../../common/editor/editorInput.js';
|
||||
|
|
@ -26,7 +26,6 @@ import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextke
|
|||
import { mountVoidSettings } from './react/out/void-settings-tsx/index.js'
|
||||
import { Codicon } from '../../../../base/common/codicons.js';
|
||||
import { IDisposable } from '../../../../base/common/lifecycle.js';
|
||||
import { DomScrollableElement } from '../../../../base/browser/ui/scrollbar/scrollableElement.js';
|
||||
|
||||
|
||||
// refer to preferences.contribution.ts keybindings editor
|
||||
|
|
@ -63,7 +62,7 @@ class VoidSettingsInput extends EditorInput {
|
|||
class VoidSettingsPane extends EditorPane {
|
||||
static readonly ID = 'workbench.test.myCustomPane';
|
||||
|
||||
private _scrollbar: DomScrollableElement | undefined;
|
||||
// private _scrollbar: DomScrollableElement | undefined;
|
||||
|
||||
constructor(
|
||||
group: IEditorGroup,
|
||||
|
|
@ -79,32 +78,31 @@ class VoidSettingsPane extends EditorPane {
|
|||
parent.style.height = '100%';
|
||||
parent.style.width = '100%';
|
||||
|
||||
const scrollableContent = document.createElement('div');
|
||||
scrollableContent.style.height = '100%';
|
||||
scrollableContent.style.width = '100%';
|
||||
const settingsElt = document.createElement('div');
|
||||
settingsElt.style.height = '100%';
|
||||
settingsElt.style.width = '100%';
|
||||
|
||||
this._scrollbar = this._register(new DomScrollableElement(scrollableContent, {}));
|
||||
parent.appendChild(this._scrollbar.getDomNode());
|
||||
this._scrollbar.scanDomNode();
|
||||
parent.appendChild(settingsElt);
|
||||
|
||||
// this._scrollbar = this._register(new DomScrollableElement(scrollableContent, {}));
|
||||
// parent.appendChild(this._scrollbar.getDomNode());
|
||||
// this._scrollbar.scanDomNode();
|
||||
|
||||
// Mount React into the scrollable content
|
||||
this.instantiationService.invokeFunction(accessor => {
|
||||
const disposables: IDisposable[] | undefined = mountVoidSettings(scrollableContent, accessor);
|
||||
const disposables: IDisposable[] | undefined = mountVoidSettings(settingsElt, accessor);
|
||||
|
||||
setTimeout(() => { // this is a complete hack and I don't really understand how scrollbar works here
|
||||
this._scrollbar?.scanDomNode();
|
||||
}, 1000)
|
||||
// setTimeout(() => { // this is a complete hack and I don't really understand how scrollbar works here
|
||||
// this._scrollbar?.scanDomNode();
|
||||
// }, 1000)
|
||||
disposables?.forEach(d => this._register(d));
|
||||
});
|
||||
}
|
||||
|
||||
layout(dimension: Dimension): void {
|
||||
if (!this._scrollbar) return;
|
||||
|
||||
this._scrollbar.getDomNode().style.height = `${dimension.height}px`;
|
||||
this._scrollbar.getDomNode().style.width = `${dimension.width}px`;
|
||||
this._scrollbar.scanDomNode();
|
||||
|
||||
// if (!settingsElt) return
|
||||
// settingsElt.style.height = `${dimension.height}px`;
|
||||
// settingsElt.style.width = `${dimension.width}px`;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -119,14 +117,13 @@ Registry.as<IEditorPaneRegistry>(EditorExtensions.EditorPane).registerEditorPane
|
|||
);
|
||||
|
||||
|
||||
export const VOID_OPEN_SETTINGS_ACTION_ID = 'workbench.action.openVoidSettings'
|
||||
// register the gear on the top right
|
||||
export const VOID_TOGGLE_SETTINGS_ACTION_ID = 'workbench.action.toggleVoidSettings'
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: VOID_OPEN_SETTINGS_ACTION_ID,
|
||||
title: nls.localize2('voidSettings', "Void: Settings"),
|
||||
f1: true,
|
||||
id: VOID_TOGGLE_SETTINGS_ACTION_ID,
|
||||
title: nls.localize2('voidSettings', "Void: Toggle Settings"),
|
||||
icon: Codicon.settingsGear,
|
||||
menu: [
|
||||
{
|
||||
|
|
@ -146,9 +143,8 @@ registerAction2(class extends Action2 {
|
|||
const editorService = accessor.get(IEditorService);
|
||||
const instantiationService = accessor.get(IInstantiationService);
|
||||
|
||||
const openEditors = editorService.findEditors(VoidSettingsInput.RESOURCE);
|
||||
|
||||
// close all instances if found
|
||||
const openEditors = editorService.findEditors(VoidSettingsInput.RESOURCE);
|
||||
if (openEditors.length > 0) {
|
||||
await editorService.closeEditors(openEditors);
|
||||
return;
|
||||
|
|
@ -161,11 +157,42 @@ registerAction2(class extends Action2 {
|
|||
})
|
||||
|
||||
|
||||
|
||||
export const VOID_OPEN_SETTINGS_ACTION_ID = 'workbench.action.openVoidSettings'
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: VOID_OPEN_SETTINGS_ACTION_ID,
|
||||
title: nls.localize2('voidSettings', "Void: Open Settings"),
|
||||
f1: true,
|
||||
icon: Codicon.settingsGear,
|
||||
});
|
||||
}
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const editorService = accessor.get(IEditorService);
|
||||
const instantiationService = accessor.get(IInstantiationService);
|
||||
|
||||
// close all instances if found
|
||||
const openEditors = editorService.findEditors(VoidSettingsInput.RESOURCE);
|
||||
if (openEditors.length > 0) {
|
||||
await editorService.closeEditors(openEditors);
|
||||
}
|
||||
|
||||
// then, open one single editor
|
||||
const input = instantiationService.createInstance(VoidSettingsInput);
|
||||
await editorService.openEditor(input);
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// add to settings gear on bottom left
|
||||
MenuRegistry.appendMenuItem(MenuId.GlobalActivity, {
|
||||
group: '0_command',
|
||||
command: {
|
||||
id: VOID_OPEN_SETTINGS_ACTION_ID,
|
||||
id: VOID_TOGGLE_SETTINGS_ACTION_ID,
|
||||
title: nls.localize('voidSettings', "Void Settings")
|
||||
},
|
||||
order: 1
|
||||
|
|
|
|||
Loading…
Reference in a new issue