mirror of
https://github.com/voideditor/void
synced 2026-05-24 09:58:23 +00:00
Merge branch 'main' into feat_mistral-199
This commit is contained in:
commit
b1626849ba
57 changed files with 10175 additions and 5396 deletions
|
|
@ -90,7 +90,7 @@ Alternatively, if you want to build Void from the terminal, instead of pressing
|
|||
- If you get `"TypeError: Failed to fetch dynamically imported module"`, make sure all imports end with `.js`.
|
||||
- If you see missing styles, wait a few seconds and then reload.
|
||||
- If you have any questions, feel free to [submit an issue](https://github.com/voideditor/void/issues/new). You can also refer to VSCode's complete [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute) page.
|
||||
|
||||
- If you get errors like `npm error libtool: error: unrecognised option: '-static'`, make sure you have GNU libtool instead of BSD libtool (BSD is the default in macos)
|
||||
|
||||
|
||||
## Packaging
|
||||
|
|
@ -104,12 +104,11 @@ We don't usually recommend packaging. Instead, you should probably just build. I
|
|||
|
||||
### Windows
|
||||
- `npm run gulp vscode-win32-x64` - most common
|
||||
- `npm run gulp vscode-win32-ia32`
|
||||
- `npm run gulp vscode-win32-arm64`
|
||||
|
||||
### Linux
|
||||
- `npm run gulp vscode-linux-x64` - most common
|
||||
- `npm run gulp vscode-linux-arm`
|
||||
- `npm run gulp vscode-linux-ia32`
|
||||
- `npm run gulp vscode-linux-arm64`
|
||||
|
||||
|
||||
### Output
|
||||
|
|
|
|||
|
|
@ -139,6 +139,7 @@ module.exports.indentationFilter = [
|
|||
'!extensions/simple-browser/media/*.js',
|
||||
|
||||
'!extensions/open-remote-ssh/out/*.js', // Void added this
|
||||
'!extensions/open-remote-wsl/out/*.js', // Void added this
|
||||
];
|
||||
|
||||
module.exports.copyrightFilter = [
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ const compilations = [
|
|||
'.vscode/extensions/vscode-selfhost-import-aid/tsconfig.json',
|
||||
|
||||
'extensions/open-remote-ssh/tsconfig.json', // Void added this
|
||||
'extensions/open-remote-wsl/tsconfig.json', // Void added this
|
||||
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -502,7 +502,7 @@ BUILD_TARGETS.forEach(buildTarget => {
|
|||
gulp.task(vscodeTaskCI);
|
||||
|
||||
const vscodeTask = task.define(`vscode${dashed(platform)}${dashed(arch)}${dashed(minified)}`, task.series(
|
||||
compileBuildWithoutManglingTask,
|
||||
minified ? compileBuildWithManglingTask : compileBuildWithoutManglingTask,
|
||||
cleanExtensionsBuildTask,
|
||||
compileNonNativeExtensionsBuildTask,
|
||||
compileExtensionMediaBuildTask,
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ const dirs = [
|
|||
'.vscode/extensions/vscode-selfhost-import-aid',
|
||||
'.vscode/extensions/vscode-selfhost-test-provider',
|
||||
'extensions/open-remote-ssh', // Void added this
|
||||
'extensions/open-remote-wsl', // Void added this
|
||||
|
||||
];
|
||||
|
||||
|
|
|
|||
3
extensions/open-remote-wsl/README.md
Normal file
3
extensions/open-remote-wsl/README.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# Remote - WSL Support
|
||||
|
||||
Inherited for Void from [Open Remote - WSL](https://github.com/jeanp413/open-remote-wsl).
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
//@ts-check
|
||||
|
||||
'use strict';
|
||||
|
||||
const withBrowserDefaults = require('../shared.webpack.config').browser;
|
||||
|
||||
module.exports = withBrowserDefaults({
|
||||
context: __dirname,
|
||||
entry: {
|
||||
extension: './src/extension.ts'
|
||||
}
|
||||
});
|
||||
20
extensions/open-remote-wsl/extension.webpack.config.js
Normal file
20
extensions/open-remote-wsl/extension.webpack.config.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
//@ts-check
|
||||
|
||||
'use strict';
|
||||
|
||||
const withDefaults = require('../shared.webpack.config');
|
||||
|
||||
module.exports = withDefaults({
|
||||
context: __dirname,
|
||||
resolve: {
|
||||
mainFields: ['module', 'main']
|
||||
},
|
||||
entry: {
|
||||
extension: './src/extension.ts',
|
||||
}
|
||||
});
|
||||
15
extensions/open-remote-wsl/package-lock.json
generated
Normal file
15
extensions/open-remote-wsl/package-lock.json
generated
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"name": "open-remote-wsl",
|
||||
"version": "0.0.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "open-remote-wsl",
|
||||
"version": "0.0.4",
|
||||
"engines": {
|
||||
"vscode": "^1.70.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
281
extensions/open-remote-wsl/package.json
Normal file
281
extensions/open-remote-wsl/package.json
Normal file
|
|
@ -0,0 +1,281 @@
|
|||
{
|
||||
"name": "open-remote-wsl",
|
||||
"displayName": "Remote - WSL",
|
||||
"description": "Open any folder in the Windows Subsystem for Linux (WSL).",
|
||||
"version": "0.0.4",
|
||||
"icon": "resources/icon.png",
|
||||
"engines": {
|
||||
"vscode": "^1.70.2"
|
||||
},
|
||||
"extensionKind": [
|
||||
"ui"
|
||||
],
|
||||
"enabledApiProposals": [
|
||||
"resolvers",
|
||||
"contribViewsRemote"
|
||||
],
|
||||
"keywords": [
|
||||
"remote development",
|
||||
"remote",
|
||||
"wsl"
|
||||
],
|
||||
"api": "none",
|
||||
"activationEvents": [
|
||||
"onCommand:openremotewsl.connect",
|
||||
"onCommand:openremotewsl.connectInNewWindow",
|
||||
"onCommand:openremotewsl.connectUsingDistro",
|
||||
"onCommand:openremotewsl.connectUsingDistroInNewWindow",
|
||||
"onCommand:openremotewsl.showLog",
|
||||
"onResolveRemoteAuthority:wsl",
|
||||
"onView:wslTargets"
|
||||
],
|
||||
"main": "./out/extension.js",
|
||||
"contributes": {
|
||||
"configuration": {
|
||||
"title": "WSL",
|
||||
"properties": {
|
||||
"remote.WSL.serverDownloadUrlTemplate": {
|
||||
"type": "string",
|
||||
"description": "The URL from where the vscode server will be downloaded. You can use the following variables and they will be replaced dynamically:\n- ${quality}: vscode server quality, e.g. stable or insiders\n- ${version}: vscode server version, e.g. 1.69.0\n- ${commit}: vscode server release commit\n- ${arch}: vscode server arch, e.g. x64, armhf, arm64\n- ${release}: release number",
|
||||
"scope": "application",
|
||||
"default": "https://github.com/voideditor/binaries/releases/download/${version}.${release}/void-reh-${os}-${arch}-${version}.${release}.tar.gz"
|
||||
}
|
||||
}
|
||||
},
|
||||
"views": {
|
||||
"remote": [
|
||||
{
|
||||
"id": "wslTargets",
|
||||
"name": "WSL Targets",
|
||||
"group": "targets@1",
|
||||
"when": "(isWindows && !isWeb)",
|
||||
"remoteName": "wsl"
|
||||
}
|
||||
]
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"command": "openremotewsl.connect",
|
||||
"title": "Connect to WSL",
|
||||
"category": "Remote-WSL"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.connectInNewWindow",
|
||||
"title": "Connect to WSL in New Window",
|
||||
"category": "Remote-WSL"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.connectUsingDistro",
|
||||
"title": "Connect to WSL using Distro...",
|
||||
"category": "Remote-WSL"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.connectUsingDistroInNewWindow",
|
||||
"title": "Connect to WSL using Distro in New Window...",
|
||||
"category": "Remote-WSL"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.showLog",
|
||||
"title": "Show Log",
|
||||
"category": "Remote-WSL"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.emptyWindowInNewWindow",
|
||||
"title": "Connect in New Window",
|
||||
"icon": "$(empty-window)"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.emptyWindowInCurrentWindow",
|
||||
"title": "Connect in Current Window"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.reopenFolderInCurrentWindow",
|
||||
"title": "Open in Current Window"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.reopenFolderInNewWindow",
|
||||
"title": "Open in New Window",
|
||||
"icon": "$(folder-opened)"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.deleteFolderHistoryItem",
|
||||
"title": "Remove From Recent List",
|
||||
"icon": "$(x)"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.refresh",
|
||||
"title": "Refresh",
|
||||
"icon": "$(refresh)"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.addDistro",
|
||||
"title": "Add a Distro",
|
||||
"icon": "$(plus)"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.setDefaultDistro",
|
||||
"title": "Set as Default Distro"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.deleteDistro",
|
||||
"title": "Delete Distro"
|
||||
}
|
||||
],
|
||||
"resourceLabelFormatters": [
|
||||
{
|
||||
"scheme": "vscode-remote",
|
||||
"authority": "wsl+*",
|
||||
"formatting": {
|
||||
"label": "${path}",
|
||||
"separator": "/",
|
||||
"tildify": true,
|
||||
"workspaceSuffix": "WSL"
|
||||
}
|
||||
}
|
||||
],
|
||||
"menus": {
|
||||
"statusBar/remoteIndicator": [
|
||||
{
|
||||
"command": "openremotewsl.connect",
|
||||
"when": "(isWindows && !isWeb)",
|
||||
"group": "remote_20_wsl_1general@1"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.connectUsingDistro",
|
||||
"when": "(isWindows && !isWeb)",
|
||||
"group": "remote_20_wsl_1general@2"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.showLog",
|
||||
"when": "remoteName =~ /^wsl$/",
|
||||
"group": "remote_20_wsl_1general@4"
|
||||
}
|
||||
],
|
||||
"commandPalette": [
|
||||
{
|
||||
"command": "openremotewsl.connect",
|
||||
"when": "(isWindows && !isWeb)"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.connectInNewWindow",
|
||||
"when": "(isWindows && !isWeb)"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.connectUsingDistro",
|
||||
"when": "(isWindows && !isWeb)"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.connectUsingDistroInNewWindow",
|
||||
"when": "(isWindows && !isWeb)"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.refresh",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.addDistro",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.emptyWindowInNewWindow",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.emptyWindowInCurrentWindow",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.reopenFolderInCurrentWindow",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.reopenFolderInNewWindow",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.deleteFolderHistoryItem",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.setDefaultDistro",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.deleteDistro",
|
||||
"when": "false"
|
||||
}
|
||||
],
|
||||
"view/title": [
|
||||
{
|
||||
"command": "openremotewsl.explorer.addDistro",
|
||||
"when": "view == wslTargets",
|
||||
"group": "navigation"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.refresh",
|
||||
"when": "view == wslTargets",
|
||||
"group": "navigation"
|
||||
}
|
||||
],
|
||||
"view/item/context": [
|
||||
{
|
||||
"command": "openremotewsl.explorer.emptyWindowInNewWindow",
|
||||
"when": "viewItem =~ /^openremotewsl.explorer.distro$/",
|
||||
"group": "inline@1"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.emptyWindowInNewWindow",
|
||||
"when": "viewItem =~ /^openremotewsl.explorer.distro$/",
|
||||
"group": "navigation@2"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.emptyWindowInCurrentWindow",
|
||||
"when": "viewItem =~ /^openremotewsl.explorer.distro$/",
|
||||
"group": "navigation@1"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.setDefaultDistro",
|
||||
"when": "viewItem =~ /^openremotewsl.explorer.distro$/",
|
||||
"group": "management@1"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.deleteDistro",
|
||||
"when": "viewItem =~ /^openremotewsl.explorer.distro$/",
|
||||
"group": "management@2"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.reopenFolderInNewWindow",
|
||||
"when": "viewItem == openremotewsl.explorer.folder",
|
||||
"group": "inline@1"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.reopenFolderInNewWindow",
|
||||
"when": "viewItem == openremotewsl.explorer.folder",
|
||||
"group": "navigation@2"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.reopenFolderInCurrentWindow",
|
||||
"when": "viewItem == openremotewsl.explorer.folder",
|
||||
"group": "navigation@1"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.deleteFolderHistoryItem",
|
||||
"when": "viewItem =~ /^openremotewsl.explorer.folder/",
|
||||
"group": "navigation@3"
|
||||
},
|
||||
{
|
||||
"command": "openremotewsl.explorer.deleteFolderHistoryItem",
|
||||
"when": "viewItem =~ /^openremotewsl.explorer.folder/",
|
||||
"group": "inline@2"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"vscode:prepublish": "npm run compile",
|
||||
"compile": "gulp compile-extension:open-remote-wsl",
|
||||
"compile-web": "npx webpack-cli --config extension-browser.webpack.config --mode none",
|
||||
"watch": "gulp watch-extension:open-remote-wsl",
|
||||
"watch-web": "npx webpack-cli --config extension-browser.webpack.config --mode none --watch --info-verbosity verbose"
|
||||
}
|
||||
}
|
||||
121
extensions/open-remote-wsl/src/authResolver.ts
Normal file
121
extensions/open-remote-wsl/src/authResolver.ts
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import Log from './common/logger';
|
||||
import { installCodeServer, ServerInstallError } from './serverSetup';
|
||||
import { WSLManager } from './wsl/wslManager';
|
||||
|
||||
export const REMOTE_WSL_AUTHORITY = 'wsl';
|
||||
|
||||
export function getRemoteAuthority(distro: string) {
|
||||
return `${REMOTE_WSL_AUTHORITY}+${distro}`;
|
||||
}
|
||||
|
||||
class Tunnel implements vscode.Tunnel {
|
||||
private _onDidDisposeEmitter = new vscode.EventEmitter<void>();
|
||||
|
||||
readonly onDidDispose = this._onDidDisposeEmitter.event;
|
||||
|
||||
constructor(
|
||||
readonly remoteAddress: { port: number; host: string },
|
||||
readonly localAddress: { port: number; host: string }
|
||||
) {
|
||||
// If ipv6 localhost 0:0:0:0:0:0:0:1 or [::1] replace with localhost
|
||||
if (localAddress.host !== 'localhost' && localAddress.host !== '127.0.0.1') {
|
||||
localAddress.host = 'localhost';
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._onDidDisposeEmitter.fire();
|
||||
}
|
||||
}
|
||||
|
||||
export class RemoteWSLResolver implements vscode.RemoteAuthorityResolver, vscode.Disposable {
|
||||
|
||||
private labelFormatterDisposable: vscode.Disposable | undefined;
|
||||
|
||||
constructor(
|
||||
private readonly wslManager: WSLManager,
|
||||
private readonly logger: Log
|
||||
) {
|
||||
}
|
||||
|
||||
resolve(authority: string, context: vscode.RemoteAuthorityResolverContext): Thenable<vscode.ResolverResult> {
|
||||
const [type, distroName] = authority.split('+');
|
||||
if (type !== REMOTE_WSL_AUTHORITY) {
|
||||
throw new Error(`Invalid authority type for WSL resolver: ${type}`);
|
||||
}
|
||||
|
||||
this.logger.info(`Resolving wsl remote authority '${authority}' (attemp #${context.resolveAttempt})`);
|
||||
|
||||
// It looks like default values are not loaded yet when resolving a remote,
|
||||
// so let's hardcode the default values here
|
||||
const remoteSSHconfig = vscode.workspace.getConfiguration('remote.WSL');
|
||||
const serverDownloadUrlTemplate = remoteSSHconfig.get<string>('serverDownloadUrlTemplate');
|
||||
|
||||
return vscode.window.withProgress({
|
||||
title: `Setting up WSL Distro: ${distroName}`,
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
cancellable: false
|
||||
}, async () => {
|
||||
try {
|
||||
const installResult = await installCodeServer(this.wslManager, distroName, serverDownloadUrlTemplate, [], [], this.logger);
|
||||
|
||||
this.labelFormatterDisposable?.dispose();
|
||||
this.labelFormatterDisposable = vscode.workspace.registerResourceLabelFormatter({
|
||||
scheme: 'vscode-remote',
|
||||
authority: `${REMOTE_WSL_AUTHORITY}+*`,
|
||||
formatting: {
|
||||
label: '${path}',
|
||||
separator: '/',
|
||||
tildify: true,
|
||||
workspaceSuffix: `WSL: ${distroName}`,
|
||||
workspaceTooltip: `Running in ${distroName}`
|
||||
}
|
||||
});
|
||||
|
||||
return new vscode.ResolvedAuthority('127.0.0.1', installResult.listeningOn, installResult.connectionToken);
|
||||
} catch (e: unknown) {
|
||||
this.logger.error(`Error resolving authority`, e);
|
||||
|
||||
// Initial connection
|
||||
if (context.resolveAttempt === 1) {
|
||||
this.logger.show();
|
||||
|
||||
const closeRemote = 'Close Remote';
|
||||
const retry = 'Retry';
|
||||
const result = await vscode.window.showErrorMessage(`Could not establish connection to WSL distro "${distroName}"`, { modal: true }, closeRemote, retry);
|
||||
if (result === closeRemote) {
|
||||
await vscode.commands.executeCommand('workbench.action.remote.close');
|
||||
} else if (result === retry) {
|
||||
await vscode.commands.executeCommand('workbench.action.reloadWindow');
|
||||
}
|
||||
}
|
||||
|
||||
if (e instanceof ServerInstallError || !(e instanceof Error)) {
|
||||
throw vscode.RemoteAuthorityResolverError.NotAvailable(e instanceof Error ? e.message : String(e));
|
||||
} else {
|
||||
throw vscode.RemoteAuthorityResolverError.TemporarilyNotAvailable(e.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async tunnelFactory(tunnelOptions: vscode.TunnelOptions) {
|
||||
return new Tunnel(
|
||||
tunnelOptions.remoteAddress,
|
||||
{
|
||||
host: tunnelOptions.remoteAddress.host,
|
||||
port: tunnelOptions.localAddressPort ?? tunnelOptions.remoteAddress.port
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.labelFormatterDisposable?.dispose();
|
||||
}
|
||||
}
|
||||
86
extensions/open-remote-wsl/src/commands.ts
Normal file
86
extensions/open-remote-wsl/src/commands.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { getRemoteAuthority } from './authResolver';
|
||||
import { WSLDistro, WSLManager, WSLOnlineDistro } from './wsl/wslManager';
|
||||
import wslTerminal from './wsl/wslTerminal';
|
||||
|
||||
async function showDistrosPicker(wslManager: WSLManager, placeHolder: string): Promise<WSLDistro | undefined> {
|
||||
const pickItemsPromise = wslManager.listDistros()
|
||||
.then(distros => distros.map(distroData => {
|
||||
return {
|
||||
...distroData,
|
||||
label: `${distroData.name}`,
|
||||
detail: distroData.isDefault ? 'default distro' : undefined,
|
||||
};
|
||||
}));
|
||||
|
||||
const picked = await vscode.window.showQuickPick(pickItemsPromise, { canPickMany: false, placeHolder });
|
||||
return picked;
|
||||
}
|
||||
|
||||
async function showOnlineDistrosPicker(wslManager: WSLManager, placeHolder: string): Promise<WSLOnlineDistro | undefined> {
|
||||
const pickItemsPromise = Promise.all([wslManager.listOnlineDistros(), wslManager.listDistros()])
|
||||
.then(([onlineDistros, localDistros]) => {
|
||||
const distroToInstall = onlineDistros.filter(d => !localDistros.some(l => l.name === d.name));
|
||||
return distroToInstall.map(distroData => {
|
||||
return {
|
||||
...distroData,
|
||||
label: `${distroData.friendlyName}`,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
const picked = await vscode.window.showQuickPick(pickItemsPromise, { canPickMany: false, placeHolder });
|
||||
return picked;
|
||||
}
|
||||
|
||||
export async function promptOpenRemoteWSLWindow(wslManager: WSLManager, useDefault: boolean, reuseWindow: boolean) {
|
||||
let distroName: string | undefined;
|
||||
if (useDefault) {
|
||||
const distros = await wslManager.listDistros();
|
||||
distroName = distros.find(distro => distro.isDefault)?.name;
|
||||
} else {
|
||||
distroName = (await showDistrosPicker(wslManager, 'Select WSL distro'))?.name;
|
||||
}
|
||||
|
||||
if (!distroName) {
|
||||
return;
|
||||
}
|
||||
|
||||
openRemoteWSLWindow(distroName, reuseWindow);
|
||||
}
|
||||
|
||||
export async function promptInstallNewWSLDistro(wslManager: WSLManager) {
|
||||
const distroName = (await showOnlineDistrosPicker(wslManager, 'Select the WSL distro to install'))?.name;
|
||||
if (!distroName) {
|
||||
return;
|
||||
}
|
||||
|
||||
wslTerminal.runCommand(`wsl.exe --install -d ${distroName}`);
|
||||
}
|
||||
|
||||
export function openRemoteWSLWindow(distro: string, reuseWindow: boolean) {
|
||||
vscode.commands.executeCommand('vscode.newWindow', { remoteAuthority: getRemoteAuthority(distro), reuseWindow });
|
||||
}
|
||||
|
||||
export function openRemoteWSLLocationWindow(distro: string, path: string, reuseWindow: boolean) {
|
||||
vscode.commands.executeCommand('vscode.openFolder', vscode.Uri.from({ scheme: 'vscode-remote', authority: getRemoteAuthority(distro), path }), { forceNewWindow: !reuseWindow });
|
||||
}
|
||||
|
||||
export async function setDefaultWSLDistro(wslManager: WSLManager, distroName: string) {
|
||||
await wslManager.setDefaultDistro(distroName);
|
||||
}
|
||||
|
||||
export async function deleteWSLDistro(wslManager: WSLManager, distroName: string) {
|
||||
const deleteAction = 'Delete';
|
||||
const resp = await vscode.window.showInformationMessage(`Are you sure you want to permanently delete the distro "${distroName}" including all its data?`, { modal: true }, deleteAction);
|
||||
if (resp === deleteAction) {
|
||||
await wslManager.deleteDistro(distroName);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
28
extensions/open-remote-wsl/src/common/async.ts
Normal file
28
extensions/open-remote-wsl/src/common/async.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export function timeout(millis: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, millis));
|
||||
}
|
||||
|
||||
export interface ITask<T> {
|
||||
(): T;
|
||||
}
|
||||
|
||||
export async function retry<T>(task: ITask<Promise<T>>, delay: number, retries: number): Promise<T> {
|
||||
let lastError: Error | undefined;
|
||||
|
||||
for (let i = 0; i < retries; i++) {
|
||||
try {
|
||||
return await task();
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
|
||||
await timeout(delay);
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError;
|
||||
}
|
||||
42
extensions/open-remote-wsl/src/common/disposable.ts
Normal file
42
extensions/open-remote-wsl/src/common/disposable.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
export function disposeAll(disposables: vscode.Disposable[]): void {
|
||||
while (disposables.length) {
|
||||
const item = disposables.pop();
|
||||
if (item) {
|
||||
item.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class Disposable {
|
||||
private _isDisposed = false;
|
||||
|
||||
protected _disposables: vscode.Disposable[] = [];
|
||||
|
||||
public dispose(): any {
|
||||
if (this._isDisposed) {
|
||||
return;
|
||||
}
|
||||
this._isDisposed = true;
|
||||
disposeAll(this._disposables);
|
||||
}
|
||||
|
||||
protected _register<T extends vscode.Disposable>(value: T): T {
|
||||
if (this._isDisposed) {
|
||||
value.dispose();
|
||||
} else {
|
||||
this._disposables.push(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
protected get isDisposed(): boolean {
|
||||
return this._isDisposed;
|
||||
}
|
||||
}
|
||||
142
extensions/open-remote-wsl/src/common/event.ts
Normal file
142
extensions/open-remote-wsl/src/common/event.ts
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export interface IDisposable {
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
export interface Event<T> {
|
||||
(listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[]): IDisposable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a promise that resolves when the event fires, or when cancellation
|
||||
* is requested, whichever happens first.
|
||||
*/
|
||||
export function toPromise<T>(event: Event<T>): Promise<T>;
|
||||
export function toPromise<T>(event: Event<T>, signal: AbortSignal): Promise<T | undefined>;
|
||||
export function toPromise<T>(event: Event<T>, signal?: AbortSignal): Promise<T | undefined> {
|
||||
if (!signal) {
|
||||
return new Promise<T>((resolve) => once(event, resolve));
|
||||
}
|
||||
|
||||
if (signal.aborted) {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const d2 = once(event, (data) => {
|
||||
(signal as any).removeEventListener('abort', d1);
|
||||
resolve(data);
|
||||
});
|
||||
|
||||
const d1 = () => {
|
||||
d2.dispose();
|
||||
(signal as any).removeEventListener('abort', d1);
|
||||
resolve(undefined);
|
||||
};
|
||||
|
||||
(signal as any).addEventListener('abort', d1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a handler that handles one event on the emitter, then disposes itself.
|
||||
*/
|
||||
export const once = <T>(event: Event<T>, listener: (data: T) => void): IDisposable => {
|
||||
const disposable = event((value) => {
|
||||
listener(value);
|
||||
disposable.dispose();
|
||||
});
|
||||
|
||||
return disposable;
|
||||
};
|
||||
|
||||
/**
|
||||
* Base event emitter. Calls listeners when data is emitted.
|
||||
*/
|
||||
export class EventEmitter<T> {
|
||||
private listeners?: Array<(data: T) => void> | ((data: T) => void);
|
||||
|
||||
/**
|
||||
* Event<T> function.
|
||||
*/
|
||||
public readonly event: Event<T> = (listener, thisArgs, disposables) => {
|
||||
const d = this.add(thisArgs ? listener.bind(thisArgs) : listener);
|
||||
disposables?.push(d);
|
||||
return d;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the number of event listeners.
|
||||
*/
|
||||
public get size() {
|
||||
if (!this.listeners) {
|
||||
return 0;
|
||||
} else if (typeof this.listeners === 'function') {
|
||||
return 1;
|
||||
} else {
|
||||
return this.listeners.length;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits event data.
|
||||
*/
|
||||
public fire(value: T) {
|
||||
if (!this.listeners) {
|
||||
// no-op
|
||||
} else if (typeof this.listeners === 'function') {
|
||||
this.listeners(value);
|
||||
} else {
|
||||
for (const listener of this.listeners) {
|
||||
listener(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes of the emitter.
|
||||
*/
|
||||
public dispose() {
|
||||
this.listeners = undefined;
|
||||
}
|
||||
|
||||
private add(listener: (data: T) => void): IDisposable {
|
||||
if (!this.listeners) {
|
||||
this.listeners = listener;
|
||||
} else if (typeof this.listeners === 'function') {
|
||||
this.listeners = [this.listeners, listener];
|
||||
} else {
|
||||
this.listeners.push(listener);
|
||||
}
|
||||
|
||||
return { dispose: () => this.rm(listener) };
|
||||
}
|
||||
|
||||
private rm(listener: (data: T) => void) {
|
||||
if (!this.listeners) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof this.listeners === 'function') {
|
||||
if (this.listeners === listener) {
|
||||
this.listeners = undefined;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const index = this.listeners.indexOf(listener);
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.listeners.length === 2) {
|
||||
this.listeners = index === 0 ? this.listeners[1] : this.listeners[0];
|
||||
} else {
|
||||
this.listeners = this.listeners.slice(0, index).concat(this.listeners.slice(index + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
26
extensions/open-remote-wsl/src/common/files.ts
Normal file
26
extensions/open-remote-wsl/src/common/files.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
|
||||
const homeDir = os.homedir();
|
||||
|
||||
export async function exists(path: string) {
|
||||
try {
|
||||
await fs.promises.access(path);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function untildify(path: string) {
|
||||
return path.replace(/^~(?=$|\/|\\)/, homeDir);
|
||||
}
|
||||
|
||||
export function normalizeToSlash(path: string) {
|
||||
return path.replace(/\\/g, '/');
|
||||
}
|
||||
64
extensions/open-remote-wsl/src/common/logger.ts
Normal file
64
extensions/open-remote-wsl/src/common/logger.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
type LogLevel = 'Trace' | 'Info' | 'Error';
|
||||
|
||||
export default class Log {
|
||||
private output: vscode.OutputChannel;
|
||||
|
||||
constructor(name: string) {
|
||||
this.output = vscode.window.createOutputChannel(name);
|
||||
}
|
||||
|
||||
private data2String(data: any): string {
|
||||
if (data instanceof Error) {
|
||||
return data.stack || data.message;
|
||||
}
|
||||
if (data.success === false && data.message) {
|
||||
return data.message;
|
||||
}
|
||||
return data.toString();
|
||||
}
|
||||
|
||||
public trace(message: string, data?: any): void {
|
||||
this.logLevel('Trace', message, data);
|
||||
}
|
||||
|
||||
public info(message: string, data?: any): void {
|
||||
this.logLevel('Info', message, data);
|
||||
}
|
||||
|
||||
public error(message: string, data?: any): void {
|
||||
this.logLevel('Error', message, data);
|
||||
}
|
||||
|
||||
public logLevel(level: LogLevel, message: string, data?: any): void {
|
||||
this.output.appendLine(`[${level} - ${this.now()}] ${message}`);
|
||||
if (data) {
|
||||
this.output.appendLine(this.data2String(data));
|
||||
}
|
||||
}
|
||||
|
||||
private now(): string {
|
||||
const now = new Date();
|
||||
return padLeft(now.getUTCHours() + '', 2, '0')
|
||||
+ ':' + padLeft(now.getMinutes() + '', 2, '0')
|
||||
+ ':' + padLeft(now.getUTCSeconds() + '', 2, '0') + '.' + now.getMilliseconds();
|
||||
}
|
||||
|
||||
public show() {
|
||||
this.output.show();
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
this.output.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
function padLeft(s: string, n: number, pad = ' ') {
|
||||
return pad.repeat(Math.max(0, n - s.length)) + s;
|
||||
}
|
||||
8
extensions/open-remote-wsl/src/common/platform.ts
Normal file
8
extensions/open-remote-wsl/src/common/platform.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export const isWindows = process.platform === 'win32';
|
||||
export const isMacintosh = process.platform === 'darwin';
|
||||
export const isLinux = process.platform === 'linux';
|
||||
134
extensions/open-remote-wsl/src/common/ports.ts
Normal file
134
extensions/open-remote-wsl/src/common/ports.ts
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as net from 'net';
|
||||
|
||||
/**
|
||||
* Finds a random unused port assigned by the operating system. Will reject in case no free port can be found.
|
||||
*/
|
||||
export function findRandomPort(): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const server = net.createServer({ pauseOnConnect: true });
|
||||
server.on('error', reject);
|
||||
server.on('listening', () => {
|
||||
const port = (server.address() as net.AddressInfo).port;
|
||||
server.close(() => resolve(port));
|
||||
});
|
||||
server.listen(0, '127.0.0.1');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a start point and a max number of retries, will find a port that
|
||||
* is openable. Will return 0 in case no free port can be found.
|
||||
*/
|
||||
export function findFreePort(startPort: number, giveUpAfter: number, timeout: number, stride = 1): Promise<number> {
|
||||
let done = false;
|
||||
|
||||
return new Promise(resolve => {
|
||||
const timeoutHandle = setTimeout(() => {
|
||||
if (!done) {
|
||||
done = true;
|
||||
return resolve(0);
|
||||
}
|
||||
}, timeout);
|
||||
|
||||
doFindFreePort(startPort, giveUpAfter, stride, (port) => {
|
||||
if (!done) {
|
||||
done = true;
|
||||
clearTimeout(timeoutHandle);
|
||||
return resolve(port);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function doFindFreePort(startPort: number, giveUpAfter: number, stride: number, clb: (port: number) => void): void {
|
||||
if (giveUpAfter === 0) {
|
||||
return clb(0);
|
||||
}
|
||||
|
||||
const client = new net.Socket();
|
||||
|
||||
// If we can connect to the port it means the port is already taken so we continue searching
|
||||
client.once('connect', () => {
|
||||
dispose(client);
|
||||
|
||||
return doFindFreePort(startPort + stride, giveUpAfter - 1, stride, clb);
|
||||
});
|
||||
|
||||
client.once('data', () => {
|
||||
// this listener is required since node.js 8.x
|
||||
});
|
||||
|
||||
client.once('error', (err: Error & { code?: string }) => {
|
||||
dispose(client);
|
||||
|
||||
// If we receive any non ECONNREFUSED error, it means the port is used but we cannot connect
|
||||
if (err.code !== 'ECONNREFUSED') {
|
||||
return doFindFreePort(startPort + stride, giveUpAfter - 1, stride, clb);
|
||||
}
|
||||
|
||||
// Otherwise it means the port is free to use!
|
||||
return clb(startPort);
|
||||
});
|
||||
|
||||
client.connect(startPort, '127.0.0.1');
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses listen instead of connect. Is faster, but if there is another listener on 0.0.0.0 then this will take 127.0.0.1 from that listener.
|
||||
*/
|
||||
export function findFreePortFaster(startPort: number, giveUpAfter: number, timeout: number): Promise<number> {
|
||||
let resolved = false;
|
||||
let timeoutHandle: NodeJS.Timeout | undefined = undefined;
|
||||
let countTried = 1;
|
||||
const server = net.createServer({ pauseOnConnect: true });
|
||||
function doResolve(port: number, resolve: (port: number) => void) {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
server.removeAllListeners();
|
||||
server.close();
|
||||
if (timeoutHandle) {
|
||||
clearTimeout(timeoutHandle);
|
||||
}
|
||||
resolve(port);
|
||||
}
|
||||
}
|
||||
return new Promise<number>(resolve => {
|
||||
timeoutHandle = setTimeout(() => {
|
||||
doResolve(0, resolve);
|
||||
}, timeout);
|
||||
|
||||
server.on('listening', () => {
|
||||
doResolve(startPort, resolve);
|
||||
});
|
||||
server.on('error', err => {
|
||||
if (err && ((<any>err).code === 'EADDRINUSE' || (<any>err).code === 'EACCES') && (countTried < giveUpAfter)) {
|
||||
startPort++;
|
||||
countTried++;
|
||||
server.listen(startPort, '127.0.0.1');
|
||||
} else {
|
||||
doResolve(0, resolve);
|
||||
}
|
||||
});
|
||||
server.on('close', () => {
|
||||
doResolve(0, resolve);
|
||||
});
|
||||
server.listen(startPort, '127.0.0.1');
|
||||
});
|
||||
}
|
||||
|
||||
function dispose(socket: net.Socket): void {
|
||||
try {
|
||||
socket.removeAllListeners('connect');
|
||||
socket.removeAllListeners('error');
|
||||
socket.end();
|
||||
socket.destroy();
|
||||
socket.unref();
|
||||
} catch (error) {
|
||||
console.error(error); // otherwise this error would get lost in the callback chain
|
||||
}
|
||||
}
|
||||
109
extensions/open-remote-wsl/src/distroTreeView.ts
Normal file
109
extensions/open-remote-wsl/src/distroTreeView.ts
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import { RemoteLocationHistory } from './remoteLocationHistory';
|
||||
import { Disposable } from './common/disposable';
|
||||
import { openRemoteWSLWindow, openRemoteWSLLocationWindow, promptInstallNewWSLDistro, deleteWSLDistro, setDefaultWSLDistro } from './commands';
|
||||
import { WSLManager } from './wsl/wslManager';
|
||||
|
||||
class DistroItem {
|
||||
constructor(
|
||||
public name: string,
|
||||
public isDefault: boolean,
|
||||
public locations: string[]
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
class DistroLocationItem {
|
||||
constructor(
|
||||
public path: string,
|
||||
public name: string
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
type DataTreeItem = DistroItem | DistroLocationItem;
|
||||
|
||||
export class DistroTreeDataProvider extends Disposable implements vscode.TreeDataProvider<DataTreeItem> {
|
||||
|
||||
private readonly _onDidChangeTreeData = this._register(new vscode.EventEmitter<DataTreeItem | DataTreeItem[] | void>());
|
||||
public readonly onDidChangeTreeData = this._onDidChangeTreeData.event;
|
||||
|
||||
constructor(
|
||||
private readonly locationHistory: RemoteLocationHistory,
|
||||
private readonly wslManager: WSLManager
|
||||
) {
|
||||
super();
|
||||
|
||||
this._register(vscode.commands.registerCommand('openremotewsl.explorer.addDistro', () => promptInstallNewWSLDistro(wslManager)));
|
||||
this._register(vscode.commands.registerCommand('openremotewsl.explorer.refresh', () => this.refresh()));
|
||||
this._register(vscode.commands.registerCommand('openremotewsl.explorer.emptyWindowInNewWindow', e => this.openRemoteWSLWindow(e, false)));
|
||||
this._register(vscode.commands.registerCommand('openremotewsl.explorer.emptyWindowInCurrentWindow', e => this.openRemoteWSLWindow(e, true)));
|
||||
this._register(vscode.commands.registerCommand('openremotewsl.explorer.reopenFolderInNewWindow', e => this.openRemoteWSLocationWindow(e, false)));
|
||||
this._register(vscode.commands.registerCommand('openremotewsl.explorer.reopenFolderInCurrentWindow', e => this.openRemoteWSLocationWindow(e, true)));
|
||||
this._register(vscode.commands.registerCommand('openremotewsl.explorer.deleteFolderHistoryItem', e => this.deleteDistroLocation(e)));
|
||||
this._register(vscode.commands.registerCommand('openremotewsl.explorer.setDefaultDistro', e => this.setDefaultDistro(e)));
|
||||
this._register(vscode.commands.registerCommand('openremotewsl.explorer.deleteDistro', e => this.deleteDistro(e)));
|
||||
}
|
||||
|
||||
getTreeItem(element: DataTreeItem): vscode.TreeItem {
|
||||
if (element instanceof DistroLocationItem) {
|
||||
const label = path.posix.basename(element.path).replace(/\.code-workspace$/, ' (Workspace)');
|
||||
const treeItem = new vscode.TreeItem(label);
|
||||
treeItem.description = path.posix.dirname(element.path);
|
||||
treeItem.iconPath = new vscode.ThemeIcon('folder');
|
||||
treeItem.contextValue = 'openremotewsl.explorer.folder';
|
||||
return treeItem;
|
||||
}
|
||||
|
||||
const treeItem = new vscode.TreeItem(element.name);
|
||||
treeItem.description = element.isDefault ? 'default distro' : undefined;
|
||||
treeItem.collapsibleState = element.locations.length ? vscode.TreeItemCollapsibleState.Collapsed : vscode.TreeItemCollapsibleState.None;
|
||||
treeItem.iconPath = new vscode.ThemeIcon('vm');
|
||||
treeItem.contextValue = 'openremotewsl.explorer.distro';
|
||||
return treeItem;
|
||||
}
|
||||
|
||||
async getChildren(element?: DistroItem): Promise<DataTreeItem[]> {
|
||||
if (!element) {
|
||||
const distros = await this.wslManager.listDistros();
|
||||
return distros.map(distro => new DistroItem(distro.name, distro.isDefault, this.locationHistory.getHistory(distro.name)));
|
||||
}
|
||||
if (element instanceof DistroItem) {
|
||||
return element.locations.map(location => new DistroLocationItem(location, element.name));
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
private refresh() {
|
||||
this._onDidChangeTreeData.fire();
|
||||
}
|
||||
|
||||
private async deleteDistroLocation(element: DistroLocationItem) {
|
||||
await this.locationHistory.removeLocation(element.name, element.path);
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
private async openRemoteWSLWindow(element: DistroItem, reuseWindow: boolean) {
|
||||
openRemoteWSLWindow(element.name, reuseWindow);
|
||||
}
|
||||
|
||||
private async openRemoteWSLocationWindow(element: DistroLocationItem, reuseWindow: boolean) {
|
||||
openRemoteWSLLocationWindow(element.name, element.path, reuseWindow);
|
||||
}
|
||||
|
||||
private async setDefaultDistro(element: DistroItem) {
|
||||
await setDefaultWSLDistro(this.wslManager, element.name);
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
private async deleteDistro(element: DistroItem) {
|
||||
await deleteWSLDistro(this.wslManager, element.name);
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
46
extensions/open-remote-wsl/src/extension.ts
Normal file
46
extensions/open-remote-wsl/src/extension.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import Log from './common/logger';
|
||||
import { RemoteWSLResolver, REMOTE_WSL_AUTHORITY } from './authResolver';
|
||||
import { promptOpenRemoteWSLWindow } from './commands';
|
||||
import { DistroTreeDataProvider } from './distroTreeView';
|
||||
import { getRemoteWorkspaceLocationData, RemoteLocationHistory } from './remoteLocationHistory';
|
||||
import { WSLManager } from './wsl/wslManager';
|
||||
import { isWindows } from './common/platform';
|
||||
|
||||
export async function activate(context: vscode.ExtensionContext) {
|
||||
if (!isWindows) {
|
||||
return;
|
||||
}
|
||||
|
||||
const logger = new Log('Remote - WSL');
|
||||
context.subscriptions.push(logger);
|
||||
|
||||
const wslManager = new WSLManager(logger);
|
||||
const remoteWSLResolver = new RemoteWSLResolver(wslManager, logger);
|
||||
context.subscriptions.push(vscode.workspace.registerRemoteAuthorityResolver(REMOTE_WSL_AUTHORITY, remoteWSLResolver));
|
||||
context.subscriptions.push(remoteWSLResolver);
|
||||
|
||||
const locationHistory = new RemoteLocationHistory(context);
|
||||
const locationData = getRemoteWorkspaceLocationData();
|
||||
if (locationData) {
|
||||
await locationHistory.addLocation(locationData[0], locationData[1]);
|
||||
}
|
||||
|
||||
const distroTreeDataProvider = new DistroTreeDataProvider(locationHistory, wslManager);
|
||||
context.subscriptions.push(vscode.window.createTreeView('wslTargets', { treeDataProvider: distroTreeDataProvider }));
|
||||
context.subscriptions.push(distroTreeDataProvider);
|
||||
|
||||
context.subscriptions.push(vscode.commands.registerCommand('openremotewsl.connect', () => promptOpenRemoteWSLWindow(wslManager, true, true)));
|
||||
context.subscriptions.push(vscode.commands.registerCommand('openremotewsl.connectInNewWindow', () => promptOpenRemoteWSLWindow(wslManager, true, false)));
|
||||
context.subscriptions.push(vscode.commands.registerCommand('openremotewsl.connectUsingDistro', () => promptOpenRemoteWSLWindow(wslManager, false, true)));
|
||||
context.subscriptions.push(vscode.commands.registerCommand('openremotewsl.connectUsingDistroInNewWindow', () => promptOpenRemoteWSLWindow(wslManager, false, false)));
|
||||
context.subscriptions.push(vscode.commands.registerCommand('openremotewsl.showLog', () => logger.show()));
|
||||
}
|
||||
|
||||
export function deactivate() {
|
||||
}
|
||||
56
extensions/open-remote-wsl/src/remoteLocationHistory.ts
Normal file
56
extensions/open-remote-wsl/src/remoteLocationHistory.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { REMOTE_WSL_AUTHORITY } from './authResolver';
|
||||
|
||||
export class RemoteLocationHistory {
|
||||
private static STORAGE_KEY = 'remoteLocationHistory_v0';
|
||||
|
||||
private remoteLocationHistory: Record<string, string[]> = {};
|
||||
|
||||
constructor(private context: vscode.ExtensionContext) {
|
||||
// context.globalState.update(RemoteLocationHistory.STORAGE_KEY, undefined);
|
||||
this.remoteLocationHistory = context.globalState.get(RemoteLocationHistory.STORAGE_KEY) || {};
|
||||
}
|
||||
|
||||
getHistory(host: string): string[] {
|
||||
return this.remoteLocationHistory[host] || [];
|
||||
}
|
||||
|
||||
async addLocation(host: string, path: string) {
|
||||
const hostLocations = this.remoteLocationHistory[host] || [];
|
||||
if (!hostLocations.includes(path)) {
|
||||
hostLocations.unshift(path);
|
||||
this.remoteLocationHistory[host] = hostLocations;
|
||||
|
||||
await this.context.globalState.update(RemoteLocationHistory.STORAGE_KEY, this.remoteLocationHistory);
|
||||
}
|
||||
}
|
||||
|
||||
async removeLocation(host: string, path: string) {
|
||||
let hostLocations = this.remoteLocationHistory[host] || [];
|
||||
hostLocations = hostLocations.filter(l => l !== path);
|
||||
this.remoteLocationHistory[host] = hostLocations;
|
||||
|
||||
await this.context.globalState.update(RemoteLocationHistory.STORAGE_KEY, this.remoteLocationHistory);
|
||||
}
|
||||
}
|
||||
|
||||
export function getRemoteWorkspaceLocationData(): [string, string] | undefined {
|
||||
let location = vscode.workspace.workspaceFile;
|
||||
if (location && location.scheme === 'vscode-remote' && location.authority.startsWith(REMOTE_WSL_AUTHORITY) && location.path.endsWith('.code-workspace')) {
|
||||
const [, distroName] = location.authority.split('+');
|
||||
return [distroName, location.path];
|
||||
}
|
||||
|
||||
location = vscode.workspace.workspaceFolders?.[0].uri;
|
||||
if (location && location.scheme === 'vscode-remote' && location.authority.startsWith(REMOTE_WSL_AUTHORITY)) {
|
||||
const [, distroName] = location.authority.split('+');
|
||||
return [distroName, location.path];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
44
extensions/open-remote-wsl/src/serverConfig.ts
Normal file
44
extensions/open-remote-wsl/src/serverConfig.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
let vscodeProductJson: any;
|
||||
async function getVSCodeProductJson() {
|
||||
if (!vscodeProductJson) {
|
||||
const productJsonStr = await fs.promises.readFile(path.join(vscode.env.appRoot, 'product.json'), 'utf8');
|
||||
vscodeProductJson = JSON.parse(productJsonStr);
|
||||
}
|
||||
|
||||
return vscodeProductJson;
|
||||
}
|
||||
|
||||
export interface IServerConfig {
|
||||
version: string;
|
||||
commit: string;
|
||||
quality: string;
|
||||
release?: string; // void-like specific
|
||||
serverApplicationName: string;
|
||||
serverDataFolderName: string;
|
||||
serverDownloadUrlTemplate?: string; // void-like specific
|
||||
}
|
||||
|
||||
export async function getVSCodeServerConfig(): Promise<IServerConfig> {
|
||||
const productJson = await getVSCodeProductJson();
|
||||
|
||||
return {
|
||||
// version: vscode.version.replace('-insider', ''),
|
||||
commit: productJson.commit,
|
||||
quality: productJson.quality,
|
||||
release: productJson.release,
|
||||
serverApplicationName: productJson.serverApplicationName,
|
||||
serverDataFolderName: productJson.serverDataFolderName,
|
||||
serverDownloadUrlTemplate: productJson.serverDownloadUrlTemplate,
|
||||
// Void changed this
|
||||
version: productJson.voidVersion
|
||||
};
|
||||
}
|
||||
353
extensions/open-remote-wsl/src/serverSetup.ts
Normal file
353
extensions/open-remote-wsl/src/serverSetup.ts
Normal file
|
|
@ -0,0 +1,353 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as crypto from 'crypto';
|
||||
import Log from './common/logger';
|
||||
import { getVSCodeServerConfig } from './serverConfig';
|
||||
import { WSLManager } from './wsl/wslManager';
|
||||
|
||||
export interface ServerInstallOptions {
|
||||
id: string;
|
||||
quality: string;
|
||||
commit: string;
|
||||
version: string;
|
||||
release?: string; // void specific
|
||||
extensionIds: string[];
|
||||
envVariables: string[];
|
||||
serverApplicationName: string;
|
||||
serverDataFolderName: string;
|
||||
serverDownloadUrlTemplate: string;
|
||||
}
|
||||
|
||||
export interface ServerInstallResult {
|
||||
exitCode: number;
|
||||
listeningOn: number;
|
||||
connectionToken: string;
|
||||
logFile: string;
|
||||
osReleaseId: string;
|
||||
arch: string;
|
||||
platform: string;
|
||||
tmpDir: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export class ServerInstallError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_DOWNLOAD_URL_TEMPLATE = 'https://github.com/voideditor/binaries/releases/download/${version}.${release}/void-reh-${os}-${arch}-${version}.${release}.tar.gz';
|
||||
|
||||
export async function installCodeServer(wslManager: WSLManager, distroName: string, serverDownloadUrlTemplate: string | undefined, extensionIds: string[], envVariables: string[], logger: Log): Promise<ServerInstallResult> {
|
||||
const scriptId = crypto.randomBytes(12).toString('hex');
|
||||
|
||||
const vscodeServerConfig = await getVSCodeServerConfig();
|
||||
const installOptions: ServerInstallOptions = {
|
||||
id: scriptId,
|
||||
version: vscodeServerConfig.version,
|
||||
commit: vscodeServerConfig.commit,
|
||||
quality: vscodeServerConfig.quality,
|
||||
release: vscodeServerConfig.release,
|
||||
extensionIds,
|
||||
envVariables,
|
||||
serverApplicationName: vscodeServerConfig.serverApplicationName,
|
||||
serverDataFolderName: vscodeServerConfig.serverDataFolderName,
|
||||
serverDownloadUrlTemplate: serverDownloadUrlTemplate ?? vscodeServerConfig.serverDownloadUrlTemplate ?? DEFAULT_DOWNLOAD_URL_TEMPLATE,
|
||||
};
|
||||
|
||||
const installServerScript = generateBashInstallScript(installOptions);
|
||||
|
||||
// Fish shell does not support heredoc so let's workaround it using -c option,
|
||||
// also replace single quotes (') within the script with ('\'') as there's no quoting within single quotes, see https://unix.stackexchange.com/a/24676
|
||||
const resp = await wslManager.exec('bash', ['-c', `'${installServerScript.replace(/'/g, `'\\''`)}'`], distroName);
|
||||
|
||||
const endScriptRegex = new RegExp(`${scriptId}: Server installation script done`, 'm');
|
||||
const commandOutput = await Promise.race([
|
||||
resp.exitPromise.then(result => ({ stdout: resp.stdout, stderr: resp.stderr, exitCode: result.exitCode })),
|
||||
new Promise<{ stdout: string; stderr: string; exitCode: number }>((resolve) => {
|
||||
resp.onStdoutData(buffer => {
|
||||
if (endScriptRegex.test(buffer.toString('utf8'))) {
|
||||
resolve({ stdout: resp.stdout, stderr: resp.stderr, exitCode: 0 });
|
||||
}
|
||||
});
|
||||
})
|
||||
]);
|
||||
|
||||
if (commandOutput.exitCode) {
|
||||
logger.trace('Server install command stderr:', commandOutput.stderr);
|
||||
}
|
||||
logger.trace('Server install command stdout:', commandOutput.stdout);
|
||||
|
||||
const resultMap = parseServerInstallOutput(commandOutput.stdout, scriptId);
|
||||
if (!resultMap) {
|
||||
throw new ServerInstallError(`Failed parsing install script output`);
|
||||
}
|
||||
|
||||
const exitCode = parseInt(resultMap.exitCode, 10);
|
||||
if (exitCode !== 0) {
|
||||
throw new ServerInstallError(`Couldn't install void server on remote server, install script returned non-zero exit status`);
|
||||
}
|
||||
|
||||
const listeningOn = parseInt(resultMap.listeningOn, 10);
|
||||
|
||||
const remoteEnvVars = Object.fromEntries(Object.entries(resultMap).filter(([key,]) => envVariables.includes(key)));
|
||||
|
||||
return {
|
||||
exitCode,
|
||||
listeningOn,
|
||||
connectionToken: resultMap.connectionToken,
|
||||
logFile: resultMap.logFile,
|
||||
osReleaseId: resultMap.osReleaseId,
|
||||
arch: resultMap.arch,
|
||||
platform: resultMap.platform,
|
||||
tmpDir: resultMap.tmpDir,
|
||||
...remoteEnvVars
|
||||
};
|
||||
}
|
||||
|
||||
function parseServerInstallOutput(str: string, scriptId: string): { [k: string]: string } | undefined {
|
||||
const startResultStr = `${scriptId}: start`;
|
||||
const endResultStr = `${scriptId}: end`;
|
||||
|
||||
const startResultIdx = str.indexOf(startResultStr);
|
||||
if (startResultIdx < 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const endResultIdx = str.indexOf(endResultStr, startResultIdx + startResultStr.length);
|
||||
if (endResultIdx < 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const installResult = str.substring(startResultIdx + startResultStr.length, endResultIdx);
|
||||
|
||||
const resultMap: { [k: string]: string } = {};
|
||||
const resultArr = installResult.split(/\r?\n/);
|
||||
for (const line of resultArr) {
|
||||
const [key, value] = line.split('==');
|
||||
resultMap[key] = value;
|
||||
}
|
||||
|
||||
return resultMap;
|
||||
}
|
||||
|
||||
function generateBashInstallScript({ id, quality, version, commit, release, extensionIds, envVariables, serverApplicationName, serverDataFolderName, serverDownloadUrlTemplate }: ServerInstallOptions) {
|
||||
const extensions = extensionIds.map(id => '--install-extension ' + id).join(' ');
|
||||
return `
|
||||
# Server installation script
|
||||
|
||||
TMP_DIR="\${XDG_RUNTIME_DIR:-"/tmp"}"
|
||||
|
||||
DISTRO_VERSION="${version}"
|
||||
DISTRO_COMMIT="${commit}"
|
||||
DISTRO_QUALITY="${quality}"
|
||||
DISTRO_VSCODIUM_RELEASE="${release ?? ''}"
|
||||
|
||||
SERVER_APP_NAME="${serverApplicationName}"
|
||||
SERVER_INITIAL_EXTENSIONS="${extensions}"
|
||||
SERVER_LISTEN_FLAG="--port=0"
|
||||
SERVER_DATA_DIR="$HOME/${serverDataFolderName}"
|
||||
SERVER_DIR="$SERVER_DATA_DIR/bin/$DISTRO_COMMIT"
|
||||
SERVER_SCRIPT="$SERVER_DIR/bin/$SERVER_APP_NAME"
|
||||
SERVER_LOGFILE="$SERVER_DATA_DIR/.$DISTRO_COMMIT.log"
|
||||
SERVER_PIDFILE="$SERVER_DATA_DIR/.$DISTRO_COMMIT.pid"
|
||||
SERVER_TOKENFILE="$SERVER_DATA_DIR/.$DISTRO_COMMIT.token"
|
||||
SERVER_OS=
|
||||
SERVER_ARCH=
|
||||
SERVER_CONNECTION_TOKEN=
|
||||
SERVER_DOWNLOAD_URL=
|
||||
|
||||
LISTENING_ON=
|
||||
OS_RELEASE_ID=
|
||||
ARCH=
|
||||
PLATFORM=
|
||||
|
||||
# Mimic output from logs of remote-ssh extension
|
||||
print_install_results_and_exit() {
|
||||
echo "${id}: start"
|
||||
echo "exitCode==$1=="
|
||||
echo "listeningOn==$LISTENING_ON=="
|
||||
echo "connectionToken==$SERVER_CONNECTION_TOKEN=="
|
||||
echo "logFile==$SERVER_LOGFILE=="
|
||||
echo "osReleaseId==$OS_RELEASE_ID=="
|
||||
echo "arch==$ARCH=="
|
||||
echo "platform==$PLATFORM=="
|
||||
echo "tmpDir==$TMP_DIR=="
|
||||
${envVariables.map(envVar => `echo "${envVar}==$${envVar}=="`).join('\n')}
|
||||
echo "${id}: end"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Check if platform is supported
|
||||
PLATFORM="$(uname -s)"
|
||||
case $PLATFORM in
|
||||
Linux)
|
||||
SERVER_OS="linux"
|
||||
;;
|
||||
*)
|
||||
echo "Error platform not supported: $PLATFORM"
|
||||
print_install_results_and_exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Check machine architecture
|
||||
ARCH="$(uname -m)"
|
||||
case $ARCH in
|
||||
x86_64 | amd64)
|
||||
SERVER_ARCH="x64"
|
||||
;;
|
||||
armv7l | armv8l)
|
||||
SERVER_ARCH="armhf"
|
||||
;;
|
||||
arm64 | aarch64)
|
||||
SERVER_ARCH="arm64"
|
||||
;;
|
||||
*)
|
||||
echo "Error architecture not supported: $ARCH"
|
||||
print_install_results_and_exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# https://www.freedesktop.org/software/systemd/man/os-release.html
|
||||
OS_RELEASE_ID="$(grep -i '^ID=' /etc/os-release 2>/dev/null | sed 's/^ID=//gi' | sed 's/"//g')"
|
||||
if [[ -z $OS_RELEASE_ID ]]; then
|
||||
OS_RELEASE_ID="$(grep -i '^ID=' /usr/lib/os-release 2>/dev/null | sed 's/^ID=//gi' | sed 's/"//g')"
|
||||
if [[ -z $OS_RELEASE_ID ]]; then
|
||||
OS_RELEASE_ID="unknown"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Create installation folder
|
||||
if [[ ! -d $SERVER_DIR ]]; then
|
||||
mkdir -p $SERVER_DIR
|
||||
if (( $? > 0 )); then
|
||||
echo "Error creating server install directory"
|
||||
print_install_results_and_exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
SERVER_DOWNLOAD_URL="$(echo "${serverDownloadUrlTemplate.replace(/\$\{/g, '\\${')}" | sed "s/\\\${quality}/$DISTRO_QUALITY/g" | sed "s/\\\${version}/$DISTRO_VERSION/g" | sed "s/\\\${commit}/$DISTRO_COMMIT/g" | sed "s/\\\${os}/$SERVER_OS/g" | sed "s/\\\${arch}/$SERVER_ARCH/g" | sed "s/\\\${release}/$DISTRO_VSCODIUM_RELEASE/g")"
|
||||
|
||||
# Check if server script is already installed
|
||||
if [[ ! -f $SERVER_SCRIPT ]]; then
|
||||
if [[ "$SERVER_OS" = "dragonfly" ]] || [[ "$SERVER_OS" = "freebsd" ]]; then
|
||||
echo "Error "$SERVER_OS" needs manual installation of remote extension host"
|
||||
print_install_results_and_exit 1
|
||||
fi
|
||||
|
||||
pushd $SERVER_DIR > /dev/null
|
||||
|
||||
if [[ ! -z $(which wget) ]]; then
|
||||
wget --tries=3 --timeout=10 --continue --no-verbose -O vscode-server.tar.gz $SERVER_DOWNLOAD_URL
|
||||
elif [[ ! -z $(which curl) ]]; then
|
||||
curl --retry 3 --connect-timeout 10 --location --show-error --silent --output vscode-server.tar.gz $SERVER_DOWNLOAD_URL
|
||||
else
|
||||
echo "Error no tool to download server binary"
|
||||
print_install_results_and_exit 1
|
||||
fi
|
||||
|
||||
if (( $? > 0 )); then
|
||||
echo "Error downloading server from $SERVER_DOWNLOAD_URL"
|
||||
print_install_results_and_exit 1
|
||||
fi
|
||||
|
||||
tar -xf vscode-server.tar.gz --strip-components 1
|
||||
if (( $? > 0 )); then
|
||||
echo "Error while extracting server contents"
|
||||
print_install_results_and_exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f $SERVER_SCRIPT ]]; then
|
||||
echo "Error server contents are corrupted"
|
||||
print_install_results_and_exit 1
|
||||
fi
|
||||
|
||||
rm -f vscode-server.tar.gz
|
||||
|
||||
popd > /dev/null
|
||||
else
|
||||
echo "Server script already installed in $SERVER_SCRIPT"
|
||||
fi
|
||||
|
||||
# Try to find if server is already running
|
||||
if [[ -f $SERVER_PIDFILE ]]; then
|
||||
SERVER_PID="$(cat $SERVER_PIDFILE)"
|
||||
SERVER_RUNNING_PROCESS="$(ps -o pid,args -p $SERVER_PID | grep $SERVER_SCRIPT)"
|
||||
else
|
||||
SERVER_RUNNING_PROCESS="$(ps -o pid,args -A | grep $SERVER_SCRIPT | grep -v grep)"
|
||||
fi
|
||||
|
||||
if [[ -z $SERVER_RUNNING_PROCESS ]]; then
|
||||
if [[ -f $SERVER_LOGFILE ]]; then
|
||||
rm $SERVER_LOGFILE
|
||||
fi
|
||||
if [[ -f $SERVER_TOKENFILE ]]; then
|
||||
rm $SERVER_TOKENFILE
|
||||
fi
|
||||
|
||||
touch $SERVER_TOKENFILE
|
||||
chmod 600 $SERVER_TOKENFILE
|
||||
SERVER_CONNECTION_TOKEN="${crypto.randomUUID()}"
|
||||
echo $SERVER_CONNECTION_TOKEN > $SERVER_TOKENFILE
|
||||
|
||||
$SERVER_SCRIPT --start-server --host=127.0.0.1 $SERVER_LISTEN_FLAG $SERVER_INITIAL_EXTENSIONS --connection-token-file $SERVER_TOKENFILE --telemetry-level off --use-host-proxy --disable-websocket-compression --without-browser-env-var --enable-remote-auto-shutdown --accept-server-license-terms &> $SERVER_LOGFILE &
|
||||
echo $! > $SERVER_PIDFILE
|
||||
else
|
||||
echo "Server script is already running $SERVER_SCRIPT"
|
||||
fi
|
||||
|
||||
if [[ -f $SERVER_TOKENFILE ]]; then
|
||||
SERVER_CONNECTION_TOKEN="$(cat $SERVER_TOKENFILE)"
|
||||
else
|
||||
echo "Error server token file not found $SERVER_TOKENFILE"
|
||||
print_install_results_and_exit 1
|
||||
fi
|
||||
|
||||
if [[ -f $SERVER_LOGFILE ]]; then
|
||||
for i in {1..5}; do
|
||||
LISTENING_ON="$(cat $SERVER_LOGFILE | grep -E 'Extension host agent listening on .+' | sed 's/Extension host agent listening on //')"
|
||||
if [[ -n $LISTENING_ON ]]; then
|
||||
break
|
||||
fi
|
||||
sleep 0.5
|
||||
done
|
||||
|
||||
if [[ -z $LISTENING_ON ]]; then
|
||||
echo "Error server did not start sucessfully"
|
||||
print_install_results_and_exit 1
|
||||
fi
|
||||
else
|
||||
echo "Error server log file not found $SERVER_LOGFILE"
|
||||
print_install_results_and_exit 1
|
||||
fi
|
||||
|
||||
# Finish server setup and keep script running
|
||||
if [[ -z $SERVER_RUNNING_PROCESS ]]; then
|
||||
echo "${id}: start"
|
||||
echo "exitCode==0=="
|
||||
echo "listeningOn==$LISTENING_ON=="
|
||||
echo "connectionToken==$SERVER_CONNECTION_TOKEN=="
|
||||
echo "logFile==$SERVER_LOGFILE=="
|
||||
echo "osReleaseId==$OS_RELEASE_ID=="
|
||||
echo "arch==$ARCH=="
|
||||
echo "platform==$PLATFORM=="
|
||||
echo "tmpDir==$TMP_DIR=="
|
||||
${envVariables.map(envVar => `echo "${envVar}==$${envVar}=="`).join('\n')}
|
||||
echo "${id}: end"
|
||||
|
||||
echo "${id}: Server installation script done"
|
||||
|
||||
SERVER_PID="$(cat $SERVER_PIDFILE)"
|
||||
SERVER_RUNNING_PROCESS="$(ps -o pid,args -p $SERVER_PID | grep $SERVER_SCRIPT)"
|
||||
while [[ -n $SERVER_RUNNING_PROCESS ]]; do
|
||||
sleep 300;
|
||||
SERVER_RUNNING_PROCESS="$(ps -o pid,args -p $SERVER_PID | grep $SERVER_SCRIPT)"
|
||||
done
|
||||
else
|
||||
print_install_results_and_exit 0
|
||||
fi
|
||||
`;
|
||||
}
|
||||
150
extensions/open-remote-wsl/src/wsl/wslManager.ts
Normal file
150
extensions/open-remote-wsl/src/wsl/wslManager.ts
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as cp from 'child_process';
|
||||
import Log from '../common/logger';
|
||||
import { EventEmitter } from '../common/event';
|
||||
|
||||
const wslBinary = 'wsl.exe';
|
||||
|
||||
export interface WSLDistro {
|
||||
isDefault: boolean;
|
||||
name: string;
|
||||
state: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface WSLOnlineDistro {
|
||||
name: string;
|
||||
friendlyName: string;
|
||||
}
|
||||
|
||||
export class WSLManager {
|
||||
constructor(private readonly logger: Log) {
|
||||
}
|
||||
|
||||
async listDistros() {
|
||||
const resp = this._runWSLCommand(['--list', '--verbose'], 'utf16le');
|
||||
const { exitCode } = await resp.exitPromise;
|
||||
const { stdout, stderr } = resp;
|
||||
if (exitCode) {
|
||||
this.logger.trace(`Command wsl listDistros exited with code ${exitCode}`, stdout + '\n\n' + stderr);
|
||||
throw new Error(`Command wsl listDistros exited with code ${exitCode}`);
|
||||
}
|
||||
|
||||
const regex = /(?<default>\*|\s)\s+(?<name>[\w\.-]+)\s+(?<state>[\w]+)\s+(?<version>\d)/;
|
||||
const distros: WSLDistro[] = [];
|
||||
for (const line of stdout.split(/\r?\n/)) {
|
||||
const matches = line.match(regex);
|
||||
if (matches && matches.groups) {
|
||||
distros.push({
|
||||
isDefault: matches.groups.default === '*',
|
||||
name: matches.groups.name,
|
||||
state: matches.groups.state,
|
||||
version: matches.groups.version,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return distros;
|
||||
}
|
||||
|
||||
async listOnlineDistros() {
|
||||
const resp = this._runWSLCommand(['--list', '--online'], 'utf16le');
|
||||
const { exitCode } = await resp.exitPromise;
|
||||
const { stdout, stderr } = resp;
|
||||
if (exitCode) {
|
||||
this.logger.trace(`Command wsl listOnlineDistros exited with code ${exitCode}`, stdout + '\n\n' + stderr);
|
||||
throw new Error(`Command wsl listOnlineDistros exited with code ${exitCode}`);
|
||||
}
|
||||
|
||||
let lines = stdout.split(/\r?\n/);
|
||||
const idx = lines.findIndex(l => /\s*NAME\s+FRIENDLY NAME\s*/.test(l));
|
||||
lines = lines.slice(idx + 1);
|
||||
|
||||
const regex = /(?<name>[\w\.-]+)\s+(?<friendlyName>\w.+\w)/;
|
||||
const distros: WSLOnlineDistro[] = [];
|
||||
for (const line of lines) {
|
||||
const matches = line.match(regex);
|
||||
if (matches && matches.groups) {
|
||||
distros.push({
|
||||
name: matches.groups.name,
|
||||
friendlyName: matches.groups.friendlyName,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return distros;
|
||||
}
|
||||
|
||||
async setDefaultDistro(distroName: string) {
|
||||
const resp = this._runWSLCommand(['--set-default', distroName], 'utf16le');
|
||||
const { exitCode } = await resp.exitPromise;
|
||||
const { stdout, stderr } = resp;
|
||||
if (exitCode) {
|
||||
this.logger.trace(`Command wsl setDefaultDistro exited with code ${exitCode}`, stdout + '\n\n' + stderr);
|
||||
throw new Error(`Command wsl setDefaultDistro exited with code ${exitCode}`);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteDistro(distroName: string) {
|
||||
const resp = this._runWSLCommand(['--unregister', distroName], 'utf16le');
|
||||
const { exitCode } = await resp.exitPromise;
|
||||
const { stdout, stderr } = resp;
|
||||
if (exitCode) {
|
||||
this.logger.trace(`Command wsl deleteDistro exited with code ${exitCode}`, stdout + '\n\n' + stderr);
|
||||
throw new Error(`Command wsl deleteDistro exited with code ${exitCode}`);
|
||||
}
|
||||
}
|
||||
|
||||
async exec(cmd: string, args: string[], distro: string) {
|
||||
return this._runWSLCommand(['--distribution', distro, '--', cmd, ...args], 'utf8');
|
||||
}
|
||||
|
||||
private _runWSLCommand(args: string[], encoding: 'utf8' | 'utf16le') {
|
||||
this.logger.trace(`Running WSL command: ${wslBinary} ${args.join(' ')}`);
|
||||
|
||||
const cmd = cp.spawn(wslBinary, args, { windowsHide: true, windowsVerbatimArguments: true });
|
||||
|
||||
const stdoutDataEmitter = new EventEmitter<Buffer>();
|
||||
const stdoutData: Buffer[] = [];
|
||||
const stderrDataEmitter = new EventEmitter<Buffer>();
|
||||
const stderrData: Buffer[] = [];
|
||||
cmd.stdout.on('data', (data: Buffer) => {
|
||||
stdoutData.push(data);
|
||||
stdoutDataEmitter.fire(data);
|
||||
});
|
||||
cmd.stderr.on('data', (data: Buffer) => {
|
||||
stderrData.push(data);
|
||||
stderrDataEmitter.fire(data);
|
||||
});
|
||||
|
||||
const exitPromise = new Promise<{ exitCode: number }>((resolve, reject) => {
|
||||
cmd.on('error', (err) => {
|
||||
this.logger.error(`Error running WSL command: ${wslBinary} ${args.join(' ')}`, err);
|
||||
reject(err);
|
||||
});
|
||||
cmd.on('exit', (code, _signal) => {
|
||||
resolve({ exitCode: code ?? 0 });
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
get stdout() {
|
||||
return Buffer.concat(stdoutData).toString(encoding);
|
||||
},
|
||||
get stderr() {
|
||||
return Buffer.concat(stderrData).toString(encoding);
|
||||
},
|
||||
get onStdoutData() {
|
||||
return stdoutDataEmitter.event;
|
||||
},
|
||||
get onStderrData() {
|
||||
return stderrDataEmitter.event;
|
||||
},
|
||||
exitPromise
|
||||
};
|
||||
}
|
||||
}
|
||||
26
extensions/open-remote-wsl/src/wsl/wslTerminal.ts
Normal file
26
extensions/open-remote-wsl/src/wsl/wslTerminal.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
class WSLTerminal {
|
||||
static NAME = 'WSL';
|
||||
|
||||
private getTerminal() {
|
||||
const wslTerminal = vscode.window.terminals.find(t => t.name === WSLTerminal.NAME);
|
||||
if (wslTerminal) {
|
||||
return wslTerminal;
|
||||
}
|
||||
return vscode.window.createTerminal(WSLTerminal.NAME);
|
||||
}
|
||||
|
||||
runCommand(command: string) {
|
||||
const wslTerminal = this.getTerminal();
|
||||
wslTerminal.show(false);
|
||||
wslTerminal.sendText(command, true);
|
||||
}
|
||||
}
|
||||
|
||||
export default new WSLTerminal();
|
||||
12
extensions/open-remote-wsl/tsconfig.json
Normal file
12
extensions/open-remote-wsl/tsconfig.json
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"extends": "../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out",
|
||||
},
|
||||
"include": [
|
||||
"src/**/*",
|
||||
"../../src/vscode-dts/vscode.d.ts",
|
||||
"../../src/vscode-dts/vscode.proposed.resolvers.d.ts",
|
||||
"../../src/vscode-dts/vscode.proposed.contribViewsRemote.d.ts",
|
||||
]
|
||||
}
|
||||
9428
package-lock.json
generated
9428
package-lock.json
generated
File diff suppressed because it is too large
Load diff
59
src/vs/workbench/contrib/void/browser/_dummyContrib.ts
Normal file
59
src/vs/workbench/contrib/void/browser/_dummyContrib.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
/*--------------------------------------------------------------------------------------
|
||||
* 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 { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';
|
||||
import { localize2 } from '../../../../nls.js';
|
||||
import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';
|
||||
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
|
||||
import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js';
|
||||
|
||||
|
||||
export interface IDummyService {
|
||||
readonly _serviceBrand: undefined;
|
||||
}
|
||||
|
||||
export const IDummyService = createDecorator<IDummyService>('DummyService');
|
||||
|
||||
|
||||
|
||||
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
f1: true,
|
||||
id: 'void.dummy',
|
||||
title: localize2('dummy', 'dummy: Init'),
|
||||
keybinding: {
|
||||
primary: KeyMod.CtrlCmd | KeyCode.Digit0,
|
||||
weight: KeybindingWeight.VoidExtension,
|
||||
}
|
||||
});
|
||||
}
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
console.log('hi')
|
||||
const n = accessor.get(IDummyService)
|
||||
console.log('Hi', n._serviceBrand)
|
||||
}
|
||||
})
|
||||
|
||||
// on mount
|
||||
class DummyService extends Disposable implements IWorkbenchContribution, IDummyService {
|
||||
static readonly ID = 'workbench.contrib.void.dummy'
|
||||
_serviceBrand: undefined;
|
||||
|
||||
constructor(
|
||||
) {
|
||||
super()
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(IDummyService, DummyService, InstantiationType.Eager);
|
||||
|
||||
registerWorkbenchContribution2(DummyService.ID, DummyService, WorkbenchPhase.BlockRestore);
|
||||
|
|
@ -34,7 +34,7 @@
|
|||
// // const result = await new Promise((res, rej) => {
|
||||
// // sendLLMMessage({
|
||||
// // messages,
|
||||
// // tools: ['grep_search'],
|
||||
// // tools: ['search_files'],
|
||||
// // onFinalMessage: ({ result: r, }) => {
|
||||
// // res(r)
|
||||
// // },
|
||||
|
|
@ -73,7 +73,7 @@
|
|||
// // const result = new Promise((res, rej) => {
|
||||
// // sendLLMMessage({
|
||||
// // messages,
|
||||
// // tools: ['grep_search'],
|
||||
// // tools: ['search_files'],
|
||||
// // onResult: (r) => {
|
||||
// // res(r)
|
||||
// // }
|
||||
|
|
|
|||
|
|
@ -793,7 +793,7 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
|
|||
|
||||
const featureName: FeatureName = 'Autocomplete'
|
||||
const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName]
|
||||
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[modelSelection.providerName]?.[modelSelection.modelName] : undefined
|
||||
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] : undefined
|
||||
|
||||
|
||||
// set parameters of `newAutocompletion` appropriately
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
317
src/vs/workbench/contrib/void/browser/directoryStrService.ts
Normal file
317
src/vs/workbench/contrib/void/browser/directoryStrService.ts
Normal file
|
|
@ -0,0 +1,317 @@
|
|||
/*--------------------------------------------------------------------------------------
|
||||
* 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 { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IFileService } from '../../../../platform/files/common/files.js';
|
||||
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
|
||||
import { ShallowDirectoryItem, ToolCallParams, ToolResultType } from '../common/toolsServiceTypes.js';
|
||||
import { MAX_CHILDREN_URIs_PAGE } from './toolsService.js';
|
||||
import { IExplorerService } from '../../files/browser/files.js';
|
||||
import { SortOrder } from '../../files/common/files.js';
|
||||
import { ExplorerItem } from '../../files/common/explorerModel.js';
|
||||
import { VoidDirectoryItem } from '../common/directoryStrTypes.js';
|
||||
|
||||
|
||||
const MAX_CHARS_TOTAL_BEGINNING = 20_000
|
||||
const MAX_CHARS_TOTAL_TOOL = 20_000
|
||||
// const MAX_FILES_TOTAL = 200
|
||||
|
||||
|
||||
export interface IDirectoryStrService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
getDirectoryStrTool(uri: URI): Promise<{ wasCutOff: boolean, str: string }>
|
||||
getAllDirectoriesStr(): Promise<{ wasCutOff: boolean, str: string }>
|
||||
|
||||
}
|
||||
export const IDirectoryStrService = createDecorator<IDirectoryStrService>('voidDirectoryStrService');
|
||||
|
||||
|
||||
|
||||
|
||||
// Check if it's a known filtered type like .git
|
||||
const shouldExcludeDirectory = (item: ExplorerItem) => {
|
||||
if (item.name === '.git' ||
|
||||
item.name === 'node_modules' ||
|
||||
item.name.startsWith('.') ||
|
||||
item.name === 'dist' ||
|
||||
item.name === 'build' ||
|
||||
item.name === 'out' ||
|
||||
item.name === 'bin' ||
|
||||
item.name === 'coverage' ||
|
||||
item.name === '__pycache__' ||
|
||||
item.name === 'env' ||
|
||||
item.name === 'venv' ||
|
||||
item.name === 'tmp' ||
|
||||
item.name === 'temp' ||
|
||||
item.name === 'artifacts' ||
|
||||
item.name === 'target' ||
|
||||
item.name === 'obj' ||
|
||||
item.name === 'vendor' ||
|
||||
item.name === 'logs' ||
|
||||
item.name === 'cache'
|
||||
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---------- ONE LAYER DEEP ----------
|
||||
|
||||
export const computeDirectoryTree1Deep = async (
|
||||
fileService: IFileService,
|
||||
rootURI: URI,
|
||||
pageNumber: number = 1,
|
||||
): Promise<ToolResultType['ls_dir']> => {
|
||||
const stat = await fileService.resolve(rootURI, { resolveMetadata: false });
|
||||
if (!stat.isDirectory) {
|
||||
return { children: null, hasNextPage: false, hasPrevPage: false, itemsRemaining: 0 };
|
||||
}
|
||||
|
||||
const nChildren = stat.children?.length ?? 0;
|
||||
|
||||
const fromChildIdx = MAX_CHILDREN_URIs_PAGE * (pageNumber - 1);
|
||||
const toChildIdx = MAX_CHILDREN_URIs_PAGE * pageNumber - 1; // INCLUSIVE
|
||||
const listChildren = stat.children?.slice(fromChildIdx, toChildIdx + 1);
|
||||
|
||||
const children: ShallowDirectoryItem[] = listChildren?.map(child => ({
|
||||
name: child.name,
|
||||
uri: child.resource,
|
||||
isDirectory: child.isDirectory,
|
||||
isSymbolicLink: child.isSymbolicLink
|
||||
})) ?? [];
|
||||
|
||||
const hasNextPage = (nChildren - 1) > toChildIdx;
|
||||
const hasPrevPage = pageNumber > 1;
|
||||
const itemsRemaining = Math.max(0, nChildren - (toChildIdx + 1));
|
||||
|
||||
return {
|
||||
children,
|
||||
hasNextPage,
|
||||
hasPrevPage,
|
||||
itemsRemaining
|
||||
};
|
||||
};
|
||||
|
||||
export const stringifyDirectoryTree1Deep = (params: ToolCallParams['ls_dir'], result: ToolResultType['ls_dir']): string => {
|
||||
if (!result.children) {
|
||||
return `Error: ${params.rootURI} is not a directory`;
|
||||
}
|
||||
|
||||
let output = '';
|
||||
const entries = result.children;
|
||||
|
||||
if (!result.hasPrevPage) { // is first page
|
||||
output += `${params.rootURI.fsPath}\n`;
|
||||
}
|
||||
|
||||
for (let i = 0; i < entries.length; i++) {
|
||||
const entry = entries[i];
|
||||
const isLast = i === entries.length - 1 && !result.hasNextPage;
|
||||
const prefix = isLast ? '└── ' : '├── ';
|
||||
|
||||
output += `${prefix}${entry.name}${entry.isDirectory ? '/' : ''}${entry.isSymbolicLink ? ' (symbolic link)' : ''}\n`;
|
||||
}
|
||||
|
||||
if (result.hasNextPage) {
|
||||
output += `└── (${result.itemsRemaining} results remaining...)\n`;
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
|
||||
// ---------- IN GENERAL ----------
|
||||
|
||||
|
||||
// if the filter exists use it to filter out files and folders when creating the tree
|
||||
const computeDirectoryTree = async (
|
||||
eItem: ExplorerItem,
|
||||
explorerService: IExplorerService
|
||||
): Promise<VoidDirectoryItem> => {
|
||||
// Fetch children with default sort order
|
||||
const eChildren = await eItem.fetchChildren(SortOrder.FilesFirst);
|
||||
|
||||
const isGitIgnoredDirectory = eItem.isDirectory && shouldExcludeDirectory(eItem)
|
||||
|
||||
// Process children recursively
|
||||
const children = !isGitIgnoredDirectory ? await Promise.all(
|
||||
eChildren.map(async c => await computeDirectoryTree(c, explorerService))
|
||||
) : null
|
||||
|
||||
// Create our directory item
|
||||
const item: VoidDirectoryItem = {
|
||||
uri: eItem.resource,
|
||||
name: eItem.name,
|
||||
isDirectory: eItem.isDirectory,
|
||||
isSymbolicLink: eItem.isSymbolicLink,
|
||||
children,
|
||||
isGitIgnoredDirectory: isGitIgnoredDirectory && { numChildren: eItem.children.size },
|
||||
};
|
||||
|
||||
return item;
|
||||
};
|
||||
|
||||
|
||||
const stringifyDirectoryTree = (
|
||||
node: VoidDirectoryItem,
|
||||
MAX_CHARS: number,
|
||||
): { content: string, wasCutOff: boolean } => {
|
||||
let content = '';
|
||||
let wasCutOff = false;
|
||||
|
||||
// If we're already exceeding the max characters, return immediately
|
||||
if (MAX_CHARS <= 0) {
|
||||
return { content, wasCutOff: true };
|
||||
}
|
||||
|
||||
// Add the root node first (without tree characters)
|
||||
const nodeLine = `${node.name}${node.isDirectory ? '/' : ''}${node.isSymbolicLink ? ' (symbolic link)' : ''}\n`;
|
||||
|
||||
if (nodeLine.length > MAX_CHARS) {
|
||||
return { content: '', wasCutOff: true };
|
||||
}
|
||||
|
||||
content += nodeLine;
|
||||
let remainingChars = MAX_CHARS - nodeLine.length;
|
||||
|
||||
// Then recursively add all children with proper tree formatting
|
||||
if (node.children && node.children.length > 0) {
|
||||
const { childrenContent, childrenCutOff } = renderChildren(
|
||||
node.children,
|
||||
remainingChars,
|
||||
''
|
||||
);
|
||||
content += childrenContent;
|
||||
wasCutOff = childrenCutOff;
|
||||
}
|
||||
return { content, wasCutOff };
|
||||
};
|
||||
|
||||
// Helper function to render children with proper tree formatting
|
||||
const renderChildren = (
|
||||
children: VoidDirectoryItem[],
|
||||
maxChars: number,
|
||||
parentPrefix: string
|
||||
): { childrenContent: string, childrenCutOff: boolean } => {
|
||||
let childrenContent = '';
|
||||
let childrenCutOff = false;
|
||||
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const child = children[i];
|
||||
const isLast = i === children.length - 1;
|
||||
|
||||
// Create the tree branch symbols
|
||||
const branchSymbol = isLast ? '└── ' : '├── ';
|
||||
const childLine = `${parentPrefix}${branchSymbol}${child.name}${child.isDirectory ? '/' : ''}${child.isSymbolicLink ? ' (symbolic link)' : ''}\n`;
|
||||
|
||||
// Check if adding this line would exceed the limit
|
||||
if (childrenContent.length + childLine.length > maxChars) {
|
||||
childrenCutOff = true;
|
||||
break;
|
||||
}
|
||||
childrenContent += childLine;
|
||||
|
||||
const nextLevelPrefix = parentPrefix + (isLast ? ' ' : '│ ');
|
||||
|
||||
|
||||
// if gitignored, just say the number of children
|
||||
if (child.isDirectory && child.isGitIgnoredDirectory && child.isGitIgnoredDirectory.numChildren > 0) {
|
||||
childrenContent += `${nextLevelPrefix}└── ... (${child.isGitIgnoredDirectory.numChildren} children) ...\n`
|
||||
}
|
||||
|
||||
// Create the prefix for the next level (continuation line or space)
|
||||
else if (child.children && child.children.length > 0) {
|
||||
|
||||
const {
|
||||
childrenContent: grandChildrenContent,
|
||||
childrenCutOff: grandChildrenCutOff
|
||||
} = renderChildren(
|
||||
child.children,
|
||||
maxChars,
|
||||
nextLevelPrefix
|
||||
);
|
||||
|
||||
// If adding grandchildren content would exceed the limit
|
||||
if (childrenContent.length + grandChildrenContent.length > maxChars) {
|
||||
childrenCutOff = true;
|
||||
break;
|
||||
}
|
||||
|
||||
childrenContent += grandChildrenContent;
|
||||
|
||||
if (grandChildrenCutOff) {
|
||||
childrenCutOff = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { childrenContent, childrenCutOff };
|
||||
};
|
||||
|
||||
|
||||
// ---------------------------------------------------
|
||||
|
||||
|
||||
class DirectoryStrService extends Disposable implements IDirectoryStrService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
constructor(
|
||||
@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,
|
||||
@IExplorerService private readonly explorerService: IExplorerService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async getDirectoryStrTool(uri: URI) {
|
||||
const eRoot = this.explorerService.findClosest(uri)
|
||||
if (!eRoot) throw new Error(`There was a problem reading the URI: ${uri.fsPath}.`)
|
||||
|
||||
const dirTree = await computeDirectoryTree(eRoot, this.explorerService);
|
||||
const { content, wasCutOff } = stringifyDirectoryTree(dirTree, MAX_CHARS_TOTAL_TOOL);
|
||||
|
||||
return {
|
||||
str: `Directory of ${uri.fsPath}:\n${content}`,
|
||||
wasCutOff,
|
||||
}
|
||||
}
|
||||
|
||||
async getAllDirectoriesStr() {
|
||||
let str: string = '';
|
||||
let cutOff = false;
|
||||
const folders = this.workspaceContextService.getWorkspace().folders;
|
||||
|
||||
for (let i = 0; i < folders.length; i += 1) {
|
||||
if (i > 0) str += '\n';
|
||||
|
||||
// this prioritizes filling 1st workspace before any other, etc
|
||||
const f = folders[i];
|
||||
str += `Directory of ${f.uri.fsPath}:\n`;
|
||||
const rootURI = f.uri;
|
||||
|
||||
const eRoot = this.explorerService.findClosestRoot(rootURI);
|
||||
if (!eRoot) continue;
|
||||
|
||||
// Use our new approach with direct explorer service
|
||||
const dirTree = await computeDirectoryTree(eRoot, this.explorerService);
|
||||
console.log('dirtree', dirTree)
|
||||
const { content, wasCutOff } = stringifyDirectoryTree(dirTree, MAX_CHARS_TOTAL_BEGINNING - str.length);
|
||||
str += content;
|
||||
if (wasCutOff) {
|
||||
cutOff = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return { wasCutOff: cutOff, str };
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(IDirectoryStrService, DirectoryStrService, InstantiationType.Delayed);
|
||||
|
|
@ -11,7 +11,7 @@ import { ICodeEditor, IOverlayWidget, IViewZone } from '../../../../editor/brows
|
|||
// import { IUndoRedoService } from '../../../../platform/undoRedo/common/undoRedo.js';
|
||||
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
|
||||
// import { throttle } from '../../../../base/common/decorators.js';
|
||||
import { ComputedDiff, findDiffs } from './helpers/findDiffs.js';
|
||||
import { findDiffs } from './helpers/findDiffs.js';
|
||||
import { EndOfLinePreference, IModelDecorationOptions, ITextModel } from '../../../../editor/common/model.js';
|
||||
import { IRange } from '../../../../editor/common/core/range.js';
|
||||
import { registerColor } from '../../../../platform/theme/common/colorUtils.js';
|
||||
|
|
@ -40,13 +40,14 @@ import { ICommandService } from '../../../../platform/commands/common/commands.j
|
|||
import { ILLMMessageService } from '../common/sendLLMMessageService.js';
|
||||
import { LLMChatMessage, OnError, errorDetails } from '../common/sendLLMMessageTypes.js';
|
||||
import { IMetricsService } from '../common/metricsService.js';
|
||||
import { IEditCodeService, AddCtrlKOpts, StartApplyingOpts } from './editCodeServiceInterface.js';
|
||||
import { IEditCodeService, AddCtrlKOpts, StartApplyingOpts, CallBeforeStartApplyingOpts, } from './editCodeServiceInterface.js';
|
||||
import { IVoidSettingsService } from '../common/voidSettingsService.js';
|
||||
import { FeatureName } from '../common/voidSettingsTypes.js';
|
||||
import { IVoidModelService } from '../common/voidModelService.js';
|
||||
import { ITextFileService } from '../../../services/textfile/common/textfiles.js';
|
||||
import { deepClone } from '../../../../base/common/objects.js';
|
||||
import { acceptBg, acceptBorder, buttonFontSize, buttonTextColor, rejectBg, rejectBorder } from '../common/helpers/colors.js';
|
||||
import { DiffArea, Diff, CtrlKZone, VoidFileSnapshot, DiffAreaSnapshotEntry, diffAreaSnapshotKeys, DiffZone, TrackingZone, ComputedDiff } from '../common/editCodeServiceTypes.js';
|
||||
|
||||
const configOfBG = (color: Color) => {
|
||||
return { dark: color, light: color, hcDark: color, hcLight: color, }
|
||||
|
|
@ -107,15 +108,31 @@ const getLeadingWhitespacePx = (editor: ICodeEditor, startLine: number): number
|
|||
};
|
||||
|
||||
|
||||
// Helper function to remove whitespace except newlines
|
||||
const removeWhitespaceExceptNewlines = (str: string): string => {
|
||||
return str.replace(/[^\S\n]+/g, '');
|
||||
}
|
||||
|
||||
|
||||
|
||||
// finds block.orig in fileContents and return its range in file
|
||||
// startingAtLine is 1-indexed and inclusive
|
||||
const findTextInCode = (text: string, fileContents: string, startingAtLine?: number) => {
|
||||
const idx = fileContents.indexOf(text,
|
||||
startingAtLine !== undefined ?
|
||||
fileContents.split('\n').slice(0, startingAtLine).join('\n').length // num characters in all lines before startingAtLine
|
||||
: 0
|
||||
)
|
||||
const findTextInCode = (text: string, fileContents: string, canFallbackToRemoveWhitespace: boolean, startingAtLine?: number) => {
|
||||
|
||||
const startLineIdx = (fileContents: string) => startingAtLine !== undefined ?
|
||||
fileContents.split('\n').slice(0, startingAtLine).join('\n').length // num characters in all lines before startingAtLine
|
||||
: 0
|
||||
|
||||
// idx = starting index in fileContents
|
||||
let idx = fileContents.indexOf(text, startLineIdx(fileContents))
|
||||
|
||||
// try to find it ignoring all whitespace this time
|
||||
if (idx === -1 && canFallbackToRemoveWhitespace) {
|
||||
text = removeWhitespaceExceptNewlines(text)
|
||||
fileContents = removeWhitespaceExceptNewlines(fileContents)
|
||||
idx = fileContents.indexOf(text, startLineIdx(fileContents));
|
||||
}
|
||||
|
||||
if (idx === -1) return 'Not found' as const
|
||||
const lastIdx = fileContents.lastIndexOf(text)
|
||||
if (lastIdx !== idx) return 'Not unique' as const
|
||||
|
|
@ -127,108 +144,6 @@ const findTextInCode = (text: string, fileContents: string, startingAtLine?: num
|
|||
|
||||
|
||||
|
||||
|
||||
// // TODO diffArea should be removed if we just discovered it has no more diffs in it
|
||||
// for (const diffareaid of this.diffAreasOfURI[uri.fsPath] || []) {
|
||||
// const diffArea = this.diffAreaOfId[diffareaid]
|
||||
// if (Object.keys(diffArea._diffOfId).length === 0 && !diffArea._sweepState.isStreaming) {
|
||||
// const { onFinishEdit } = this._addToHistory(uri)
|
||||
// this._deleteDiffArea(diffArea)
|
||||
// onFinishEdit()
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
export type Diff = {
|
||||
diffid: number;
|
||||
diffareaid: number; // the diff area this diff belongs to, "computed"
|
||||
} & ComputedDiff
|
||||
|
||||
|
||||
|
||||
// _ means anything we don't include if we clone it
|
||||
// DiffArea.originalStartLine is the line in originalCode (not the file)
|
||||
|
||||
type CommonZoneProps = {
|
||||
diffareaid: number;
|
||||
startLine: number;
|
||||
endLine: number;
|
||||
|
||||
_URI: URI; // typically we get the URI from model
|
||||
|
||||
}
|
||||
|
||||
type CtrlKZone = {
|
||||
type: 'CtrlKZone';
|
||||
originalCode?: undefined;
|
||||
|
||||
editorId: string; // the editor the input lives on
|
||||
|
||||
_mountInfo: null | {
|
||||
textAreaRef: { current: HTMLTextAreaElement | null }
|
||||
dispose: () => void;
|
||||
refresh: () => void;
|
||||
}
|
||||
|
||||
_linkedStreamingDiffZone: number | null; // diffareaid of the diffZone currently streaming here
|
||||
_removeStylesFns: Set<Function> // these don't remove diffs or this diffArea, only their styles
|
||||
|
||||
} & CommonZoneProps
|
||||
|
||||
|
||||
export type DiffZone = {
|
||||
type: 'DiffZone',
|
||||
originalCode: string;
|
||||
_diffOfId: Record<string, Diff>; // diffid -> diff in this DiffArea
|
||||
_streamState: {
|
||||
isStreaming: true;
|
||||
streamRequestIdRef: { current: string | null };
|
||||
line: number;
|
||||
} | {
|
||||
isStreaming: false;
|
||||
streamRequestIdRef?: undefined;
|
||||
line?: undefined;
|
||||
};
|
||||
editorId?: undefined;
|
||||
linkedStreamingDiffZone?: undefined;
|
||||
_removeStylesFns: Set<Function> // these don't remove diffs or this diffArea, only their styles
|
||||
} & CommonZoneProps
|
||||
|
||||
|
||||
|
||||
type TrackingZone<T> = {
|
||||
type: 'TrackingZone';
|
||||
metadata: T;
|
||||
originalCode?: undefined;
|
||||
editorId?: undefined;
|
||||
_removeStylesFns?: undefined;
|
||||
} & CommonZoneProps
|
||||
|
||||
|
||||
// called DiffArea for historical purposes, we can rename to something like TextRegion if we want
|
||||
export type DiffArea = CtrlKZone | DiffZone | TrackingZone<any>
|
||||
|
||||
const diffAreaSnapshotKeys = [
|
||||
'type',
|
||||
'diffareaid',
|
||||
'originalCode',
|
||||
'startLine',
|
||||
'endLine',
|
||||
'editorId',
|
||||
|
||||
] as const satisfies (keyof DiffArea)[]
|
||||
|
||||
type DiffAreaSnapshot<DiffAreaType extends DiffArea = DiffArea> = Pick<DiffAreaType, typeof diffAreaSnapshotKeys[number]>
|
||||
|
||||
|
||||
|
||||
type HistorySnapshot = {
|
||||
snapshottedDiffAreaOfId: Record<string, DiffAreaSnapshot>;
|
||||
entireFileCode: string;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// line/col is the location, originalCodeStartLine is the start line of the original code being displayed
|
||||
type StreamLocationMutable = { line: number, col: number, addedSplitYet: boolean, originalCodeStartLine: number }
|
||||
|
||||
|
|
@ -243,23 +158,21 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
diffAreaOfId: Record<string, DiffArea> = {}; // diffareaId -> diffArea
|
||||
diffOfId: Record<string, Diff> = {}; // diffid -> diff (redundant with diffArea._diffOfId)
|
||||
|
||||
|
||||
// events
|
||||
|
||||
|
||||
// uri: diffZones // listen on change diffZones
|
||||
private readonly _onDidAddOrDeleteDiffZones = new Emitter<{ uri: URI }>();
|
||||
onDidAddOrDeleteDiffZones = this._onDidAddOrDeleteDiffZones.event;
|
||||
|
||||
// diffZone: [uri], diffs, isStreaming // listen on change diffs, change streaming (uri is const)
|
||||
private readonly _onDidChangeDiffsInDiffZone = new Emitter<{ uri: URI, diffareaid: number }>();
|
||||
private readonly _onDidChangeDiffsInDiffZoneNotStreaming = new Emitter<{ uri: URI, diffareaid: number }>();
|
||||
private readonly _onDidChangeStreamingInDiffZone = new Emitter<{ uri: URI, diffareaid: number }>();
|
||||
onDidChangeDiffsInDiffZone = this._onDidChangeDiffsInDiffZone.event;
|
||||
onDidChangeDiffsInDiffZoneNotStreaming = this._onDidChangeDiffsInDiffZoneNotStreaming.event;
|
||||
onDidChangeStreamingInDiffZone = this._onDidChangeStreamingInDiffZone.event;
|
||||
|
||||
// ctrlKZone: [uri], isStreaming // listen on change streaming
|
||||
private readonly _onDidChangeStreamingInCtrlKZone = new Emitter<{ uri: URI; diffareaid: number }>();
|
||||
onDidChangeStreamingInCtrlKZone = this._onDidChangeStreamingInCtrlKZone.event
|
||||
onDidChangeStreamingInCtrlKZone = this._onDidChangeStreamingInCtrlKZone.event;
|
||||
|
||||
|
||||
constructor(
|
||||
|
|
@ -722,98 +635,98 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
private _getCurrentVoidFileSnapshot = (uri: URI): VoidFileSnapshot => {
|
||||
const { model } = this._voidModelService.getModel(uri)
|
||||
const snapshottedDiffAreaOfId: Record<string, DiffAreaSnapshotEntry> = {}
|
||||
|
||||
for (const diffareaid in this.diffAreaOfId) {
|
||||
const diffArea = this.diffAreaOfId[diffareaid]
|
||||
|
||||
if (diffArea._URI.fsPath !== uri.fsPath) continue
|
||||
|
||||
snapshottedDiffAreaOfId[diffareaid] = deepClone(
|
||||
Object.fromEntries(diffAreaSnapshotKeys.map(key => [key, diffArea[key]]))
|
||||
) as DiffAreaSnapshotEntry
|
||||
}
|
||||
|
||||
const entireFileCode = model ? model.getValue(EndOfLinePreference.LF) : ''
|
||||
|
||||
// this._noLongerNeedModelReference(uri)
|
||||
return {
|
||||
snapshottedDiffAreaOfId,
|
||||
entireFileCode, // the whole file's code
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private _restoreVoidFileSnapshot = async (uri: URI, snapshot: VoidFileSnapshot) => {
|
||||
// for each diffarea in this uri, stop streaming if currently streaming
|
||||
for (const diffareaid in this.diffAreaOfId) {
|
||||
const diffArea = this.diffAreaOfId[diffareaid]
|
||||
if (diffArea.type === 'DiffZone')
|
||||
this._stopIfStreaming(diffArea)
|
||||
}
|
||||
|
||||
// delete all diffareas on this uri (clearing their styles)
|
||||
this._deleteAllDiffAreas(uri)
|
||||
|
||||
const { snapshottedDiffAreaOfId, entireFileCode: entireModelCode } = deepClone(snapshot) // don't want to destroy the snapshot
|
||||
|
||||
// restore diffAreaOfId and diffAreasOfModelId
|
||||
for (const diffareaid in snapshottedDiffAreaOfId) {
|
||||
|
||||
const snapshottedDiffArea = snapshottedDiffAreaOfId[diffareaid]
|
||||
|
||||
if (snapshottedDiffArea.type === 'DiffZone') {
|
||||
this.diffAreaOfId[diffareaid] = {
|
||||
...snapshottedDiffArea as DiffAreaSnapshotEntry<DiffZone>,
|
||||
type: 'DiffZone',
|
||||
_diffOfId: {},
|
||||
_URI: uri,
|
||||
_streamState: { isStreaming: false }, // when restoring, we will never be streaming
|
||||
_removeStylesFns: new Set(),
|
||||
}
|
||||
}
|
||||
else if (snapshottedDiffArea.type === 'CtrlKZone') {
|
||||
this.diffAreaOfId[diffareaid] = {
|
||||
...snapshottedDiffArea as DiffAreaSnapshotEntry<CtrlKZone>,
|
||||
_URI: uri,
|
||||
_removeStylesFns: new Set<Function>(),
|
||||
_mountInfo: null,
|
||||
_linkedStreamingDiffZone: null, // when restoring, we will never be streaming
|
||||
}
|
||||
}
|
||||
this._addOrInitializeDiffAreaAtURI(uri, diffareaid)
|
||||
}
|
||||
this._onDidAddOrDeleteDiffZones.fire({ uri })
|
||||
|
||||
// restore file content
|
||||
this._writeURIText(uri, entireModelCode,
|
||||
'wholeFileRange',
|
||||
{ shouldRealignDiffAreas: false }
|
||||
)
|
||||
// this._noLongerNeedModelReference(uri)
|
||||
}
|
||||
|
||||
private _addToHistory(uri: URI, opts?: { onWillUndo?: () => void }) {
|
||||
|
||||
const getCurrentSnapshot = (): HistorySnapshot => {
|
||||
|
||||
const { model } = this._voidModelService.getModel(uri)
|
||||
const snapshottedDiffAreaOfId: Record<string, DiffAreaSnapshot> = {}
|
||||
|
||||
for (const diffareaid in this.diffAreaOfId) {
|
||||
const diffArea = this.diffAreaOfId[diffareaid]
|
||||
|
||||
if (diffArea._URI.fsPath !== uri.fsPath) continue
|
||||
|
||||
snapshottedDiffAreaOfId[diffareaid] = deepClone(
|
||||
Object.fromEntries(diffAreaSnapshotKeys.map(key => [key, diffArea[key]]))
|
||||
) as DiffAreaSnapshot
|
||||
}
|
||||
|
||||
const entireFileCode = model ? model.getValue(EndOfLinePreference.LF) : ''
|
||||
|
||||
// this._noLongerNeedModelReference(uri)
|
||||
return {
|
||||
snapshottedDiffAreaOfId,
|
||||
entireFileCode, // the whole file's code
|
||||
}
|
||||
}
|
||||
|
||||
const restoreDiffAreas = async (snapshot: HistorySnapshot) => {
|
||||
|
||||
// for each diffarea in this uri, stop streaming if currently streaming
|
||||
for (const diffareaid in this.diffAreaOfId) {
|
||||
const diffArea = this.diffAreaOfId[diffareaid]
|
||||
if (diffArea.type === 'DiffZone')
|
||||
this._stopIfStreaming(diffArea)
|
||||
}
|
||||
|
||||
// delete all diffareas on this uri (clearing their styles)
|
||||
this._deleteAllDiffAreas(uri)
|
||||
this.diffAreasOfURI[uri.fsPath]?.clear()
|
||||
|
||||
const { snapshottedDiffAreaOfId, entireFileCode: entireModelCode } = deepClone(snapshot) // don't want to destroy the snapshot
|
||||
|
||||
// restore diffAreaOfId and diffAreasOfModelId
|
||||
for (const diffareaid in snapshottedDiffAreaOfId) {
|
||||
|
||||
const snapshottedDiffArea = snapshottedDiffAreaOfId[diffareaid]
|
||||
|
||||
if (snapshottedDiffArea.type === 'DiffZone') {
|
||||
this.diffAreaOfId[diffareaid] = {
|
||||
...snapshottedDiffArea as DiffAreaSnapshot<DiffZone>,
|
||||
type: 'DiffZone',
|
||||
_diffOfId: {},
|
||||
_URI: uri,
|
||||
_streamState: { isStreaming: false }, // when restoring, we will never be streaming
|
||||
_removeStylesFns: new Set(),
|
||||
}
|
||||
}
|
||||
else if (snapshottedDiffArea.type === 'CtrlKZone') {
|
||||
this.diffAreaOfId[diffareaid] = {
|
||||
...snapshottedDiffArea as DiffAreaSnapshot<CtrlKZone>,
|
||||
_URI: uri,
|
||||
_removeStylesFns: new Set<Function>(),
|
||||
_mountInfo: null,
|
||||
_linkedStreamingDiffZone: null, // when restoring, we will never be streaming
|
||||
}
|
||||
}
|
||||
this._addOrInitializeDiffAreaAtURI(uri, diffareaid)
|
||||
}
|
||||
this._onDidAddOrDeleteDiffZones.fire({ uri })
|
||||
|
||||
// restore file content
|
||||
this._writeURIText(uri, entireModelCode,
|
||||
'wholeFileRange',
|
||||
{ shouldRealignDiffAreas: false }
|
||||
)
|
||||
// this._noLongerNeedModelReference(uri)
|
||||
}
|
||||
|
||||
const beforeSnapshot: HistorySnapshot = getCurrentSnapshot()
|
||||
let afterSnapshot: HistorySnapshot | null = null
|
||||
const beforeSnapshot: VoidFileSnapshot = this._getCurrentVoidFileSnapshot(uri)
|
||||
let afterSnapshot: VoidFileSnapshot | null = null
|
||||
|
||||
const elt: IUndoRedoElement = {
|
||||
type: UndoRedoElementType.Resource,
|
||||
resource: uri,
|
||||
label: 'Void Agent',
|
||||
code: 'undoredo.editCode',
|
||||
undo: () => { opts?.onWillUndo?.(); restoreDiffAreas(beforeSnapshot); },
|
||||
redo: () => { if (afterSnapshot) restoreDiffAreas(afterSnapshot) }
|
||||
undo: () => { opts?.onWillUndo?.(); this._restoreVoidFileSnapshot(uri, beforeSnapshot); },
|
||||
redo: () => { if (afterSnapshot) this._restoreVoidFileSnapshot(uri, afterSnapshot) }
|
||||
}
|
||||
this._undoRedoService.pushElement(elt)
|
||||
|
||||
const onFinishEdit = async () => {
|
||||
afterSnapshot = getCurrentSnapshot()
|
||||
afterSnapshot = this._getCurrentVoidFileSnapshot(uri)
|
||||
await this._textFileService.save(uri, { // we want [our change] -> [save] so it's all treated as one change.
|
||||
skipSaveParticipants: true // avoid triggering extensions etc (if they reformat the page, it will add another item to the undo stack)
|
||||
})
|
||||
|
|
@ -822,6 +735,16 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
}
|
||||
|
||||
|
||||
public getVoidFileSnapshot(uri: URI) {
|
||||
return this._getCurrentVoidFileSnapshot(uri)
|
||||
}
|
||||
|
||||
|
||||
public restoreVoidFileSnapshot(uri: URI, snapshot: VoidFileSnapshot): void {
|
||||
this._restoreVoidFileSnapshot(uri, snapshot)
|
||||
}
|
||||
|
||||
|
||||
// delete diffOfId and diffArea._diffOfId
|
||||
private _deleteDiff(diff: Diff) {
|
||||
const diffArea = this.diffAreaOfId[diff.diffareaid]
|
||||
|
|
@ -886,6 +809,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
else if (diffArea.type === 'CtrlKZone')
|
||||
this._deleteCtrlKZone(diffArea)
|
||||
})
|
||||
this.diffAreasOfURI[uri.fsPath]?.clear()
|
||||
}
|
||||
|
||||
private _addOrInitializeDiffAreaAtURI = (uri: URI, diffareaid: string | number) => {
|
||||
|
|
@ -994,7 +918,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
if (diffArea?.type !== 'DiffZone') continue
|
||||
// fire changed diffs (this is the only place Diffs are added)
|
||||
if (!diffArea._streamState.isStreaming) {
|
||||
this._onDidChangeDiffsInDiffZone.fire({ uri, diffareaid: diffArea.diffareaid })
|
||||
this._onDidChangeDiffsInDiffZoneNotStreaming.fire({ uri, diffareaid: diffArea.diffareaid })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1160,29 +1084,50 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
|
||||
|
||||
|
||||
private _getURIBeforeStartApplying(opts: CallBeforeStartApplyingOpts) {
|
||||
// SR
|
||||
if (opts.from === 'ClickApply') {
|
||||
const uri = this._uriOfGivenURI(opts.uri)
|
||||
if (!uri) return
|
||||
return uri
|
||||
}
|
||||
else if (opts.from === 'QuickEdit') {
|
||||
const { diffareaid } = opts
|
||||
const ctrlKZone = this.diffAreaOfId[diffareaid]
|
||||
if (ctrlKZone?.type !== 'CtrlKZone') return
|
||||
const { _URI: uri } = ctrlKZone
|
||||
return uri
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
public async callBeforeStartApplying(opts: CallBeforeStartApplyingOpts) {
|
||||
const uri = this._getURIBeforeStartApplying(opts)
|
||||
if (!uri) return
|
||||
await this._voidModelService.initializeModel(uri)
|
||||
}
|
||||
|
||||
|
||||
// the applyDonePromise this returns can reject, and should be caught with .catch
|
||||
public async startApplying(opts: StartApplyingOpts): Promise<[URI, Promise<void>] | null> {
|
||||
public startApplying(opts: StartApplyingOpts): [URI, Promise<void>] | null {
|
||||
let res: [DiffZone, Promise<void>] | undefined = undefined
|
||||
|
||||
if (opts.from === 'QuickEdit') {
|
||||
res = await this._initializeWriteoverStream(opts) // rewrite
|
||||
res = this._initializeWriteoverStream(opts) // rewrite
|
||||
}
|
||||
else if (opts.from === 'ClickApply') {
|
||||
if (this._settingsService.state.globalSettings.enableFastApply) {
|
||||
const numCharsInFile = this._fileLengthOfGivenURI(opts.uri)
|
||||
if (numCharsInFile === null) return null
|
||||
if (numCharsInFile < 1000) { // slow apply for short files (especially important for empty files)
|
||||
res = await this._initializeWriteoverStream(opts)
|
||||
res = this._initializeWriteoverStream(opts)
|
||||
}
|
||||
else {
|
||||
res = await this._initializeSearchAndReplaceStream(opts) // fast apply
|
||||
res = this._initializeSearchAndReplaceStream(opts) // fast apply
|
||||
}
|
||||
}
|
||||
else {
|
||||
res = await this._initializeWriteoverStream(opts) // rewrite
|
||||
res = this._initializeWriteoverStream(opts) // rewrite
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1278,6 +1223,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
_removeStylesFns: new Set(),
|
||||
}
|
||||
|
||||
console.log('FIRING START STREAMING IN DIFFZONE!!!')
|
||||
const diffZone = this._addDiffArea(adding)
|
||||
this._onDidChangeStreamingInDiffZone.fire({ uri, diffareaid: diffZone.diffareaid })
|
||||
this._onDidAddOrDeleteDiffZones.fire({ uri })
|
||||
|
|
@ -1308,19 +1254,17 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
}
|
||||
|
||||
|
||||
private async _initializeWriteoverStream(opts: StartApplyingOpts): Promise<[DiffZone, Promise<void>] | undefined> {
|
||||
private _initializeWriteoverStream(opts: StartApplyingOpts): [DiffZone, Promise<void>] | undefined {
|
||||
|
||||
const { from, } = opts
|
||||
|
||||
let uri: URI
|
||||
let startRange: 'fullFile' | [number, number]
|
||||
const uri = this._getURIBeforeStartApplying(opts)
|
||||
if (!uri) return
|
||||
|
||||
let startRange: 'fullFile' | [number, number]
|
||||
let ctrlKZoneIfQuickEdit: CtrlKZone | null = null
|
||||
|
||||
if (from === 'ClickApply') {
|
||||
const uri_ = this._uriOfGivenURI(opts.uri)
|
||||
if (!uri_) return
|
||||
uri = uri_
|
||||
startRange = 'fullFile'
|
||||
}
|
||||
else if (from === 'QuickEdit') {
|
||||
|
|
@ -1328,15 +1272,13 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
const ctrlKZone = this.diffAreaOfId[diffareaid]
|
||||
if (ctrlKZone?.type !== 'CtrlKZone') return
|
||||
ctrlKZoneIfQuickEdit = ctrlKZone
|
||||
const { startLine: startLine_, endLine: endLine_, _URI } = ctrlKZone
|
||||
uri = _URI
|
||||
const { startLine: startLine_, endLine: endLine_ } = ctrlKZone
|
||||
startRange = [startLine_, endLine_]
|
||||
}
|
||||
else {
|
||||
throw new Error(`Void: diff.type not recognized on: ${from}`)
|
||||
}
|
||||
|
||||
await this._voidModelService.initializeModel(uri)
|
||||
const { model } = this._voidModelService.getModel(uri)
|
||||
if (!model) return
|
||||
|
||||
|
|
@ -1434,7 +1376,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
|
||||
const featureName: FeatureName = opts.from === 'ClickApply' ? 'Apply' : 'Ctrl+K'
|
||||
const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName]
|
||||
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[modelSelection.providerName]?.[modelSelection.modelName] : undefined
|
||||
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] : undefined
|
||||
|
||||
// allowed to throw errors - this is called inside a promise that handles everything
|
||||
const runWriteover = async () => {
|
||||
|
|
@ -1530,13 +1472,12 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
}
|
||||
|
||||
|
||||
private async _initializeSearchAndReplaceStream(opts: StartApplyingOpts & { from: 'ClickApply' }): Promise<[DiffZone, Promise<void>] | undefined> {
|
||||
const { from, applyStr, uri: givenURI, } = opts
|
||||
private _initializeSearchAndReplaceStream(opts: StartApplyingOpts & { from: 'ClickApply' }): [DiffZone, Promise<void>] | undefined {
|
||||
const { from, applyStr, } = opts
|
||||
|
||||
const uri = this._uriOfGivenURI(givenURI)
|
||||
const uri = this._getURIBeforeStartApplying(opts)
|
||||
if (!uri) return
|
||||
|
||||
await this._voidModelService.initializeModel(uri)
|
||||
const { model } = this._voidModelService.getModel(uri)
|
||||
if (!model) return
|
||||
|
||||
|
|
@ -1643,7 +1584,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
|
||||
const featureName: FeatureName = 'Apply'
|
||||
const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName]
|
||||
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[modelSelection.providerName]?.[modelSelection.modelName] : undefined
|
||||
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] : undefined
|
||||
|
||||
const N_RETRIES = 5
|
||||
|
||||
|
|
@ -1691,7 +1632,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
// update stream state to the first line of original if some portion of original has been written
|
||||
if (shouldUpdateOrigStreamStyle && block.orig.trim().length >= 20) {
|
||||
const startingAtLine = diffZone._streamState.line ?? 1 // dont go backwards if already have a stream line
|
||||
const originalRange = findTextInCode(block.orig, originalFileCode, startingAtLine)
|
||||
const originalRange = findTextInCode(block.orig, originalFileCode, false, startingAtLine)
|
||||
if (typeof originalRange !== 'string') {
|
||||
const [startLine, _] = convertOriginalRangeToFinalRange(originalRange)
|
||||
diffZone._streamState.line = startLine
|
||||
|
|
@ -1718,7 +1659,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
if (!(blockNum in addedTrackingZoneOfBlockNum)) {
|
||||
|
||||
|
||||
const originalBounds = findTextInCode(block.orig, originalFileCode)
|
||||
const originalBounds = findTextInCode(block.orig, originalFileCode, true)
|
||||
// if error
|
||||
if (typeof originalBounds === 'string') {
|
||||
console.log('--------------Error finding text in code:')
|
||||
|
|
@ -1757,14 +1698,15 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
return
|
||||
}
|
||||
|
||||
console.log('---------adding-------')
|
||||
console.log('CURRENT TEXT!!!', { current: model?.getValue() })
|
||||
console.log('block', deepClone(block))
|
||||
console.log('origBounds', originalBounds)
|
||||
|
||||
|
||||
const [startLine, endLine] = convertOriginalRangeToFinalRange(originalBounds)
|
||||
console.log('start end', startLine, endLine)
|
||||
|
||||
// console.log('---------adding-------')
|
||||
// console.log('CURRENT TEXT!!!', { current: model?.getValue() })
|
||||
// console.log('block', deepClone(block))
|
||||
// console.log('origBounds', originalBounds)
|
||||
// console.log('start end', startLine, endLine)
|
||||
|
||||
// otherwise if no error, add the position as a diffarea
|
||||
const adding: Omit<TrackingZone<SearchReplaceDiffAreaMetadata>, 'diffareaid'> = {
|
||||
|
|
@ -1821,7 +1763,6 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
onFinalMessage: async (params) => {
|
||||
const { fullText } = params
|
||||
|
||||
console.log('DONE - editCode!', { fullText })
|
||||
|
||||
// 1. wait 500ms and fix lint errors - call lint error workflow
|
||||
// (update react state to say "Fixing errors")
|
||||
|
|
@ -1836,10 +1777,11 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
// IMPORTANT - sort by lineNum
|
||||
addedTrackingZoneOfBlockNum.sort((a, b) => a.metadata.originalBounds[0] - b.metadata.originalBounds[0])
|
||||
|
||||
const { model } = this._voidModelService.getModel(uri)
|
||||
console.log('CURRENT TEXT!!!', { current: model?.getValue() })
|
||||
console.log('addedTrackingZoneOfBlockNum', addedTrackingZoneOfBlockNum)
|
||||
console.log('blocks', deepClone(blocks))
|
||||
// const { model } = this._voidModelService.getModel(uri)
|
||||
// console.log('DONE - editCode!', { fullText })
|
||||
// console.log('CURRENT TEXT!!!', { current: model?.getValue() })
|
||||
// console.log('addedTrackingZoneOfBlockNum', addedTrackingZoneOfBlockNum)
|
||||
// console.log('blocks', deepClone(blocks))
|
||||
|
||||
for (let blockNum = addedTrackingZoneOfBlockNum.length - 1; blockNum >= 0; blockNum -= 1) {
|
||||
const { originalBounds } = addedTrackingZoneOfBlockNum[blockNum].metadata
|
||||
|
|
|
|||
|
|
@ -7,13 +7,20 @@ import { Event } from '../../../../base/common/event.js';
|
|||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { Diff, DiffArea } from './editCodeService.js';
|
||||
|
||||
import { Diff, DiffArea, VoidFileSnapshot } from '../common/editCodeServiceTypes.js';
|
||||
|
||||
|
||||
export type StartBehavior = 'accept-conflicts' | 'reject-conflicts' | 'keep-conflicts'
|
||||
|
||||
export type StartApplyingOpts = ({
|
||||
export type CallBeforeStartApplyingOpts = {
|
||||
from: 'QuickEdit';
|
||||
diffareaid: number; // id of the CtrlK area (contains text selection)
|
||||
} | {
|
||||
from: 'ClickApply';
|
||||
uri: 'current' | URI;
|
||||
}
|
||||
|
||||
export type StartApplyingOpts = {
|
||||
from: 'QuickEdit';
|
||||
diffareaid: number; // id of the CtrlK area (contains text selection)
|
||||
startBehavior: StartBehavior;
|
||||
|
|
@ -22,9 +29,7 @@ export type StartApplyingOpts = ({
|
|||
applyStr: string;
|
||||
uri: 'current' | URI;
|
||||
startBehavior: StartBehavior;
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
|
||||
export type AddCtrlKOpts = {
|
||||
startLine: number,
|
||||
|
|
@ -37,7 +42,8 @@ export const IEditCodeService = createDecorator<IEditCodeService>('editCodeServi
|
|||
export interface IEditCodeService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
startApplying(opts: StartApplyingOpts): Promise<[URI, Promise<void>] | null>;
|
||||
callBeforeStartApplying(opts: CallBeforeStartApplyingOpts): Promise<void>;
|
||||
startApplying(opts: StartApplyingOpts): [URI, Promise<void>] | null;
|
||||
addCtrlKZone(opts: AddCtrlKOpts): number | undefined;
|
||||
removeCtrlKZone(opts: { diffareaid: number }): void;
|
||||
|
||||
|
|
@ -49,7 +55,7 @@ export interface IEditCodeService {
|
|||
|
||||
// events
|
||||
onDidAddOrDeleteDiffZones: Event<{ uri: URI }>;
|
||||
onDidChangeDiffsInDiffZone: Event<{ uri: URI; diffareaid: number }>; // only fires when not streaming!!! streaming would be too much
|
||||
onDidChangeDiffsInDiffZoneNotStreaming: Event<{ uri: URI; diffareaid: number }>; // only fires when not streaming!!! streaming would be too much
|
||||
onDidChangeStreamingInDiffZone: Event<{ uri: URI; diffareaid: number }>;
|
||||
onDidChangeStreamingInCtrlKZone: Event<{ uri: URI; diffareaid: number }>;
|
||||
|
||||
|
|
@ -61,4 +67,6 @@ export interface IEditCodeService {
|
|||
interruptURIStreaming(opts: { uri: URI }): void;
|
||||
|
||||
// testDiffs(): void;
|
||||
getVoidFileSnapshot(uri: URI): VoidFileSnapshot;
|
||||
restoreVoidFileSnapshot(uri: URI, snapshot: VoidFileSnapshot): void;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,34 +3,9 @@
|
|||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ComputedDiff } from '../../common/editCodeServiceTypes.js';
|
||||
import { diffLines } from '../react/out/diff/index.js'
|
||||
|
||||
export type ComputedDiff = {
|
||||
type: 'edit';
|
||||
originalCode: string;
|
||||
originalStartLine: number;
|
||||
originalEndLine: number;
|
||||
code: string;
|
||||
startLine: number; // 1-indexed
|
||||
endLine: number;
|
||||
} | {
|
||||
type: 'insertion';
|
||||
// originalCode: string;
|
||||
originalStartLine: number; // insertion starts on column 0 of this
|
||||
// originalEndLine: number;
|
||||
code: string;
|
||||
startLine: number;
|
||||
endLine: number;
|
||||
} | {
|
||||
type: 'deletion';
|
||||
originalCode: string;
|
||||
originalStartLine: number;
|
||||
originalEndLine: number;
|
||||
// code: string;
|
||||
startLine: number; // deletion starts on column 0 of this
|
||||
// endLine: number;
|
||||
}
|
||||
|
||||
export function findDiffs(oldStr: string, newStr: string) {
|
||||
|
||||
// this makes it so the end of the file always ends with a \n (if you don't have this, then diffing E vs E\n gives an "edit". With it, you end up diffing E\n vs E\n\n which now properly gives an insertion)
|
||||
|
|
|
|||
|
|
@ -1,12 +1,16 @@
|
|||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { useAccessor, useCommandBarState, useCommandBarURIListener, useSettingsState } from '../util/services.js'
|
||||
import { usePromise, useRefState } from '../util/helpers.js'
|
||||
import { isFeatureNameDisabled } from '../../../../common/voidSettingsTypes.js'
|
||||
import { URI } from '../../../../../../../base/common/uri.js'
|
||||
import { FileSymlink, LucideIcon, RotateCw } from 'lucide-react'
|
||||
import { FileSymlink, LucideIcon, RotateCw, Terminal } from 'lucide-react'
|
||||
import { Check, X, Square, Copy, Play, } from 'lucide-react'
|
||||
import { getBasename, ListableToolItem, ToolChildrenWrapper } from '../sidebar-tsx/SidebarChat.js'
|
||||
import { ChatMarkdownRender } from './ChatMarkdownRender.js'
|
||||
|
||||
enum CopyButtonText {
|
||||
Idle = 'Copy',
|
||||
|
|
@ -64,9 +68,9 @@ export const IconShell1 = ({ onClick, Icon, disabled, className }: IconButtonPro
|
|||
// </button>
|
||||
// )
|
||||
|
||||
const COPY_FEEDBACK_TIMEOUT = 1000 // amount of time to say 'Copied!'
|
||||
const COPY_FEEDBACK_TIMEOUT = 1500 // amount of time to say 'Copied!'
|
||||
|
||||
const CopyButton = ({ codeStr }: { codeStr: string }) => {
|
||||
export const CopyButton = ({ codeStr }: { codeStr: string }) => {
|
||||
const accessor = useAccessor()
|
||||
|
||||
const metricsService = accessor.get('IMetricsService')
|
||||
|
|
@ -94,11 +98,6 @@ const CopyButton = ({ codeStr }: { codeStr: string }) => {
|
|||
}
|
||||
|
||||
|
||||
// state persisted for duration of react only
|
||||
// TODO change this to use type `ChatThreads.applyBoxState[applyBoxId]`
|
||||
const applyingURIOfApplyBoxIdRef: { current: { [applyBoxId: string]: URI | undefined } } = { current: {} }
|
||||
|
||||
|
||||
|
||||
|
||||
export const JumpToFileButton = ({ uri }: { uri: URI | 'current' }) => {
|
||||
|
|
@ -113,164 +112,76 @@ export const JumpToFileButton = ({ uri }: { uri: URI | 'current' }) => {
|
|||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
return jumpToFileButton
|
||||
}
|
||||
|
||||
export const useApplyButtonHTML = ({ codeStr, applyBoxId, uri }: { codeStr: string, applyBoxId: string, uri: URI | 'current' }) => {
|
||||
|
||||
|
||||
export const JumpToTerminalButton = ({ onClick }: { onClick: () => void }) => {
|
||||
return (
|
||||
<IconShell1
|
||||
Icon={Terminal}
|
||||
onClick={onClick}
|
||||
className="text-void-fg-1"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// state persisted for duration of react only
|
||||
// TODO change this to use type `ChatThreads.applyBoxState[applyBoxId]`
|
||||
const applyingURIOfApplyBoxIdRef: { current: { [applyBoxId: string]: URI | undefined } } = { current: {} }
|
||||
|
||||
const getUriBeingApplied = (applyBoxId: string) => {
|
||||
return applyingURIOfApplyBoxIdRef.current[applyBoxId] ?? null
|
||||
}
|
||||
|
||||
|
||||
export const useApplyButtonState = ({ applyBoxId, uri }: { applyBoxId: string, uri: URI | 'current' }) => {
|
||||
|
||||
const settingsState = useSettingsState()
|
||||
const isDisabled = !!isFeatureNameDisabled('Apply', settingsState) || !applyBoxId
|
||||
|
||||
const accessor = useAccessor()
|
||||
const editCodeService = accessor.get('IEditCodeService')
|
||||
const voidCommandBarService = accessor.get('IVoidCommandBarService')
|
||||
const metricsService = accessor.get('IMetricsService')
|
||||
|
||||
const [_, rerender] = useState(0)
|
||||
|
||||
const getUriBeingApplied = useCallback(() => {
|
||||
return applyingURIOfApplyBoxIdRef.current[applyBoxId] ?? null
|
||||
}, [applyBoxId])
|
||||
|
||||
const getStreamState = useCallback(() => {
|
||||
const uri = getUriBeingApplied()
|
||||
const uri = getUriBeingApplied(applyBoxId)
|
||||
if (!uri) return 'idle-no-changes'
|
||||
return voidCommandBarService.getStreamState(uri)
|
||||
}, [voidCommandBarService, getUriBeingApplied])
|
||||
}, [voidCommandBarService, applyBoxId])
|
||||
|
||||
// listen for stream updates on this box
|
||||
|
||||
|
||||
useCommandBarURIListener(useCallback((uri_) => {
|
||||
const shouldUpdate = (
|
||||
getUriBeingApplied()?.fsPath === uri_.fsPath
|
||||
getUriBeingApplied(applyBoxId)?.fsPath === uri_.fsPath
|
||||
|| (uri !== 'current' && uri.fsPath === uri_.fsPath)
|
||||
)
|
||||
if (!shouldUpdate) return
|
||||
rerender(c => c + 1)
|
||||
}, [applyBoxId, editCodeService, getUriBeingApplied, uri])
|
||||
)
|
||||
|
||||
const onClickSubmit = useCallback(async () => {
|
||||
if (isDisabled) return
|
||||
if (getStreamState() === 'streaming') return
|
||||
const [newApplyingUri, applyDonePromise] = await editCodeService.startApplying({
|
||||
from: 'ClickApply',
|
||||
applyStr: codeStr,
|
||||
uri: uri,
|
||||
startBehavior: 'keep-conflicts',
|
||||
}) ?? []
|
||||
// catch any errors by interrupting the stream
|
||||
applyDonePromise?.catch(e => { if (newApplyingUri) editCodeService.interruptURIStreaming({ uri: newApplyingUri }) })
|
||||
|
||||
applyingURIOfApplyBoxIdRef.current[applyBoxId] = newApplyingUri ?? undefined
|
||||
|
||||
rerender(c => c + 1)
|
||||
metricsService.capture('Apply Code', { length: codeStr.length }) // capture the length only
|
||||
}, [isDisabled, getStreamState, editCodeService, codeStr, uri, applyBoxId, metricsService])
|
||||
|
||||
|
||||
const onInterrupt = useCallback(() => {
|
||||
if (getStreamState() !== 'streaming') return
|
||||
const uri = getUriBeingApplied()
|
||||
if (!uri) return
|
||||
|
||||
editCodeService.interruptURIStreaming({ uri })
|
||||
metricsService.capture('Stop Apply', {})
|
||||
}, [getStreamState, getUriBeingApplied, editCodeService, metricsService])
|
||||
|
||||
const onAccept = useCallback(() => {
|
||||
const uri = getUriBeingApplied()
|
||||
if (uri) editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false })
|
||||
}, [getUriBeingApplied, editCodeService])
|
||||
|
||||
const onReject = useCallback(() => {
|
||||
const uri = getUriBeingApplied()
|
||||
if (uri) editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false })
|
||||
}, [getUriBeingApplied, editCodeService])
|
||||
|
||||
const onReapply = useCallback(() => {
|
||||
onReject()
|
||||
onClickSubmit()
|
||||
}, [onReject, onClickSubmit])
|
||||
if (shouldUpdate) {
|
||||
rerender(c => c + 1)
|
||||
console.log('rerendering....')
|
||||
}
|
||||
}, [applyBoxId, applyBoxId, uri]))
|
||||
|
||||
const currStreamState = getStreamState()
|
||||
|
||||
const copyButton = (
|
||||
<CopyButton codeStr={codeStr} />
|
||||
)
|
||||
|
||||
const playButton = (
|
||||
<IconShell1
|
||||
Icon={Play}
|
||||
onClick={onClickSubmit}
|
||||
/>
|
||||
)
|
||||
|
||||
const stopButton = (
|
||||
<IconShell1
|
||||
Icon={Square}
|
||||
onClick={onInterrupt}
|
||||
/>
|
||||
)
|
||||
|
||||
const reapplyButton = (
|
||||
<IconShell1
|
||||
Icon={RotateCw}
|
||||
onClick={onReapply}
|
||||
/>
|
||||
)
|
||||
|
||||
const acceptButton = (
|
||||
<IconShell1
|
||||
Icon={Check}
|
||||
onClick={onAccept}
|
||||
className="text-green-600"
|
||||
/>
|
||||
)
|
||||
|
||||
const rejectButton = (
|
||||
<IconShell1
|
||||
Icon={X}
|
||||
onClick={onReject}
|
||||
className="text-red-600"
|
||||
/>
|
||||
)
|
||||
|
||||
|
||||
|
||||
let buttonsHTML = <></>
|
||||
|
||||
if (currStreamState === 'streaming') {
|
||||
buttonsHTML = <>
|
||||
<JumpToFileButton uri={uri} />
|
||||
{copyButton}
|
||||
{stopButton}
|
||||
</>
|
||||
return {
|
||||
getStreamState,
|
||||
isDisabled,
|
||||
currStreamState,
|
||||
}
|
||||
}
|
||||
|
||||
if (currStreamState === 'idle-no-changes') {
|
||||
buttonsHTML = <>
|
||||
<JumpToFileButton uri={uri} />
|
||||
{copyButton}
|
||||
{playButton}
|
||||
</>
|
||||
}
|
||||
|
||||
if (currStreamState === 'idle-has-changes') {
|
||||
buttonsHTML = <>
|
||||
<JumpToFileButton uri={uri} />
|
||||
{reapplyButton}
|
||||
{rejectButton}
|
||||
{acceptButton}
|
||||
</>
|
||||
}
|
||||
export const StatusIndicatorHTML = ({ applyBoxId, uri }: { applyBoxId: string, uri: URI | 'current' }) => {
|
||||
const { currStreamState } = useApplyButtonState({ applyBoxId, uri })
|
||||
|
||||
const statusIndicatorHTML = <div className='flex flex-row items-center min-h-4 max-h-4 min-w-4 max-w-4'>
|
||||
return <div className='flex flex-row items-center min-h-4 max-h-4 min-w-4 max-w-4'>
|
||||
<div
|
||||
className={` size-1.5 rounded-full border
|
||||
${currStreamState === 'idle-no-changes' ? 'bg-void-bg-3 border-void-border-1' :
|
||||
${currStreamState === 'idle-no-changes' ? 'bg-void-bg-3 border-void-border-1' :
|
||||
currStreamState === 'streaming' ? 'bg-orange-500 border-orange-500 shadow-[0_0_4px_0px_rgba(234,88,12,0.6)]' :
|
||||
currStreamState === 'idle-has-changes' ? 'bg-green-500 border-green-500 shadow-[0_0_4px_0px_rgba(22,163,74,0.6)]' :
|
||||
'bg-void-border-1 border-void-border-1'
|
||||
|
|
@ -278,18 +189,97 @@ export const useApplyButtonHTML = ({ codeStr, applyBoxId, uri }: { codeStr: stri
|
|||
}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
return {
|
||||
statusIndicatorHTML,
|
||||
buttonsHTML,
|
||||
export const ApplyButtonsHTML = ({ codeStr, applyBoxId, reapplyIcon, uri }: { codeStr: string, applyBoxId: string, reapplyIcon: boolean, uri: URI | 'current' }) => {
|
||||
const accessor = useAccessor()
|
||||
const editCodeService = accessor.get('IEditCodeService')
|
||||
const metricsService = accessor.get('IMetricsService')
|
||||
|
||||
const {
|
||||
currStreamState,
|
||||
isDisabled,
|
||||
getStreamState,
|
||||
} = useApplyButtonState({ applyBoxId, uri })
|
||||
|
||||
const onClickSubmit = useCallback(async () => {
|
||||
if (isDisabled) return
|
||||
if (getStreamState() === 'streaming') return
|
||||
const opts = {
|
||||
from: 'ClickApply',
|
||||
applyStr: codeStr,
|
||||
uri: uri,
|
||||
startBehavior: 'reject-conflicts',
|
||||
} as const
|
||||
|
||||
await editCodeService.callBeforeStartApplying(opts)
|
||||
const [newApplyingUri, applyDonePromise] = editCodeService.startApplying(opts) ?? []
|
||||
|
||||
// catch any errors by interrupting the stream
|
||||
applyDonePromise?.catch(e => { if (newApplyingUri) editCodeService.interruptURIStreaming({ uri: newApplyingUri }) })
|
||||
|
||||
applyingURIOfApplyBoxIdRef.current[applyBoxId] = newApplyingUri ?? undefined
|
||||
|
||||
// rerender(c => c + 1)
|
||||
metricsService.capture('Apply Code', { length: codeStr.length }) // capture the length only
|
||||
}, [isDisabled, getStreamState, editCodeService, codeStr, uri, applyBoxId, metricsService])
|
||||
|
||||
|
||||
const onInterrupt = useCallback(() => {
|
||||
if (getStreamState() !== 'streaming') return
|
||||
const uri = getUriBeingApplied(applyBoxId)
|
||||
if (!uri) return
|
||||
|
||||
editCodeService.interruptURIStreaming({ uri })
|
||||
metricsService.capture('Stop Apply', {})
|
||||
}, [getStreamState, applyBoxId, editCodeService, metricsService])
|
||||
|
||||
const onAccept = useCallback(() => {
|
||||
const uri = getUriBeingApplied(applyBoxId)
|
||||
if (uri) editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false })
|
||||
}, [applyBoxId, editCodeService])
|
||||
|
||||
const onReject = useCallback(() => {
|
||||
const uri = getUriBeingApplied(applyBoxId)
|
||||
if (uri) editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false })
|
||||
}, [applyBoxId, editCodeService])
|
||||
|
||||
// const onReapply = useCallback(() => {
|
||||
// onReject()
|
||||
// onClickSubmit()
|
||||
// }, [onReject, onClickSubmit])
|
||||
|
||||
|
||||
if (currStreamState === 'streaming') {
|
||||
return <IconShell1 Icon={Square} onClick={onInterrupt} />
|
||||
}
|
||||
|
||||
if (currStreamState === 'idle-no-changes') {
|
||||
return <IconShell1 Icon={reapplyIcon ? RotateCw : Play} onClick={onClickSubmit} />
|
||||
}
|
||||
|
||||
if (currStreamState === 'idle-has-changes') {
|
||||
return <>
|
||||
{/* <IconShell1
|
||||
Icon={RotateCw}
|
||||
onClick={onReapply}
|
||||
/> */}
|
||||
<IconShell1
|
||||
Icon={X}
|
||||
onClick={onReject}
|
||||
className="text-red-600"
|
||||
/>
|
||||
<IconShell1
|
||||
Icon={Check}
|
||||
onClick={onAccept}
|
||||
className="text-green-600"
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export const BlockCodeApplyWrapper = ({
|
||||
children,
|
||||
initValue,
|
||||
|
|
@ -305,10 +295,10 @@ export const BlockCodeApplyWrapper = ({
|
|||
language: string;
|
||||
uri: URI | 'current',
|
||||
}) => {
|
||||
|
||||
const { statusIndicatorHTML, buttonsHTML } = useApplyButtonHTML({ codeStr: initValue, applyBoxId, uri })
|
||||
const accessor = useAccessor()
|
||||
const commandService = accessor.get('ICommandService')
|
||||
const { currStreamState } = useApplyButtonState({ applyBoxId, uri })
|
||||
|
||||
|
||||
const name = uri !== 'current' ?
|
||||
<ListableToolItem
|
||||
|
|
@ -324,13 +314,15 @@ export const BlockCodeApplyWrapper = ({
|
|||
{/* header */}
|
||||
<div className=" select-none flex justify-between items-center py-1 px-2 border-b border-void-border-3 cursor-default">
|
||||
<div className="flex items-center">
|
||||
{statusIndicatorHTML}
|
||||
<StatusIndicatorHTML uri={uri} applyBoxId={applyBoxId} />
|
||||
<span className="text-[13px] font-light text-void-fg-3">
|
||||
{name}
|
||||
</span>
|
||||
</div>
|
||||
<div className={`${canApply ? '' : 'hidden'} flex items-center gap-1`}>
|
||||
{buttonsHTML}
|
||||
<JumpToFileButton uri={uri} />
|
||||
{currStreamState === 'idle-no-changes' && <CopyButton codeStr={initValue} />}
|
||||
<ApplyButtonsHTML uri={uri} applyBoxId={applyBoxId} codeStr={initValue} reapplyIcon={false} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -63,11 +63,14 @@ export const QuickEditChat = ({
|
|||
if (isStreamingRef.current) return
|
||||
textAreaFnsRef.current?.disable()
|
||||
|
||||
const [newApplyingUri, applyDonePromise] = await editCodeService.startApplying({
|
||||
const opts = {
|
||||
from: 'QuickEdit',
|
||||
diffareaid,
|
||||
startBehavior: 'keep-conflicts',
|
||||
}) ?? []
|
||||
} as const
|
||||
|
||||
await editCodeService.callBeforeStartApplying(opts)
|
||||
const [newApplyingUri, applyDonePromise] = editCodeService.startApplying(opts) ?? []
|
||||
// catch any errors by interrupting the stream
|
||||
applyDonePromise?.catch(e => { if (newApplyingUri) editCodeService.interruptCtrlKStreaming({ diffareaid }) })
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -72,7 +72,7 @@ export const SidebarThreadSelector = () => {
|
|||
let firstMsg = null;
|
||||
// let secondMsg = null;
|
||||
|
||||
const firstUserMsgIdx = pastThread.messages.findIndex((msg) => msg.role !== 'tool' && msg.role !== 'tool_request');
|
||||
const firstUserMsgIdx = pastThread.messages.findIndex((msg) => msg.role === 'user');
|
||||
|
||||
if (firstUserMsgIdx !== -1) {
|
||||
// firstMsg = truncate(pastThread.messages[firstMsgIdx].displayContent ?? '');
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ import { IThemeService } from '../../../../../../../platform/theme/common/themeS
|
|||
import { ILLMMessageService } from '../../../../common/sendLLMMessageService.js';
|
||||
import { IRefreshModelService } from '../../../../../../../workbench/contrib/void/common/refreshModelService.js';
|
||||
import { IVoidSettingsService } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js';
|
||||
import { IEditCodeService } from '../../../editCodeServiceInterface.js'
|
||||
|
||||
import { ISidebarStateService } from '../../../sidebarStateService.js';
|
||||
import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js'
|
||||
|
|
@ -47,6 +46,7 @@ import { IVoidModelService } from '../../../../common/voidModelService.js'
|
|||
import { IWorkspaceContextService } from '../../../../../../../platform/workspace/common/workspace.js'
|
||||
import { IVoidCommandBarService } from '../../../voidCommandBarService.js'
|
||||
import { INativeHostService } from '../../../../../../../platform/native/common/native.js';
|
||||
import { IEditCodeService } from '../../../editCodeServiceInterface.js'
|
||||
|
||||
|
||||
// normally to do this you'd use a useEffect that calls .onDidChangeState(), but useEffect mounts too late and misses initial state changes
|
||||
|
|
|
|||
|
|
@ -262,7 +262,6 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider
|
|||
const settingsState = useSettingsState()
|
||||
|
||||
const settingValue = settingsState.settingsOfProvider[providerName][settingName] as string // this should always be a string in this component
|
||||
console.log(`providerName:${providerName} settingName: ${settingName}, settingValue: ${settingValue}`)
|
||||
if (typeof settingValue !== 'string') {
|
||||
console.log('Error: Provider setting had a non-string value.')
|
||||
return
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextke
|
|||
|
||||
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 { IMetricsService } from '../common/metricsService.js';
|
||||
import { ISidebarStateService } from './sidebarStateService.js';
|
||||
|
|
@ -53,23 +52,41 @@ export const roundRangeToLines = (range: IRange | null | undefined, options: { e
|
|||
return newRange
|
||||
}
|
||||
|
||||
const getContentInRange = (model: ITextModel, range: IRange | null) => {
|
||||
if (!range)
|
||||
return null
|
||||
const content = model.getValueInRange(range)
|
||||
const trimmedContent = content
|
||||
.replace(/^\s*\n/g, '') // trim pure whitespace lines from start
|
||||
.replace(/\n\s*$/g, '') // trim pure whitespace lines from end
|
||||
return trimmedContent
|
||||
}
|
||||
// const getContentInRange = (model: ITextModel, range: IRange | null) => {
|
||||
// if (!range)
|
||||
// return null
|
||||
// const content = model.getValueInRange(range)
|
||||
// const trimmedContent = content
|
||||
// .replace(/^\s*\n/g, '') // trim pure whitespace lines from start
|
||||
// .replace(/\n\s*$/g, '') // trim pure whitespace lines from end
|
||||
// return trimmedContent
|
||||
// }
|
||||
|
||||
|
||||
const findMatchingStagingIndex = (currentSelections: StagingSelectionItem[] | undefined, newSelection: StagingSelectionItem) => {
|
||||
return currentSelections?.findIndex(s =>
|
||||
s.fileURI.fsPath === newSelection.fileURI.fsPath
|
||||
&& s.range?.startLineNumber === newSelection.range?.startLineNumber
|
||||
&& s.range?.endLineNumber === newSelection.range?.endLineNumber
|
||||
)
|
||||
const findStagingItemToReplace = (currentSelections: StagingSelectionItem[] | undefined, newSelection: StagingSelectionItem): [number, StagingSelectionItem] | null => {
|
||||
if (!currentSelections) return null
|
||||
|
||||
for (let i = 0; i < currentSelections.length; i += 1) {
|
||||
const s = currentSelections[i]
|
||||
|
||||
if (s.uri.fsPath !== newSelection.uri.fsPath) continue
|
||||
|
||||
if (s.type === 'File' && newSelection.type === 'File') {
|
||||
return [i, s] as const
|
||||
}
|
||||
if (s.type === 'CodeSelection' && newSelection.type === 'CodeSelection') {
|
||||
if (s.uri.fsPath !== newSelection.uri.fsPath) continue
|
||||
// if there's any collision return true
|
||||
const [oldStart, oldEnd] = s.range
|
||||
const [newStart, newEnd] = newSelection.range
|
||||
if (oldStart !== newStart || oldEnd !== newEnd) continue
|
||||
return [i, s] as const
|
||||
}
|
||||
if (s.type === 'Folder' && newSelection.type === 'Folder') {
|
||||
return [i, s] as const
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const VOID_OPEN_SIDEBAR_ACTION_ID = 'void.sidebar.open'
|
||||
|
|
@ -114,22 +131,18 @@ registerAction2(class extends Action2 {
|
|||
editor?.setSelection({ startLineNumber: selectionRange.startLineNumber, endLineNumber: selectionRange.endLineNumber, startColumn: 1, endColumn: Number.MAX_SAFE_INTEGER })
|
||||
}
|
||||
|
||||
const selectionStr = getContentInRange(model, selectionRange)
|
||||
|
||||
const selection: StagingSelectionItem = !selectionRange || !selectionStr || (selectionRange.startLineNumber > selectionRange.endLineNumber) ? {
|
||||
const selection: StagingSelectionItem = !selectionRange || (selectionRange.startLineNumber > selectionRange.endLineNumber) ? {
|
||||
type: 'File',
|
||||
fileURI: model.uri,
|
||||
uri: model.uri,
|
||||
language: model.getLanguageId(),
|
||||
selectionStr: null,
|
||||
range: null,
|
||||
state: { isOpened: false, wasAddedAsCurrentFile: false }
|
||||
state: { wasAddedAsCurrentFile: false }
|
||||
} : {
|
||||
type: 'Selection',
|
||||
fileURI: model.uri,
|
||||
type: 'CodeSelection',
|
||||
uri: model.uri,
|
||||
language: model.getLanguageId(),
|
||||
selectionStr: selectionStr,
|
||||
range: selectionRange,
|
||||
state: { isOpened: true, wasAddedAsCurrentFile: false }
|
||||
range: [selectionRange.startLineNumber, selectionRange.endLineNumber],
|
||||
state: { wasAddedAsCurrentFile: false }
|
||||
}
|
||||
|
||||
// update the staging selections
|
||||
|
|
@ -149,17 +162,18 @@ registerAction2(class extends Action2 {
|
|||
setSelections = (s) => chatThreadService.setCurrentMessageState(focusedMessageIdx, { stagingSelections: s })
|
||||
}
|
||||
|
||||
// close all selections besides the new one
|
||||
selections = selections.map(s => ({ ...s, state: { ...s.state, isOpened: false } }))
|
||||
|
||||
// if matches with existing selection, overwrite (since text may change)
|
||||
const matchingStagingEltIdx = findMatchingStagingIndex(selections, selection)
|
||||
if (matchingStagingEltIdx !== undefined && matchingStagingEltIdx !== -1) {
|
||||
setSelections([
|
||||
...selections!.slice(0, matchingStagingEltIdx),
|
||||
selection,
|
||||
...selections!.slice(matchingStagingEltIdx + 1, Infinity)
|
||||
])
|
||||
const replaceRes = findStagingItemToReplace(selections, selection)
|
||||
if (replaceRes) {
|
||||
const [idx, newSel] = replaceRes
|
||||
|
||||
if (idx !== undefined && idx !== -1) {
|
||||
setSelections([
|
||||
...selections!.slice(0, idx),
|
||||
newSel,
|
||||
...selections!.slice(idx + 1, Infinity)
|
||||
])
|
||||
}
|
||||
}
|
||||
// if no match, add it
|
||||
else {
|
||||
|
|
@ -200,7 +214,11 @@ registerAction2(class extends Action2 {
|
|||
id: 'void.newChatAction',
|
||||
title: 'New Chat',
|
||||
icon: { id: 'add' },
|
||||
menu: [{ id: MenuId.ViewTitle, group: 'navigation', when: ContextKeyExpr.equals('view', VOID_VIEW_ID), }]
|
||||
menu: [{ id: MenuId.ViewTitle, group: 'navigation', when: ContextKeyExpr.equals('view', VOID_VIEW_ID), }],
|
||||
keybinding: {
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyL,
|
||||
weight: KeybindingWeight.VoidExtension,
|
||||
},
|
||||
});
|
||||
}
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -8,11 +8,14 @@ import { QueryBuilder } from '../../../services/search/common/queryBuilder.js'
|
|||
import { ISearchService } from '../../../services/search/common/search.js'
|
||||
import { IEditCodeService } from './editCodeServiceInterface.js'
|
||||
import { ITerminalToolService } from './terminalToolService.js'
|
||||
import { ToolCallParams, ToolDirectoryItem, ToolName, ToolResultType } from '../common/toolsServiceTypes.js'
|
||||
import { ToolCallParams, ToolName, ToolResultType } from '../common/toolsServiceTypes.js'
|
||||
import { IVoidModelService } from '../common/voidModelService.js'
|
||||
import { EndOfLinePreference } from '../../../../editor/common/model.js'
|
||||
import { basename } from '../../../../base/common/path.js'
|
||||
import { IVoidCommandBarService } from './voidCommandBarService.js'
|
||||
import { computeDirectoryTree1Deep, IDirectoryStrService, stringifyDirectoryTree1Deep } from './directoryStrService.js'
|
||||
import { IMarkerService } from '../../../../platform/markers/common/markers.js'
|
||||
import { timeout } from '../../../../base/common/async.js'
|
||||
|
||||
|
||||
// tool use for AI
|
||||
|
|
@ -22,83 +25,20 @@ import { IVoidCommandBarService } from './voidCommandBarService.js'
|
|||
|
||||
type ValidateParams = { [T in ToolName]: (p: string) => Promise<ToolCallParams[T]> }
|
||||
type CallTool = { [T in ToolName]: (p: ToolCallParams[T]) => Promise<{ result: ToolResultType[T], interruptTool?: () => void }> }
|
||||
type ToolResultToString = { [T in ToolName]: (p: ToolCallParams[T], result: ToolResultType[T]) => string }
|
||||
type ToolResultToString = { [T in ToolName]: (p: ToolCallParams[T], result: Awaited<ToolResultType[T]>) => string }
|
||||
|
||||
|
||||
|
||||
|
||||
// pagination info
|
||||
const MAX_FILE_CHARS_PAGE = 50_000
|
||||
const MAX_CHILDREN_URIs_PAGE = 500
|
||||
export const MAX_FILE_CHARS_PAGE = 50_000
|
||||
export const MAX_CHILDREN_URIs_PAGE = 500
|
||||
export const MAX_TERMINAL_CHARS_PAGE = 20_000
|
||||
export const TERMINAL_TIMEOUT_TIME = 15
|
||||
export const TERMINAL_BG_WAIT_TIME = 1
|
||||
|
||||
|
||||
|
||||
const computeDirectoryResult = async (
|
||||
fileService: IFileService,
|
||||
rootURI: URI,
|
||||
pageNumber: number = 1
|
||||
): Promise<ToolResultType['list_dir']> => {
|
||||
const stat = await fileService.resolve(rootURI, { resolveMetadata: false });
|
||||
if (!stat.isDirectory) {
|
||||
return { children: null, hasNextPage: false, hasPrevPage: false, itemsRemaining: 0 };
|
||||
}
|
||||
|
||||
const originalChildrenLength = stat.children?.length ?? 0;
|
||||
const fromChildIdx = MAX_CHILDREN_URIs_PAGE * (pageNumber - 1);
|
||||
const toChildIdx = MAX_CHILDREN_URIs_PAGE * pageNumber - 1; // INCLUSIVE
|
||||
const listChildren = stat.children?.slice(fromChildIdx, toChildIdx + 1) ?? [];
|
||||
|
||||
const children: ToolDirectoryItem[] = listChildren.map(child => ({
|
||||
name: child.name,
|
||||
uri: child.resource,
|
||||
isDirectory: child.isDirectory,
|
||||
isSymbolicLink: child.isSymbolicLink
|
||||
}));
|
||||
|
||||
const hasNextPage = (originalChildrenLength - 1) > toChildIdx;
|
||||
const hasPrevPage = pageNumber > 1;
|
||||
const itemsRemaining = Math.max(0, originalChildrenLength - (toChildIdx + 1));
|
||||
|
||||
return {
|
||||
children,
|
||||
hasNextPage,
|
||||
hasPrevPage,
|
||||
itemsRemaining
|
||||
};
|
||||
};
|
||||
|
||||
const directoryResultToString = (params: ToolCallParams['list_dir'], result: ToolResultType['list_dir']): string => {
|
||||
if (!result.children) {
|
||||
return `Error: ${params.rootURI} is not a directory`;
|
||||
}
|
||||
|
||||
let output = '';
|
||||
const entries = result.children;
|
||||
|
||||
if (!result.hasPrevPage) { // is first page
|
||||
output += `${params.rootURI.fsPath}\n`;
|
||||
}
|
||||
|
||||
for (let i = 0; i < entries.length; i++) {
|
||||
const entry = entries[i];
|
||||
const isLast = i === entries.length - 1 && !result.hasNextPage;
|
||||
const prefix = isLast ? '└── ' : '├── ';
|
||||
|
||||
output += `${prefix}${entry.name}${entry.isDirectory ? '/' : ''}${entry.isSymbolicLink ? ' (symbolic link)' : ''}\n`;
|
||||
}
|
||||
|
||||
if (result.hasNextPage) {
|
||||
output += `└── (${result.itemsRemaining} results remaining...)\n`;
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const validateJSON = (s: string): { [s: string]: unknown } => {
|
||||
|
|
@ -117,7 +57,9 @@ const validateJSON = (s: string): { [s: string]: unknown } => {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
const isFalsy = (u: unknown) => {
|
||||
return !u || u === 'null' || u === 'undefined'
|
||||
}
|
||||
|
||||
const validateStr = (argName: string, value: unknown) => {
|
||||
if (typeof value !== 'string') throw new Error(`Invalid LLM output format: ${argName} must be a string.`)
|
||||
|
|
@ -126,13 +68,24 @@ const validateStr = (argName: string, value: unknown) => {
|
|||
|
||||
|
||||
// We are NOT checking to make sure in workspace
|
||||
// TODO!!!! check to make sure folder/file exists
|
||||
const validateURI = (uriStr: unknown) => {
|
||||
if (typeof uriStr !== 'string') throw new Error('Invalid LLM output format: Provided uri must be a string.')
|
||||
|
||||
const uri = URI.file(uriStr)
|
||||
return uri
|
||||
}
|
||||
|
||||
const validateOptionalURI = (uriStr: unknown) => {
|
||||
if (isFalsy(uriStr)) return null
|
||||
return validateURI(uriStr)
|
||||
}
|
||||
|
||||
const validateOptionalStr = (argName: string, str: unknown) => {
|
||||
if (isFalsy(str)) return null
|
||||
return validateStr(argName, str)
|
||||
}
|
||||
|
||||
|
||||
const validatePageNum = (pageNumberUnknown: unknown) => {
|
||||
if (!pageNumberUnknown) return 1
|
||||
const parsedInt = Number.parseInt(pageNumberUnknown + '')
|
||||
|
|
@ -141,6 +94,20 @@ const validatePageNum = (pageNumberUnknown: unknown) => {
|
|||
return parsedInt
|
||||
}
|
||||
|
||||
const validateNumber = (numStr: unknown, opts: { default: number | null }) => {
|
||||
if (typeof numStr === 'number')
|
||||
return numStr
|
||||
if (isFalsy(numStr)) return opts.default
|
||||
|
||||
if (typeof numStr === 'string') {
|
||||
const parsedInt = Number.parseInt(numStr + '')
|
||||
if (!Number.isInteger(parsedInt)) return opts.default
|
||||
return parsedInt
|
||||
}
|
||||
|
||||
return opts.default
|
||||
}
|
||||
|
||||
const validateRecursiveParamStr = (paramsUnknown: unknown) => {
|
||||
if (typeof paramsUnknown !== 'string') throw new Error('Invalid LLM output format: Error calling tool: provided params must be a string.')
|
||||
const params = paramsUnknown
|
||||
|
|
@ -154,12 +121,15 @@ const validateProposedTerminalId = (terminalIdUnknown: unknown) => {
|
|||
return terminalId
|
||||
}
|
||||
|
||||
const validateWaitForCompletion = (b: unknown) => {
|
||||
const validateBoolean = (b: unknown, opts: { default: boolean }) => {
|
||||
if (typeof b === 'string') {
|
||||
if (b === 'true') return true
|
||||
if (b === 'false') return false
|
||||
}
|
||||
return true // default is true
|
||||
if (typeof b === 'boolean') {
|
||||
return b
|
||||
}
|
||||
return opts.default
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -195,6 +165,8 @@ export class ToolsService implements IToolsService {
|
|||
@IEditCodeService editCodeService: IEditCodeService,
|
||||
@ITerminalToolService private readonly terminalToolService: ITerminalToolService,
|
||||
@IVoidCommandBarService private readonly commandBarService: IVoidCommandBarService,
|
||||
@IDirectoryStrService private readonly directoryStrService: IDirectoryStrService,
|
||||
@IMarkerService private readonly markerService: IMarkerService,
|
||||
) {
|
||||
|
||||
const queryBuilder = instantiationService.createInstance(QueryBuilder);
|
||||
|
|
@ -202,14 +174,17 @@ export class ToolsService implements IToolsService {
|
|||
this.validateParams = {
|
||||
read_file: async (params: string) => {
|
||||
const o = validateJSON(params)
|
||||
const { uri: uriStr, pageNumber: pageNumberUnknown } = o
|
||||
const { uri: uriStr, startLine: startLineUnknown, endLine: endLineUnknown, pageNumber: pageNumberUnknown } = o
|
||||
|
||||
const uri = validateURI(uriStr)
|
||||
const pageNumber = validatePageNum(pageNumberUnknown)
|
||||
|
||||
return { uri, pageNumber }
|
||||
const startLine = validateNumber(startLineUnknown, { default: null })
|
||||
const endLine = validateNumber(endLineUnknown, { default: null })
|
||||
|
||||
return { uri, startLine, endLine, pageNumber }
|
||||
},
|
||||
list_dir: async (params: string) => {
|
||||
ls_dir: async (params: string) => {
|
||||
const o = validateJSON(params)
|
||||
const { uri: uriStr, pageNumber: pageNumberUnknown } = o
|
||||
|
||||
|
|
@ -217,29 +192,48 @@ export class ToolsService implements IToolsService {
|
|||
const pageNumber = validatePageNum(pageNumberUnknown)
|
||||
return { rootURI: uri, pageNumber }
|
||||
},
|
||||
pathname_search: async (params: string) => {
|
||||
get_dir_structure: async (params: string) => {
|
||||
const o = validateJSON(params)
|
||||
const { query: queryUnknown, pageNumber: pageNumberUnknown } = o
|
||||
const { uri: uriStr, } = o
|
||||
const uri = validateURI(uriStr)
|
||||
return { rootURI: uri }
|
||||
},
|
||||
search_pathnames_only: async (params: string) => {
|
||||
const o = validateJSON(params)
|
||||
const {
|
||||
query: queryUnknown,
|
||||
include: includeUnknown,
|
||||
pageNumber: pageNumberUnknown
|
||||
} = o
|
||||
|
||||
const queryStr = validateStr('query', queryUnknown)
|
||||
const pageNumber = validatePageNum(pageNumberUnknown)
|
||||
const include = validateOptionalStr('include', includeUnknown)
|
||||
|
||||
return { queryStr, pageNumber }
|
||||
return { queryStr, include, pageNumber }
|
||||
|
||||
},
|
||||
grep_search: async (params: string) => {
|
||||
search_files: async (params: string) => {
|
||||
const o = validateJSON(params)
|
||||
const { query: queryUnknown, pageNumber: pageNumberUnknown } = o
|
||||
const {
|
||||
query: queryUnknown,
|
||||
searchInFolder: searchInFolderUnknown,
|
||||
isRegex: isRegexUnknown,
|
||||
pageNumber: pageNumberUnknown
|
||||
} = o
|
||||
|
||||
const queryStr = validateStr('query', queryUnknown)
|
||||
const pageNumber = validatePageNum(pageNumberUnknown)
|
||||
|
||||
return { queryStr, pageNumber }
|
||||
const searchInFolder = validateOptionalURI(searchInFolderUnknown)
|
||||
const isRegex = validateBoolean(isRegexUnknown, { default: false })
|
||||
|
||||
return { queryStr, searchInFolder, isRegex, pageNumber }
|
||||
},
|
||||
|
||||
// ---
|
||||
|
||||
create_uri: async (params: string) => {
|
||||
create_file_or_folder: async (params: string) => {
|
||||
const o = validateJSON(params)
|
||||
const { uri: uriUnknown } = o
|
||||
const uri = validateURI(uriUnknown)
|
||||
|
|
@ -248,7 +242,7 @@ export class ToolsService implements IToolsService {
|
|||
return { uri, isFolder }
|
||||
},
|
||||
|
||||
delete_uri: async (params: string) => {
|
||||
delete_file_or_folder: async (params: string) => {
|
||||
const o = validateJSON(params)
|
||||
const { uri: uriUnknown, params: paramsStr } = o
|
||||
const uri = validateURI(uriUnknown)
|
||||
|
|
@ -258,7 +252,7 @@ export class ToolsService implements IToolsService {
|
|||
return { uri, isRecursive, isFolder }
|
||||
},
|
||||
|
||||
edit: async (params: string) => {
|
||||
edit_file: async (params: string) => {
|
||||
const o = validateJSON(params)
|
||||
const { uri: uriStr, changeDescription: changeDescriptionUnknown } = o
|
||||
const uri = validateURI(uriStr)
|
||||
|
|
@ -266,12 +260,12 @@ export class ToolsService implements IToolsService {
|
|||
return { uri, changeDescription }
|
||||
},
|
||||
|
||||
terminal_command: async (s: string) => {
|
||||
run_terminal_command: async (s: string) => {
|
||||
const o = validateJSON(s)
|
||||
const { command: commandUnknown, terminalId: terminalIdUnknown, waitForCompletion: waitForCompletionUnknown } = o
|
||||
const command = validateStr('command', commandUnknown)
|
||||
const proposedTerminalId = validateProposedTerminalId(terminalIdUnknown)
|
||||
const waitForCompletion = validateWaitForCompletion(waitForCompletionUnknown)
|
||||
const waitForCompletion = validateBoolean(waitForCompletionUnknown, { default: true })
|
||||
return { command, proposedTerminalId, waitForCompletion }
|
||||
},
|
||||
|
||||
|
|
@ -279,28 +273,46 @@ export class ToolsService implements IToolsService {
|
|||
|
||||
|
||||
this.callTool = {
|
||||
read_file: async ({ uri, pageNumber }) => {
|
||||
read_file: async ({ uri, startLine, endLine, pageNumber }) => {
|
||||
await voidModelService.initializeModel(uri)
|
||||
const { model } = await voidModelService.getModelSafe(uri)
|
||||
if (model === null) { throw new Error(`Contents were empty. There may have been an error, or the file may not exist.`) }
|
||||
const readFileContents = model.getValue(EndOfLinePreference.LF)
|
||||
|
||||
let contents: string
|
||||
if (startLine === null && endLine === null) {
|
||||
contents = model.getValue(EndOfLinePreference.LF)
|
||||
}
|
||||
else {
|
||||
const startLineNumber = startLine === null ? 1 : startLine
|
||||
const endLineNumber = endLine === null ? model.getLineCount() : endLine
|
||||
contents = model.getValueInRange({ startLineNumber, startColumn: 1, endLineNumber, endColumn: Number.MAX_SAFE_INTEGER }, EndOfLinePreference.LF)
|
||||
}
|
||||
|
||||
const fromIdx = MAX_FILE_CHARS_PAGE * (pageNumber - 1)
|
||||
const toIdx = MAX_FILE_CHARS_PAGE * pageNumber - 1
|
||||
const fileContents = readFileContents.slice(fromIdx, toIdx + 1) // paginate
|
||||
const hasNextPage = (readFileContents.length - 1) - toIdx >= 1
|
||||
const fileContents = contents.slice(fromIdx, toIdx + 1) // paginate
|
||||
const hasNextPage = (contents.length - 1) - toIdx >= 1
|
||||
|
||||
return { result: { fileContents, hasNextPage } }
|
||||
},
|
||||
|
||||
list_dir: async ({ rootURI, pageNumber }) => {
|
||||
const dirResult = await computeDirectoryResult(fileService, rootURI, pageNumber)
|
||||
ls_dir: async ({ rootURI, pageNumber }) => {
|
||||
const dirResult = await computeDirectoryTree1Deep(fileService, rootURI, pageNumber)
|
||||
return { result: dirResult }
|
||||
},
|
||||
|
||||
pathname_search: async ({ queryStr, pageNumber }) => {
|
||||
get_dir_structure: async ({ rootURI }) => {
|
||||
const result = await this.directoryStrService.getDirectoryStrTool(rootURI)
|
||||
let str = result.str
|
||||
if (result.wasCutOff) str += '\n(Result was truncated)'
|
||||
return { result: { str } }
|
||||
},
|
||||
|
||||
search_pathnames_only: async ({ queryStr, include, pageNumber }) => {
|
||||
|
||||
const query = queryBuilder.file(workspaceContextService.getWorkspace().folders.map(f => f.uri), {
|
||||
filePattern: queryStr,
|
||||
includePattern: include ?? undefined,
|
||||
})
|
||||
const data = await searchService.fileSearch(query, CancellationToken.None)
|
||||
|
||||
|
|
@ -314,11 +326,15 @@ export class ToolsService implements IToolsService {
|
|||
return { result: { uris, hasNextPage } }
|
||||
},
|
||||
|
||||
grep_search: async ({ queryStr, pageNumber }) => {
|
||||
search_files: async ({ queryStr, isRegex, searchInFolder, pageNumber }) => {
|
||||
const searchFolders = searchInFolder === null ?
|
||||
workspaceContextService.getWorkspace().folders.map(f => f.uri)
|
||||
: [searchInFolder]
|
||||
|
||||
const query = queryBuilder.text({
|
||||
pattern: queryStr,
|
||||
isRegExp: true,
|
||||
}, workspaceContextService.getWorkspace().folders.map(f => f.uri))
|
||||
isRegExp: isRegex,
|
||||
}, searchFolders)
|
||||
|
||||
const data = await searchService.textSearch(query, CancellationToken.None)
|
||||
|
||||
|
|
@ -334,7 +350,7 @@ export class ToolsService implements IToolsService {
|
|||
|
||||
// ---
|
||||
|
||||
create_uri: async ({ uri, isFolder }) => {
|
||||
create_file_or_folder: async ({ uri, isFolder }) => {
|
||||
if (isFolder)
|
||||
await fileService.createFolder(uri)
|
||||
else {
|
||||
|
|
@ -343,31 +359,46 @@ export class ToolsService implements IToolsService {
|
|||
return { result: {} }
|
||||
},
|
||||
|
||||
delete_uri: async ({ uri, isRecursive }) => {
|
||||
delete_file_or_folder: async ({ uri, isRecursive }) => {
|
||||
await fileService.del(uri, { recursive: isRecursive })
|
||||
return { result: {} }
|
||||
},
|
||||
|
||||
edit: async ({ uri, changeDescription }) => {
|
||||
edit_file: async ({ uri, changeDescription }) => {
|
||||
await voidModelService.initializeModel(uri)
|
||||
if (this.commandBarService.getStreamState(uri) === 'streaming') {
|
||||
throw new Error(`The Apply model was already running. This can happen if two agents try editing the same file at the same time. Please try again in a moment.`)
|
||||
throw new Error(`Another LLM is currently making changes to this file. Please stop streaming for now and resume later.`)
|
||||
}
|
||||
const res = await editCodeService.startApplying({
|
||||
const opts = {
|
||||
uri,
|
||||
applyStr: changeDescription,
|
||||
from: 'ClickApply',
|
||||
startBehavior: 'keep-conflicts',
|
||||
})
|
||||
} as const
|
||||
|
||||
await editCodeService.callBeforeStartApplying(opts)
|
||||
const res = editCodeService.startApplying(opts)
|
||||
if (!res) throw new Error(`The Apply model did not start running on ${basename(uri.fsPath)}. Please try again.`)
|
||||
const [diffZoneURI, applyDonePromise] = res
|
||||
|
||||
const interruptTool = () => { // must reject the applyPromiseDone promise
|
||||
editCodeService.interruptURIStreaming({ uri: diffZoneURI })
|
||||
}
|
||||
return { result: applyDonePromise, interruptTool }
|
||||
|
||||
const lintErrorsPromise = applyDonePromise.then(async () => {
|
||||
await timeout(500)
|
||||
const lintErrorsStr = this.markerService
|
||||
.read({ resource: uri })
|
||||
.map(l => l.message)
|
||||
.join('\n')
|
||||
|
||||
if (!lintErrorsStr) return { lintErrorsStr: null }
|
||||
return { lintErrorsStr }
|
||||
})
|
||||
|
||||
return { result: lintErrorsPromise, interruptTool }
|
||||
},
|
||||
terminal_command: async ({ command, proposedTerminalId, waitForCompletion }) => {
|
||||
run_terminal_command: async ({ command, proposedTerminalId, waitForCompletion }) => {
|
||||
const { terminalId, didCreateTerminal, result, resolveReason } = await this.terminalToolService.runCommand(command, proposedTerminalId, waitForCompletion)
|
||||
return { result: { terminalId, didCreateTerminal, result, resolveReason } }
|
||||
},
|
||||
|
|
@ -381,29 +412,31 @@ export class ToolsService implements IToolsService {
|
|||
read_file: (params, result) => {
|
||||
return result.fileContents + nextPageStr(result.hasNextPage)
|
||||
},
|
||||
list_dir: (params, result) => {
|
||||
const dirTreeStr = directoryResultToString(params, result)
|
||||
ls_dir: (params, result) => {
|
||||
const dirTreeStr = stringifyDirectoryTree1Deep(params, result)
|
||||
return dirTreeStr // + nextPageStr(result.hasNextPage) // already handles num results remaining
|
||||
},
|
||||
pathname_search: (params, result) => {
|
||||
get_dir_structure: (params, result) => {
|
||||
return result.str
|
||||
},
|
||||
search_pathnames_only: (params, result) => {
|
||||
return result.uris.map(uri => uri.fsPath).join('\n') + nextPageStr(result.hasNextPage)
|
||||
},
|
||||
grep_search: (params, result) => {
|
||||
search_files: (params, result) => {
|
||||
return result.uris.map(uri => uri.fsPath).join('\n') + nextPageStr(result.hasNextPage)
|
||||
},
|
||||
// ---
|
||||
create_uri: (params, result) => {
|
||||
create_file_or_folder: (params, result) => {
|
||||
return `URI ${params.uri.fsPath} successfully created.`
|
||||
},
|
||||
delete_uri: (params, result) => {
|
||||
delete_file_or_folder: (params, result) => {
|
||||
return `URI ${params.uri.fsPath} successfully deleted.`
|
||||
},
|
||||
edit: (params, result) => {
|
||||
console.log('STR OF RESULT', params)
|
||||
return `Change successfully made to ${params.uri.fsPath}.`
|
||||
edit_file: (params, result) => {
|
||||
const additionalStr = result.lintErrorsStr ? `Lint errors found after change:\n${result.lintErrorsStr}.\nIf this is related to a change made while calling this tool, you might want to fix the error.` : `No lint errors found.`
|
||||
return `Change successfully made to ${params.uri.fsPath}. ${additionalStr}`
|
||||
},
|
||||
terminal_command: (params, result) => {
|
||||
|
||||
run_terminal_command: (params, result) => {
|
||||
const {
|
||||
terminalId,
|
||||
didCreateTerminal,
|
||||
|
|
|
|||
|
|
@ -173,7 +173,7 @@ export class VoidCommandBarService extends Disposable implements IVoidCommandBar
|
|||
}
|
||||
|
||||
}))
|
||||
this._register(this._editCodeService.onDidChangeDiffsInDiffZone(e => {
|
||||
this._register(this._editCodeService.onDidChangeDiffsInDiffZoneNotStreaming(e => {
|
||||
for (const uri of this._listenToTheseURIs) {
|
||||
if (e.uri.fsPath !== uri.fsPath) continue
|
||||
// --- sortedURIs: no change
|
||||
|
|
|
|||
|
|
@ -1,29 +1,57 @@
|
|||
/*--------------------------------------------------------------------------------------
|
||||
* 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 { IRange } from '../../../../editor/common/core/range.js';
|
||||
import { VoidFileSnapshot } from './editCodeServiceTypes.js';
|
||||
import { AnthropicReasoning } from './sendLLMMessageTypes.js';
|
||||
import { ToolName, ToolCallParams, ToolResultType } from './toolsServiceTypes.js';
|
||||
|
||||
export type ToolMessage<T extends ToolName> = {
|
||||
role: 'tool';
|
||||
name: T; // internal use
|
||||
paramsStr: string; // internal use
|
||||
id: string; // apis require this tool use id
|
||||
content: string; // give this result to LLM
|
||||
content: string; // give this result to LLM (string of value)
|
||||
} & (
|
||||
// in order of events:
|
||||
| { type: 'invalid_params', result: null, params: null, name: string }
|
||||
|
||||
// if rejected, don't show in chat
|
||||
result:
|
||||
| { type: 'success'; params: ToolCallParams[T]; value: ToolResultType[T], }
|
||||
| { type: 'error'; params: ToolCallParams[T] | undefined; value: string }
|
||||
| { type: 'rejected'; params: ToolCallParams[T] }
|
||||
| { type: 'tool_request', result: null, name: T, params: ToolCallParams[T], } // params were validated, awaiting user
|
||||
|
||||
| { type: 'running_now', result: null, name: T, params: ToolCallParams[T], }
|
||||
|
||||
| { type: 'tool_error', result: string, name: T, params: ToolCallParams[T], } // error when tool was running
|
||||
| { type: 'success', result: ToolResultType[T], name: T, params: ToolCallParams[T], }
|
||||
| { type: 'rejected', result: null, name: T, params: ToolCallParams[T], }
|
||||
) // user rejected
|
||||
|
||||
export type DecorativeCanceledTool = {
|
||||
role: 'decorative_canceled_tool';
|
||||
name: string;
|
||||
}
|
||||
export type ToolRequestApproval<T extends ToolName> = {
|
||||
role: 'tool_request';
|
||||
name: T; // internal use
|
||||
params: ToolCallParams[T]; // internal use
|
||||
paramsStr: string; // internal use - this is what the LLM outputted, not necessarily JSON.stringify(params)
|
||||
id: string; // proposed tool's id
|
||||
|
||||
// export type ToolRequestApproval<T extends ToolName> = {
|
||||
// role: 'tool_request';
|
||||
// name: T; // internal use
|
||||
// params: ToolCallParams[T]; // internal use
|
||||
// paramsStr: string; // internal use - this is what the LLM outputted, not necessarily JSON.stringify(params)
|
||||
// id: string; // proposed tool's id
|
||||
// }
|
||||
|
||||
|
||||
// checkpoints
|
||||
export type CheckpointEntry = {
|
||||
role: 'checkpoint';
|
||||
type: 'user_edit' | 'tool_edit';
|
||||
voidFileSnapshotOfURI: { [fsPath: string]: VoidFileSnapshot | undefined };
|
||||
|
||||
userModifications: {
|
||||
voidFileSnapshotOfURI: { [fsPath: string]: VoidFileSnapshot | undefined };
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// WARNING: changing this format is a big deal!!!!!! need to migrate old format to new format on users' computers so people don't get errors.
|
||||
export type ChatMessage =
|
||||
| {
|
||||
|
|
@ -43,38 +71,31 @@ export type ChatMessage =
|
|||
anthropicReasoning: AnthropicReasoning[] | null; // anthropic reasoning
|
||||
}
|
||||
| ToolMessage<ToolName>
|
||||
| ToolRequestApproval<ToolName>
|
||||
| DecorativeCanceledTool
|
||||
| CheckpointEntry
|
||||
|
||||
|
||||
// one of the square items that indicates a selection in a chat bubble (NOT a file, a Selection of text)
|
||||
export type CodeSelection = {
|
||||
type: 'Selection';
|
||||
fileURI: URI;
|
||||
language: string;
|
||||
selectionStr: string;
|
||||
range: IRange;
|
||||
state: {
|
||||
isOpened: boolean;
|
||||
wasAddedAsCurrentFile: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export type FileSelection = {
|
||||
// one of the square items that indicates a selection in a chat bubble
|
||||
export type StagingSelectionItem = {
|
||||
type: 'File';
|
||||
fileURI: URI;
|
||||
uri: URI;
|
||||
language: string;
|
||||
selectionStr: null;
|
||||
range: null;
|
||||
state: {
|
||||
isOpened: boolean;
|
||||
wasAddedAsCurrentFile: boolean;
|
||||
};
|
||||
state: { wasAddedAsCurrentFile: boolean; };
|
||||
} | {
|
||||
type: 'CodeSelection';
|
||||
range: [number, number];
|
||||
uri: URI;
|
||||
language: string;
|
||||
state: { wasAddedAsCurrentFile: boolean; };
|
||||
} | {
|
||||
type: 'Folder';
|
||||
uri: URI;
|
||||
language?: undefined;
|
||||
state?: undefined;
|
||||
}
|
||||
|
||||
export type StagingSelectionItem = CodeSelection | FileSelection
|
||||
|
||||
|
||||
|
||||
// a link to a symbol (an underlined link to a piece of code)
|
||||
export type CodespanLocationLink = {
|
||||
uri: URI, // we handle serialization for this
|
||||
displayText: string,
|
||||
|
|
|
|||
10
src/vs/workbench/contrib/void/common/directoryStrTypes.ts
Normal file
10
src/vs/workbench/contrib/void/common/directoryStrTypes.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { URI } from '../../../../base/common/uri.js';
|
||||
|
||||
export type VoidDirectoryItem = {
|
||||
uri: URI;
|
||||
name: string;
|
||||
isSymbolicLink: boolean;
|
||||
children: VoidDirectoryItem[] | null;
|
||||
isDirectory: boolean;
|
||||
isGitIgnoredDirectory: false | { numChildren: number }; // if directory is gitignored, we ignore children
|
||||
}
|
||||
119
src/vs/workbench/contrib/void/common/editCodeServiceTypes.ts
Normal file
119
src/vs/workbench/contrib/void/common/editCodeServiceTypes.ts
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
/*--------------------------------------------------------------------------------------
|
||||
* 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';
|
||||
|
||||
export type ComputedDiff = {
|
||||
type: 'edit';
|
||||
originalCode: string;
|
||||
originalStartLine: number;
|
||||
originalEndLine: number;
|
||||
code: string;
|
||||
startLine: number; // 1-indexed
|
||||
endLine: number;
|
||||
} | {
|
||||
type: 'insertion';
|
||||
// originalCode: string;
|
||||
originalStartLine: number; // insertion starts on column 0 of this
|
||||
// originalEndLine: number;
|
||||
code: string;
|
||||
startLine: number;
|
||||
endLine: number;
|
||||
} | {
|
||||
type: 'deletion';
|
||||
originalCode: string;
|
||||
originalStartLine: number;
|
||||
originalEndLine: number;
|
||||
// code: string;
|
||||
startLine: number; // deletion starts on column 0 of this
|
||||
// endLine: number;
|
||||
}
|
||||
|
||||
// ---------- Diff types ----------
|
||||
|
||||
export type CommonZoneProps = {
|
||||
diffareaid: number;
|
||||
startLine: number;
|
||||
endLine: number;
|
||||
|
||||
_URI: URI; // typically we get the URI from model
|
||||
|
||||
}
|
||||
|
||||
|
||||
export type CtrlKZone = {
|
||||
type: 'CtrlKZone';
|
||||
originalCode?: undefined;
|
||||
|
||||
editorId: string; // the editor the input lives on
|
||||
|
||||
// _ means anything we don't include if we clone it
|
||||
_mountInfo: null | {
|
||||
textAreaRef: { current: HTMLTextAreaElement | null }
|
||||
dispose: () => void;
|
||||
refresh: () => void;
|
||||
}
|
||||
_linkedStreamingDiffZone: number | null; // diffareaid of the diffZone currently streaming here
|
||||
_removeStylesFns: Set<Function> // these don't remove diffs or this diffArea, only their styles
|
||||
} & CommonZoneProps
|
||||
|
||||
|
||||
export type TrackingZone<T> = {
|
||||
type: 'TrackingZone';
|
||||
metadata: T;
|
||||
originalCode?: undefined;
|
||||
editorId?: undefined;
|
||||
_removeStylesFns?: undefined;
|
||||
} & CommonZoneProps
|
||||
|
||||
|
||||
// called DiffArea for historical purposes, we can rename to something like TextRegion if we want
|
||||
export type DiffArea = CtrlKZone | DiffZone | TrackingZone<any>
|
||||
|
||||
|
||||
export type Diff = {
|
||||
diffid: number;
|
||||
diffareaid: number; // the diff area this diff belongs to, "computed"
|
||||
} & ComputedDiff
|
||||
|
||||
|
||||
export type DiffZone = {
|
||||
type: 'DiffZone',
|
||||
originalCode: string;
|
||||
_diffOfId: Record<string, Diff>; // diffid -> diff in this DiffArea
|
||||
_streamState: {
|
||||
isStreaming: true;
|
||||
streamRequestIdRef: { current: string | null };
|
||||
line: number;
|
||||
} | {
|
||||
isStreaming: false;
|
||||
streamRequestIdRef?: undefined;
|
||||
line?: undefined;
|
||||
};
|
||||
editorId?: undefined;
|
||||
linkedStreamingDiffZone?: undefined;
|
||||
_removeStylesFns: Set<Function> // these don't remove diffs or this diffArea, only their styles
|
||||
} & CommonZoneProps
|
||||
|
||||
|
||||
export const diffAreaSnapshotKeys = [
|
||||
'type',
|
||||
'diffareaid',
|
||||
'originalCode',
|
||||
'startLine',
|
||||
'endLine',
|
||||
'editorId',
|
||||
|
||||
] as const satisfies (keyof DiffArea)[]
|
||||
|
||||
|
||||
|
||||
export type DiffAreaSnapshotEntry<DiffAreaType extends DiffArea = DiffArea> = Pick<DiffAreaType, typeof diffAreaSnapshotKeys[number]>
|
||||
|
||||
export type VoidFileSnapshot = {
|
||||
snapshottedDiffAreaOfId: Record<string, DiffAreaSnapshotEntry>;
|
||||
entireFileCode: string;
|
||||
}
|
||||
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ModelSelectionOptions, ProviderName } from './voidSettingsTypes.js';
|
||||
import { FeatureName, ModelSelectionOptions, ProviderName } from './voidSettingsTypes.js';
|
||||
|
||||
|
||||
export const defaultModelsOfProvider = {
|
||||
|
|
@ -25,11 +25,9 @@ export const defaultModelsOfProvider = {
|
|||
'grok-3-latest',
|
||||
],
|
||||
gemini: [ // https://ai.google.dev/gemini-api/docs/models/gemini
|
||||
'gemini-2.5-pro-exp-03-25',
|
||||
'gemini-2.0-flash',
|
||||
'gemini-1.5-flash',
|
||||
'gemini-1.5-pro',
|
||||
'gemini-1.5-flash-8b',
|
||||
'gemini-2.0-flash-thinking-exp',
|
||||
'gemini-2.0-flash-lite',
|
||||
],
|
||||
deepseek: [ // https://api-docs.deepseek.com/quick_start/pricing
|
||||
'deepseek-chat',
|
||||
|
|
@ -44,8 +42,13 @@ export const defaultModelsOfProvider = {
|
|||
'anthropic/claude-3.7-sonnet',
|
||||
'anthropic/claude-3.5-sonnet',
|
||||
'deepseek/deepseek-r1',
|
||||
'deepseek/deepseek-r1-zero:free',
|
||||
'mistralai/codestral-2501',
|
||||
'qwen/qwen-2.5-coder-32b-instruct',
|
||||
// 'mistralai/mistral-small-3.1-24b-instruct:free',
|
||||
'google/gemini-2.0-flash-lite-preview-02-05:free',
|
||||
// 'google/gemini-2.0-pro-exp-02-05:free',
|
||||
// 'google/gemini-2.0-flash-exp:free',
|
||||
],
|
||||
groq: [ // https://console.groq.com/docs/models
|
||||
'qwen-qwq-32b',
|
||||
|
|
@ -76,8 +79,8 @@ export const defaultModelsOfProvider = {
|
|||
|
||||
|
||||
type ModelOptions = {
|
||||
contextWindow: number; // input tokens // <-- UNUSED
|
||||
maxOutputTokens: number | null; // output tokens // <-- UNUSED
|
||||
contextWindow: number; // input tokens
|
||||
maxOutputTokens: number | null; // output tokens, defaults to 4092
|
||||
cost: { // <-- UNUSED
|
||||
input: number;
|
||||
output: number;
|
||||
|
|
@ -122,9 +125,9 @@ type ProviderSettings = {
|
|||
|
||||
|
||||
const modelOptionsDefaults: ModelOptions = {
|
||||
contextWindow: 32_000, // unused
|
||||
maxOutputTokens: null, // unused
|
||||
cost: { input: 0, output: 0 }, // unused
|
||||
contextWindow: 32_000,
|
||||
maxOutputTokens: 4_096,
|
||||
cost: { input: 0, output: 0 },
|
||||
supportsSystemMessage: false,
|
||||
supportsTools: false,
|
||||
supportsFIM: false,
|
||||
|
|
@ -246,43 +249,95 @@ const openSourceModelOptions_assumingOAICompat = {
|
|||
supportsSystemMessage: false,
|
||||
supportsTools: false,
|
||||
reasoningCapabilities: { supportsReasoning: true, canTurnOffReasoning: false, canIOReasoning: true, openSourceThinkTags: ['<think>', '</think>'] },
|
||||
contextWindow: 32_000, maxOutputTokens: 4_096,
|
||||
},
|
||||
'deepseekCoderV3': {
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: false, // unstable
|
||||
supportsTools: false,
|
||||
reasoningCapabilities: false,
|
||||
contextWindow: 32_000, maxOutputTokens: 4_096,
|
||||
},
|
||||
'deepseekCoderV2': {
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: false, // unstable
|
||||
supportsTools: false,
|
||||
reasoningCapabilities: false,
|
||||
contextWindow: 32_000, maxOutputTokens: 4_096,
|
||||
},
|
||||
'codestral': {
|
||||
supportsFIM: true,
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: 'openai-style',
|
||||
reasoningCapabilities: false,
|
||||
contextWindow: 32_000, maxOutputTokens: 4_096,
|
||||
},
|
||||
// llama
|
||||
'openhands-lm-32b': { // https://www.all-hands.dev/blog/introducing-openhands-lm-32b----a-strong-open-coding-agent-model
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: 'openai-style',
|
||||
reasoningCapabilities: false, // built on qwen 2.5 32B instruct
|
||||
contextWindow: 128_000, maxOutputTokens: 4_096
|
||||
},
|
||||
'phi4': {
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: false,
|
||||
reasoningCapabilities: false,
|
||||
contextWindow: 16_000, maxOutputTokens: 4_096,
|
||||
},
|
||||
|
||||
'gemma': { // https://news.ycombinator.com/item?id=43451406
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: false,
|
||||
reasoningCapabilities: false,
|
||||
contextWindow: 32_000, maxOutputTokens: 4_096,
|
||||
},
|
||||
// llama 4 https://ai.meta.com/blog/llama-4-multimodal-intelligence/
|
||||
'llama4-scout': {
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: 'openai-style',
|
||||
reasoningCapabilities: false,
|
||||
contextWindow: 10_000_000, maxOutputTokens: 4_096,
|
||||
},
|
||||
'llama4-maverick': {
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: 'openai-style',
|
||||
reasoningCapabilities: false,
|
||||
contextWindow: 10_000_000, maxOutputTokens: 4_096,
|
||||
},
|
||||
|
||||
// llama 3
|
||||
'llama3': {
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: 'openai-style',
|
||||
reasoningCapabilities: false,
|
||||
contextWindow: 32_000, maxOutputTokens: 4_096,
|
||||
},
|
||||
'llama3.1': {
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: 'openai-style',
|
||||
reasoningCapabilities: false,
|
||||
contextWindow: 32_000, maxOutputTokens: 4_096,
|
||||
},
|
||||
'llama3.2': {
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: 'openai-style',
|
||||
reasoningCapabilities: false,
|
||||
contextWindow: 32_000, maxOutputTokens: 4_096,
|
||||
},
|
||||
'llama3.3': {
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: 'openai-style',
|
||||
reasoningCapabilities: false,
|
||||
contextWindow: 32_000, maxOutputTokens: 4_096,
|
||||
},
|
||||
// qwen
|
||||
'qwen2.5coder': {
|
||||
|
|
@ -290,12 +345,14 @@ const openSourceModelOptions_assumingOAICompat = {
|
|||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: 'openai-style',
|
||||
reasoningCapabilities: false,
|
||||
contextWindow: 32_000, maxOutputTokens: 4_096,
|
||||
},
|
||||
'qwq': {
|
||||
supportsFIM: false, // no FIM, yes reasoning
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: 'openai-style',
|
||||
reasoningCapabilities: { supportsReasoning: true, canTurnOffReasoning: false, canIOReasoning: true, openSourceThinkTags: ['<think>', '</think>'] },
|
||||
contextWindow: 128_000, maxOutputTokens: 8_192,
|
||||
},
|
||||
// FIM only
|
||||
'starcoder2': {
|
||||
|
|
@ -303,19 +360,26 @@ const openSourceModelOptions_assumingOAICompat = {
|
|||
supportsSystemMessage: false,
|
||||
supportsTools: false,
|
||||
reasoningCapabilities: false,
|
||||
contextWindow: 128_000, maxOutputTokens: 8_192,
|
||||
|
||||
},
|
||||
'codegemma:2b': {
|
||||
supportsFIM: true,
|
||||
supportsSystemMessage: false,
|
||||
supportsTools: false,
|
||||
reasoningCapabilities: false,
|
||||
contextWindow: 128_000, maxOutputTokens: 8_192,
|
||||
|
||||
},
|
||||
} as const satisfies { [s: string]: Partial<ModelOptions> }
|
||||
} as const satisfies { [s: string]: Omit<ModelOptions, 'cost'> }
|
||||
|
||||
|
||||
|
||||
|
||||
const extensiveModelFallback: ProviderSettings['modelOptionsFallback'] = (modelName) => {
|
||||
|
||||
const lower = modelName.toLowerCase()
|
||||
|
||||
const toFallback = (opts: Omit<ModelOptions, 'cost'>): ModelOptions & { modelName: string } => {
|
||||
return {
|
||||
modelName,
|
||||
|
|
@ -324,16 +388,46 @@ const extensiveModelFallback: ProviderSettings['modelOptionsFallback'] = (modelN
|
|||
cost: { input: 0, output: 0 },
|
||||
}
|
||||
}
|
||||
if (modelName.includes('gpt-4o')) return toFallback(openAIModelOptions['gpt-4o'])
|
||||
if (modelName.includes('claude-3-5') || modelName.includes('claude-3.5')) return toFallback(anthropicModelOptions['claude-3-5-sonnet-20241022'])
|
||||
if (modelName.includes('claude')) return toFallback(anthropicModelOptions['claude-3-7-sonnet-20250219'])
|
||||
if (modelName.includes('grok')) return toFallback(xAIModelOptions['grok-2'])
|
||||
if (modelName.includes('deepseek-r1') || modelName.includes('deepseek-reasoner')) return toFallback({ ...openSourceModelOptions_assumingOAICompat.deepseekR1, contextWindow: 32_000, maxOutputTokens: 4_096, })
|
||||
if (modelName.includes('deepseek')) return toFallback({ ...openSourceModelOptions_assumingOAICompat.deepseekCoderV2, contextWindow: 32_000, maxOutputTokens: 4_096, })
|
||||
if (modelName.includes('llama3')) return toFallback({ ...openSourceModelOptions_assumingOAICompat.llama3, contextWindow: 32_000, maxOutputTokens: 4_096, })
|
||||
if (modelName.includes('qwen') && modelName.includes('2.5') && modelName.includes('coder')) return toFallback({ ...openSourceModelOptions_assumingOAICompat['qwen2.5coder'], contextWindow: 32_000, maxOutputTokens: 4_096, })
|
||||
if (modelName.includes('codestral')) return toFallback({ ...mistralModelOptions['codestral-latest'], contextWindow: 32_000, maxOutputTokens: 4_096, })
|
||||
if (/\bo1\b/.test(modelName) || /\bo3\b/.test(modelName)) return toFallback(openAIModelOptions['o1'])
|
||||
if (Object.keys(openSourceModelOptions_assumingOAICompat).map(k => k.toLowerCase()).includes(lower))
|
||||
return toFallback(openSourceModelOptions_assumingOAICompat[lower as keyof typeof openSourceModelOptions_assumingOAICompat])
|
||||
|
||||
if (lower.includes('gemini') && (lower.includes('2.5') || lower.includes('2-5'))) return toFallback(geminiModelOptions['gemini-2.5-pro-exp-03-25'])
|
||||
|
||||
if (lower.includes('claude-3-5') || lower.includes('claude-3.5')) return toFallback(anthropicModelOptions['claude-3-5-sonnet-20241022'])
|
||||
if (lower.includes('claude')) return toFallback(anthropicModelOptions['claude-3-7-sonnet-20250219'])
|
||||
|
||||
if (lower.includes('grok')) return toFallback(xAIModelOptions['grok-2'])
|
||||
|
||||
if (lower.includes('deepseek-r1') || lower.includes('deepseek-reasoner')) return toFallback({ ...openSourceModelOptions_assumingOAICompat.deepseekR1 })
|
||||
if (lower.includes('deepseek') && lower.includes('v2')) return toFallback({ ...openSourceModelOptions_assumingOAICompat.deepseekCoderV2 })
|
||||
if (lower.includes('deepseek')) return toFallback({ ...openSourceModelOptions_assumingOAICompat.deepseekCoderV3 })
|
||||
|
||||
if (lower.includes('llama3')) return toFallback({ ...openSourceModelOptions_assumingOAICompat.llama3, })
|
||||
if (lower.includes('llama3.1')) return toFallback({ ...openSourceModelOptions_assumingOAICompat['llama3.1'], })
|
||||
if (lower.includes('llama3.2')) return toFallback({ ...openSourceModelOptions_assumingOAICompat['llama3.2'], })
|
||||
if (lower.includes('llama3.3')) return toFallback({ ...openSourceModelOptions_assumingOAICompat['llama3.3'], })
|
||||
if (lower.includes('llama') || lower.includes('scout')) return toFallback({ ...openSourceModelOptions_assumingOAICompat['llama4-scout'] })
|
||||
if (lower.includes('llama') || lower.includes('maverick')) return toFallback({ ...openSourceModelOptions_assumingOAICompat['llama4-scout'] })
|
||||
if (lower.includes('llama')) return toFallback({ ...openSourceModelOptions_assumingOAICompat['llama4-scout'] })
|
||||
|
||||
if (lower.includes('qwen') && lower.includes('2.5') && lower.includes('coder')) return toFallback({ ...openSourceModelOptions_assumingOAICompat['qwen2.5coder'] })
|
||||
if (lower.includes('qwq')) { return toFallback({ ...openSourceModelOptions_assumingOAICompat.qwq, }) }
|
||||
if (lower.includes('phi4')) return toFallback({ ...openSourceModelOptions_assumingOAICompat.phi4, })
|
||||
if (lower.includes('codestral')) return toFallback({ ...openSourceModelOptions_assumingOAICompat.codestral })
|
||||
|
||||
if (lower.includes('gemma')) return toFallback({ ...openSourceModelOptions_assumingOAICompat.gemma, })
|
||||
|
||||
if (lower.includes('starcoder2')) return toFallback({ ...openSourceModelOptions_assumingOAICompat.starcoder2, })
|
||||
|
||||
if (lower.includes('openhands')) return toFallback({ ...openSourceModelOptions_assumingOAICompat['openhands-lm-32b'], }) // max output unclear
|
||||
|
||||
if (lower.includes('4o') && lower.includes('mini')) return toFallback(openAIModelOptions['gpt-4o-mini'])
|
||||
if (lower.includes('4o')) return toFallback(openAIModelOptions['gpt-4o'])
|
||||
if (lower.includes('o1') && lower.includes('mini')) return toFallback(openAIModelOptions['o1-mini'])
|
||||
if (lower.includes('o1')) return toFallback(openAIModelOptions['o1'])
|
||||
if (lower.includes('o3') && lower.includes('mini')) return toFallback(openAIModelOptions['o3-mini'])
|
||||
// if (lower.includes('o3')) return toFallback(openAIModelOptions['o3'])
|
||||
|
||||
return toFallback(modelOptionsDefaults)
|
||||
}
|
||||
|
||||
|
|
@ -409,12 +503,13 @@ const anthropicSettings: ProviderSettings = {
|
|||
},
|
||||
modelOptions: anthropicModelOptions,
|
||||
modelOptionsFallback: (modelName) => {
|
||||
const lower = modelName.toLowerCase()
|
||||
let fallbackName: keyof typeof anthropicModelOptions | null = null
|
||||
if (modelName.includes('claude-3-7-sonnet')) fallbackName = 'claude-3-7-sonnet-20250219'
|
||||
if (modelName.includes('claude-3-5-sonnet')) fallbackName = 'claude-3-5-sonnet-20241022'
|
||||
if (modelName.includes('claude-3-5-haiku')) fallbackName = 'claude-3-5-haiku-20241022'
|
||||
if (modelName.includes('claude-3-opus')) fallbackName = 'claude-3-opus-20240229'
|
||||
if (modelName.includes('claude-3-sonnet')) fallbackName = 'claude-3-sonnet-20240229'
|
||||
if (lower.includes('claude-3-7-sonnet')) fallbackName = 'claude-3-7-sonnet-20250219'
|
||||
if (lower.includes('claude-3-5-sonnet')) fallbackName = 'claude-3-5-sonnet-20241022'
|
||||
if (lower.includes('claude-3-5-haiku')) fallbackName = 'claude-3-5-haiku-20241022'
|
||||
if (lower.includes('claude-3-opus')) fallbackName = 'claude-3-opus-20240229'
|
||||
if (lower.includes('claude-3-sonnet')) fallbackName = 'claude-3-sonnet-20240229'
|
||||
if (fallbackName) return { modelName: fallbackName, ...anthropicModelOptions[fallbackName] }
|
||||
return { modelName, ...modelOptionsDefaults, maxOutputTokens: 4_096 }
|
||||
},
|
||||
|
|
@ -474,10 +569,11 @@ const openAIModelOptions = { // https://platform.openai.com/docs/pricing
|
|||
const openAISettings: ProviderSettings = {
|
||||
modelOptions: openAIModelOptions,
|
||||
modelOptionsFallback: (modelName) => {
|
||||
const lower = modelName.toLowerCase()
|
||||
let fallbackName: keyof typeof openAIModelOptions | null = null
|
||||
if (modelName.includes('o1')) { fallbackName = 'o1' }
|
||||
if (modelName.includes('o3-mini')) { fallbackName = 'o3-mini' }
|
||||
if (modelName.includes('gpt-4o')) { fallbackName = 'gpt-4o' }
|
||||
if (lower.includes('o1')) { fallbackName = 'o1' }
|
||||
if (lower.includes('o3-mini')) { fallbackName = 'o3-mini' }
|
||||
if (lower.includes('gpt-4o')) { fallbackName = 'gpt-4o' }
|
||||
if (fallbackName) return { modelName: fallbackName, ...openAIModelOptions[fallbackName] }
|
||||
return null
|
||||
}
|
||||
|
|
@ -499,8 +595,9 @@ const xAIModelOptions = {
|
|||
const xAISettings: ProviderSettings = {
|
||||
modelOptions: xAIModelOptions,
|
||||
modelOptionsFallback: (modelName) => {
|
||||
const lower = modelName.toLowerCase()
|
||||
let fallbackName: keyof typeof xAIModelOptions | null = null
|
||||
if (modelName.includes('grok-2')) fallbackName = 'grok-2'
|
||||
if (lower.includes('grok-2')) fallbackName = 'grok-2'
|
||||
if (fallbackName) return { modelName: fallbackName, ...xAIModelOptions[fallbackName] }
|
||||
return null
|
||||
}
|
||||
|
|
@ -509,9 +606,18 @@ const xAISettings: ProviderSettings = {
|
|||
|
||||
// ---------------- GEMINI ----------------
|
||||
const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing
|
||||
'gemini-2.5-pro-exp-03-25': {
|
||||
contextWindow: 1_048_576,
|
||||
maxOutputTokens: 8_192,
|
||||
cost: { input: 0, output: 0 },
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: 'openai-style', // we are assuming OpenAI SDK when calling gemini
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
'gemini-2.0-flash': {
|
||||
contextWindow: 1_048_576,
|
||||
maxOutputTokens: null, // 8_192,
|
||||
maxOutputTokens: 8_192, // 8_192,
|
||||
cost: { input: 0.10, output: 0.40 },
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
|
|
@ -520,7 +626,7 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing
|
|||
},
|
||||
'gemini-2.0-flash-lite-preview-02-05': {
|
||||
contextWindow: 1_048_576,
|
||||
maxOutputTokens: null, // 8_192,
|
||||
maxOutputTokens: 8_192, // 8_192,
|
||||
cost: { input: 0.075, output: 0.30 },
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
|
|
@ -529,7 +635,7 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing
|
|||
},
|
||||
'gemini-1.5-flash': {
|
||||
contextWindow: 1_048_576,
|
||||
maxOutputTokens: null, // 8_192,
|
||||
maxOutputTokens: 8_192, // 8_192,
|
||||
cost: { input: 0.075, output: 0.30 }, // TODO!!! price doubles after 128K tokens, we are NOT encoding that info right now
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
|
|
@ -538,7 +644,7 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing
|
|||
},
|
||||
'gemini-1.5-pro': {
|
||||
contextWindow: 2_097_152,
|
||||
maxOutputTokens: null, // 8_192,
|
||||
maxOutputTokens: 8_192,
|
||||
cost: { input: 1.25, output: 5.00 }, // TODO!!! price doubles after 128K tokens, we are NOT encoding that info right now
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
|
|
@ -547,7 +653,7 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing
|
|||
},
|
||||
'gemini-1.5-flash-8b': {
|
||||
contextWindow: 1_048_576,
|
||||
maxOutputTokens: null, // 8_192,
|
||||
maxOutputTokens: 8_192,
|
||||
cost: { input: 0.0375, output: 0.15 }, // TODO!!! price doubles after 128K tokens, we are NOT encoding that info right now
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
|
|
@ -568,13 +674,13 @@ const deepseekModelOptions = {
|
|||
'deepseek-chat': {
|
||||
...openSourceModelOptions_assumingOAICompat.deepseekR1,
|
||||
contextWindow: 64_000, // https://api-docs.deepseek.com/quick_start/pricing
|
||||
maxOutputTokens: null, // 8_000,
|
||||
maxOutputTokens: 8_000, // 8_000,
|
||||
cost: { cache_read: .07, input: .27, output: 1.10, },
|
||||
},
|
||||
'deepseek-reasoner': {
|
||||
...openSourceModelOptions_assumingOAICompat.deepseekCoderV2,
|
||||
contextWindow: 64_000,
|
||||
maxOutputTokens: null, // 8_000,
|
||||
maxOutputTokens: 8_000, // 8_000,
|
||||
cost: { cache_read: .14, input: .55, output: 2.19, },
|
||||
},
|
||||
} as const satisfies { [s: string]: ModelOptions }
|
||||
|
|
@ -593,7 +699,7 @@ const deepseekSettings: ProviderSettings = {
|
|||
const groqModelOptions = { // https://console.groq.com/docs/models, https://groq.com/pricing/
|
||||
'llama-3.3-70b-versatile': {
|
||||
contextWindow: 128_000,
|
||||
maxOutputTokens: null, // 32_768,
|
||||
maxOutputTokens: 32_768, // 32_768,
|
||||
cost: { input: 0.59, output: 0.79 },
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
|
|
@ -602,7 +708,7 @@ const groqModelOptions = { // https://console.groq.com/docs/models, https://groq
|
|||
},
|
||||
'llama-3.1-8b-instant': {
|
||||
contextWindow: 128_000,
|
||||
maxOutputTokens: null, // 8_192,
|
||||
maxOutputTokens: 8_192,
|
||||
cost: { input: 0.05, output: 0.08 },
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
|
|
@ -669,6 +775,42 @@ const openaiCompatible: ProviderSettings = {
|
|||
|
||||
// ---------------- OPENROUTER ----------------
|
||||
const openRouterModelOptions_assumingOpenAICompat = {
|
||||
'mistralai/mistral-small-3.1-24b-instruct:free': {
|
||||
contextWindow: 128_000,
|
||||
maxOutputTokens: null,
|
||||
cost: { input: 0, output: 0 },
|
||||
supportsFIM: false,
|
||||
supportsTools: 'openai-style',
|
||||
supportsSystemMessage: 'system-role',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
'google/gemini-2.0-flash-lite-preview-02-05:free': {
|
||||
contextWindow: 1_048_576,
|
||||
maxOutputTokens: null,
|
||||
cost: { input: 0, output: 0 },
|
||||
supportsFIM: false,
|
||||
supportsTools: 'openai-style',
|
||||
supportsSystemMessage: 'system-role',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
'google/gemini-2.0-pro-exp-02-05:free': {
|
||||
contextWindow: 1_048_576,
|
||||
maxOutputTokens: null,
|
||||
cost: { input: 0, output: 0 },
|
||||
supportsFIM: false,
|
||||
supportsTools: 'openai-style',
|
||||
supportsSystemMessage: 'system-role',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
'google/gemini-2.0-flash-exp:free': {
|
||||
contextWindow: 1_048_576,
|
||||
maxOutputTokens: null,
|
||||
cost: { input: 0, output: 0 },
|
||||
supportsFIM: false,
|
||||
supportsTools: 'openai-style',
|
||||
supportsSystemMessage: 'system-role',
|
||||
reasoningCapabilities: false,
|
||||
},
|
||||
'deepseek/deepseek-r1': {
|
||||
...openSourceModelOptions_assumingOAICompat.deepseekR1,
|
||||
contextWindow: 128_000,
|
||||
|
|
@ -777,6 +919,7 @@ const modelSettingsOfProvider: { [providerName in ProviderName]: ProviderSetting
|
|||
mistral: mistralSettings,
|
||||
// googleVertex: {},
|
||||
// microsoftAzure: {},
|
||||
// openHands: {},
|
||||
} as const
|
||||
|
||||
|
||||
|
|
@ -784,8 +927,16 @@ const modelSettingsOfProvider: { [providerName in ProviderName]: ProviderSetting
|
|||
|
||||
// returns the capabilities and the adjusted modelName if it was a fallback
|
||||
export const getModelCapabilities = (providerName: ProviderName, modelName: string): ModelOptions & { modelName: string; isUnrecognizedModel: boolean } => {
|
||||
const lowercaseModelName = modelName.toLowerCase()
|
||||
const { modelOptions, modelOptionsFallback } = modelSettingsOfProvider[providerName]
|
||||
if (modelName in modelOptions) return { modelName, ...modelOptions[modelName], isUnrecognizedModel: false }
|
||||
|
||||
// search model options object directly first
|
||||
for (const modelName_ in modelOptions) {
|
||||
const lowercaseModelName_ = modelName_.toLowerCase()
|
||||
if (lowercaseModelName === lowercaseModelName_)
|
||||
return { modelName, ...modelOptions[modelName], isUnrecognizedModel: false }
|
||||
}
|
||||
|
||||
const result = modelOptionsFallback(modelName)
|
||||
if (result) return { ...result, isUnrecognizedModel: false }
|
||||
return { modelName, ...modelOptionsDefaults, isUnrecognizedModel: true }
|
||||
|
|
@ -806,15 +957,18 @@ export type SendableReasoningInfo = {
|
|||
|
||||
|
||||
|
||||
export const getIsResoningEnabledState = (
|
||||
export const getIsReasoningEnabledState = (
|
||||
featureName: FeatureName,
|
||||
providerName: ProviderName,
|
||||
modelName: string,
|
||||
modelSelectionOptions: ModelSelectionOptions | undefined,
|
||||
) => {
|
||||
const { supportsReasoning } = getModelCapabilities(providerName, modelName).reasoningCapabilities || {}
|
||||
const { supportsReasoning, canTurnOffReasoning } = getModelCapabilities(providerName, modelName).reasoningCapabilities || {}
|
||||
if (!supportsReasoning) return false
|
||||
|
||||
const defaultEnabledVal = true // if can't toggle reasoning, then this must be true. just true as default
|
||||
// default to enabled if can't turn off, or if the featureName is Chat.
|
||||
const defaultEnabledVal = featureName === 'Chat' || !canTurnOffReasoning
|
||||
|
||||
const isReasoningEnabled = modelSelectionOptions?.reasoningEnabled ?? defaultEnabledVal
|
||||
return isReasoningEnabled
|
||||
}
|
||||
|
|
@ -822,6 +976,7 @@ export const getIsResoningEnabledState = (
|
|||
|
||||
// used to force reasoning state (complex) into something simple we can just read from when sending a message
|
||||
export const getSendableReasoningInfo = (
|
||||
featureName: FeatureName,
|
||||
providerName: ProviderName,
|
||||
modelName: string,
|
||||
modelSelectionOptions: ModelSelectionOptions | undefined,
|
||||
|
|
@ -829,7 +984,7 @@ export const getSendableReasoningInfo = (
|
|||
|
||||
const { canIOReasoning, reasoningBudgetSlider } = getModelCapabilities(providerName, modelName).reasoningCapabilities || {}
|
||||
if (!canIOReasoning) return null
|
||||
const isReasoningEnabled = getIsResoningEnabledState(providerName, modelName, modelSelectionOptions)
|
||||
const isReasoningEnabled = getIsReasoningEnabledState(featureName, providerName, modelName, modelSelectionOptions)
|
||||
if (!isReasoningEnabled) return null
|
||||
|
||||
// check for reasoning budget
|
||||
|
|
|
|||
|
|
@ -3,15 +3,12 @@
|
|||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
import { URI } from '../../../../../base/common/uri.js';
|
||||
import { os } from '../helpers/systemInfo.js';
|
||||
import { CodeSelection, FileSelection, StagingSelectionItem } from '../chatThreadServiceTypes.js';
|
||||
import { StagingSelectionItem } from '../chatThreadServiceTypes.js';
|
||||
import { ChatMode } from '../voidSettingsTypes.js';
|
||||
import { InternalToolInfo } from '../toolsServiceTypes.js';
|
||||
import { IVoidModelService } from '../voidModelService.js';
|
||||
import { EndOfLinePreference } from '../../../../../editor/common/model.js';
|
||||
import { InternalToolInfo } from '../toolsServiceTypes.js';
|
||||
|
||||
|
||||
// this is just for ease of readability
|
||||
export const tripleTick = ['```', '```']
|
||||
|
|
@ -42,10 +39,21 @@ ${tripleTick[1]}`
|
|||
// ======================================================== tools ========================================================
|
||||
|
||||
const paginationHelper = {
|
||||
desc: `Very large results may be paginated (indicated in the result). Pagination fails gracefully if out of bounds or invalid page number.`,
|
||||
desc: `Very large results may be paginated (a note will always be included if pagination took place). Pagination fails gracefully if out of bounds or invalid page number.`,
|
||||
param: { pageNumber: { type: 'number', description: 'The page number (default is the first page = 1).' }, }
|
||||
} as const
|
||||
|
||||
const uriParam = (object: string) => ({
|
||||
uri: { type: 'string', description: `The FULL path to the ${object}.` }
|
||||
})
|
||||
|
||||
|
||||
const searchParams = {
|
||||
searchInFolder: { type: 'string', description: 'Only search files in this given folder. Leave as empty to search all available files.' },
|
||||
isRegex: { type: 'string', description: 'Whether to treat the query as a regular expression. Default is "false".' },
|
||||
} as const
|
||||
|
||||
|
||||
export const voidTools = {
|
||||
// --- context-gathering (read/search/list) ---
|
||||
|
||||
|
|
@ -53,62 +61,74 @@ export const voidTools = {
|
|||
name: 'read_file',
|
||||
description: `Returns file contents of a given URI. ${paginationHelper.desc}`,
|
||||
params: {
|
||||
uri: { type: 'string', description: undefined },
|
||||
...uriParam('file'),
|
||||
startLine: { type: 'string', description: 'Line to start reading from. Default is "null", treated as 1.' },
|
||||
endLine: { type: 'string', description: 'Line to stop reading from (inclusive). Default is "null", treated as Infinity.' },
|
||||
...paginationHelper.param,
|
||||
},
|
||||
},
|
||||
|
||||
list_dir: {
|
||||
name: 'list_dir',
|
||||
description: `Returns all file names and folder names in a given URI. ${paginationHelper.desc}`,
|
||||
ls_dir: {
|
||||
name: 'ls_dir',
|
||||
description: `Returns all file names and folder names in a given folder. ${paginationHelper.desc}`,
|
||||
params: {
|
||||
uri: { type: 'string', description: undefined },
|
||||
...uriParam('folder'),
|
||||
...paginationHelper.param,
|
||||
},
|
||||
},
|
||||
|
||||
pathname_search: {
|
||||
name: 'pathname_search',
|
||||
description: `Returns all pathnames that match a given \`find\`-style query (searches ONLY file names). You should use this when looking for a file with a specific name or path. ${paginationHelper.desc}`,
|
||||
get_dir_structure: {
|
||||
name: 'get_dir_structure',
|
||||
description: `This is a very effective way to learn about the user's codebase. You might want to use this instead of ls_dir. Returns a tree diagram of all the files and folders in the given folder URI. If results are large, the given string will be truncated (this will be indicated), in which case you might want to call this tool on a lower folder to get better results, or just use ls_dir which supports pagination.`,
|
||||
params: {
|
||||
...uriParam('folder')
|
||||
}
|
||||
},
|
||||
|
||||
search_pathnames_only: {
|
||||
name: 'search_pathnames_only',
|
||||
description: `Returns all pathnames that match a given query (searches ONLY file names). You should use this when looking for a file with a specific name or path. ${paginationHelper.desc}`,
|
||||
params: {
|
||||
query: { type: 'string', description: undefined },
|
||||
...searchParams,
|
||||
...paginationHelper.param,
|
||||
},
|
||||
},
|
||||
|
||||
grep_search: {
|
||||
name: 'grep_search',
|
||||
search_files: {
|
||||
name: 'search_files',
|
||||
description: `Returns all pathnames that match a given \`grep\`-style query (searches ONLY file contents). The query can be any regex. This is often followed by the \`read_file\` tool to view the full file contents of results. ${paginationHelper.desc}`,
|
||||
params: {
|
||||
query: { type: 'string', description: undefined },
|
||||
...searchParams,
|
||||
...paginationHelper.param,
|
||||
},
|
||||
},
|
||||
|
||||
// --- editing (create/delete) ---
|
||||
|
||||
create_uri: {
|
||||
name: 'create_uri',
|
||||
create_file_or_folder: {
|
||||
name: 'create_file_or_folder',
|
||||
description: `Create a file or folder at the given path. To create a folder, ensure the path ends with a trailing slash. Fails gracefully if the file already exists. Missing ancestors in the path will be recursively created automatically.`,
|
||||
params: {
|
||||
uri: { type: 'string', description: undefined },
|
||||
...uriParam('file or folder'),
|
||||
},
|
||||
},
|
||||
|
||||
delete_uri: {
|
||||
name: 'delete_uri',
|
||||
delete_file_or_folder: {
|
||||
name: 'delete_file_or_folder',
|
||||
description: `Delete a file or folder at the given path. Fails gracefully if the file or folder does not exist.`,
|
||||
params: {
|
||||
uri: { type: 'string', description: undefined },
|
||||
params: { type: 'string', description: 'Return -r here to delete this URI and all descendants (if applicable). Default is the empty string.' }
|
||||
...uriParam('file or folder'),
|
||||
params: { type: 'string', description: 'Return -r here to delete recursively (if applicable). Default is the empty string.' }
|
||||
},
|
||||
},
|
||||
|
||||
edit: { // APPLY TOOL
|
||||
name: 'edit',
|
||||
edit_file: { // APPLY TOOL
|
||||
name: 'edit_file',
|
||||
description: `Edits the contents of a file, given the file's URI and a description. Fails gracefully if the file does not exist.`,
|
||||
params: {
|
||||
uri: { type: 'string', description: undefined },
|
||||
...uriParam('file'),
|
||||
changeDescription: {
|
||||
type: 'string', description: `\
|
||||
- Your changeDescription should be a brief code description of the change you want to make, with comments like "// ... existing code ..." to condense your writing.
|
||||
|
|
@ -120,11 +140,11 @@ Here's an example of a good description:\n${editToolDescription}.`
|
|||
},
|
||||
},
|
||||
|
||||
terminal_command: {
|
||||
name: 'terminal_command',
|
||||
run_terminal_command: {
|
||||
name: 'run_terminal_command',
|
||||
description: `Executes a terminal command.`,
|
||||
params: {
|
||||
command: { type: 'string', description: 'The terminal command to execute.' },
|
||||
command: { type: 'string', description: 'The terminal command to execute. Typically you should pipe to cat to avoid pagination.' },
|
||||
waitForCompletion: { type: 'string', description: `Whether or not to await the command to complete and get the final result. Default is true. Make this value false when you want a command to run indefinitely without waiting for it.` },
|
||||
terminalId: { type: 'string', description: 'Optional (value must be an integer >= 1, or empty which will go with the default). This is the ID of the terminal instance to execute the command in. The primary purpose of this is to start a new terminal for background processes or tasks that run indefinitely (e.g. if you want to run a server locally). Fails gracefully if a terminal ID does not exist, by creating a new terminal instance. Defaults to the preferred terminal ID.' },
|
||||
},
|
||||
|
|
@ -144,8 +164,8 @@ Here's an example of a good description:\n${editToolDescription}.`
|
|||
|
||||
|
||||
|
||||
export const chat_systemMessage = (workspaces: string[], runningTerminalIds: string[], mode: ChatMode) => `\
|
||||
You are an expert coding ${mode === 'agent' ? 'agent' : 'assistant'} that runs in the Void code editor. Your job is \
|
||||
export const chat_systemMessage = ({ workspaceFolders, openedURIs, activeURI, runningTerminalIds, directoryStr, chatMode: mode }: { workspaceFolders: string[], directoryStr: string, openedURIs: string[], activeURI: string | undefined, runningTerminalIds: string[], chatMode: ChatMode }) => `\
|
||||
You are an expert coding ${mode === 'agent' ? 'agent' : 'assistant'} that runs in the user's IDE called Void. Your job is \
|
||||
${mode === 'agent' ? `to help the user develop, run, deploy, and make changes to their codebase. You should ALWAYS bring user's task to completion to the fullest extent possible, calling tools to make all necessary changes.`
|
||||
: mode === 'gather' ? `to search and understand the user's codebase. You MUST use tools to read files and help the user understand the codebase, even if you were initially given files.`
|
||||
: mode === 'normal' ? `to assist the user with their coding tasks.`
|
||||
|
|
@ -155,10 +175,12 @@ Please assist the user with their query. The user's query is never invalid.
|
|||
${/* system info */''}
|
||||
The user's system information is as follows:
|
||||
- ${os}
|
||||
- Open workspace(s): ${workspaces.join(', ') || 'NO WORKSPACE OPEN'}
|
||||
${(mode === 'agent') && runningTerminalIds.length !== 0 ? `\
|
||||
- Existing terminal IDs: ${runningTerminalIds.join(', ')}
|
||||
`: '\n'}
|
||||
- Open workspace(s): ${workspaceFolders.join(', ') || 'NO WORKSPACE OPEN'}
|
||||
- Open tab(s): ${openedURIs.join(', ') || 'NO OPENED EDITORS'}
|
||||
- Active tab: ${activeURI}
|
||||
${(mode === 'agent') && runningTerminalIds.length !== 0 ? `
|
||||
- Existing terminal IDs: ${runningTerminalIds.join(', ')}` : ''}
|
||||
|
||||
${/* tool use */ mode === 'agent' || mode === 'gather' ? `\
|
||||
You will be given tools you can call.
|
||||
${mode === 'agent' ? `\
|
||||
|
|
@ -189,109 +211,134 @@ If you think it's appropriate to suggest an edit to a file, then you must descri
|
|||
- The remaining contents should be a brief code description of the change you want to make, with comments like "// ... existing code ..." to condense your writing.
|
||||
- NEVER re-write the whole file, and ALWAYS use comments like "// ... existing code ...". Bias towards writing as little as possible.
|
||||
- Your description will be handed to a dumber, faster model that will quickly apply the change, so it should be clear and concise.
|
||||
Here's an example of a good code block:\n${fileNameEdit}.\
|
||||
Here's an example of a good code block:\n${fileNameEdit}.
|
||||
|
||||
If you write a code block that's related to a specific file, please use the same format as above:
|
||||
- The first line of the code block must be the FULL PATH of the related file if known.
|
||||
- The remaining contents of the file should proceed as usual.
|
||||
\
|
||||
`}
|
||||
${/* misc */''}
|
||||
Misc:
|
||||
- Do not make things up.
|
||||
- Do not be lazy.
|
||||
- NEVER re-write the entire file.
|
||||
- Always wrap any code you produce in triple backticks, and specify a language if possible. For example, ${tripleTick[0]}typescript\n...\n${tripleTick[1]}.\
|
||||
- Always wrap any code you produce in triple backticks, and specify a language if possible. For example, ${tripleTick[0]}typescript\n...\n${tripleTick[1]}.
|
||||
- Today's date is ${new Date().toDateString()}
|
||||
The user's codebase is structured as follows:\n${directoryStr}
|
||||
\
|
||||
`
|
||||
// agent mode doesn't know about 1st line paths yet
|
||||
// - If you wrote triple ticks and ___, then include the file's full path in the first line of the triple ticks. This is only for display purposes to the user, and it's preferred but optional. Never do this in a tool parameter, or if there's ambiguity about the full path.
|
||||
|
||||
|
||||
type FileSelnLocal = { fileURI: URI, language: string, content: string }
|
||||
const stringifyFileSelection = ({ fileURI, language, content }: FileSelnLocal) => {
|
||||
return `\
|
||||
${fileURI.fsPath}
|
||||
${tripleTick[0]}${language}
|
||||
${content}
|
||||
${tripleTick[1]}
|
||||
`
|
||||
}
|
||||
const stringifyCodeSelection = ({ fileURI, language, selectionStr, range }: CodeSelection) => {
|
||||
return `\
|
||||
${fileURI.fsPath} (lines ${range.startLineNumber}:${range.endLineNumber})
|
||||
${tripleTick[0]}${language}
|
||||
${selectionStr}
|
||||
${tripleTick[1]}
|
||||
`
|
||||
}
|
||||
// type FileSelnLocal = { fileURI: URI, language: string, content: string }
|
||||
// const stringifyFileSelection = ({ fileURI, language, content }: FileSelnLocal) => {
|
||||
// return `\
|
||||
// ${fileURI.fsPath}
|
||||
// ${tripleTick[0]}${language}
|
||||
// ${content}
|
||||
// ${tripleTick[1]}
|
||||
// `
|
||||
// }
|
||||
// const stringifyCodeSelection = ({ uri, language, range }: StagingSelectionItem & { type: 'CodeSelection' }) => {
|
||||
// return `\
|
||||
|
||||
const failToReadStr = 'Could not read content. This file may have been deleted. If you expected content here, you can tell the user about this as they might not know.'
|
||||
const stringifyFileSelections = async (fileSelections: FileSelection[], voidModelService: IVoidModelService) => {
|
||||
if (fileSelections.length === 0) return null
|
||||
const fileSlns: FileSelnLocal[] = await Promise.all(fileSelections.map(async (sel) => {
|
||||
const { model } = await voidModelService.getModelSafe(sel.fileURI)
|
||||
const content = model?.getValue(EndOfLinePreference.LF) ?? failToReadStr
|
||||
return { ...sel, content }
|
||||
}))
|
||||
return fileSlns.map(sel => stringifyFileSelection(sel)).join('\n')
|
||||
}
|
||||
// ${tripleTick[0]}${language}
|
||||
// ${selectionStr}
|
||||
// ${tripleTick[1]}
|
||||
// `
|
||||
// }
|
||||
|
||||
// const failToReadStr = 'Could not read content. This file may have been deleted. If you expected content here, you can tell the user about this as they might not know.'
|
||||
// const stringifyFileSelections = async (fileSelections: FileSelection[], voidModelService: IVoidModelService) => {
|
||||
// if (fileSelections.length === 0) return null
|
||||
// const fileSlns: FileSelnLocal[] = await Promise.all(fileSelections.map(async (sel) => {
|
||||
// const { model } = await voidModelService.getModelSafe(sel.fileURI)
|
||||
// const content = model?.getValue(EndOfLinePreference.LF) ?? failToReadStr
|
||||
// return { ...sel, content }
|
||||
// }))
|
||||
// return fileSlns.map(sel => stringifyFileSelection(sel)).join('\n')
|
||||
// }
|
||||
|
||||
|
||||
const stringifyCodeSelections = (codeSelections: CodeSelection[]) => {
|
||||
return codeSelections.map(sel => {
|
||||
stringifyCodeSelection(sel)
|
||||
}).join('\n') || null
|
||||
}
|
||||
|
||||
const stringifySelectionNames = (currSelns: StagingSelectionItem[] | null): string => {
|
||||
if (!currSelns) return ''
|
||||
return currSelns.map(s => `${s.fileURI.fsPath}${s.range ? ` (lines ${s.range.startLineNumber}:${s.range.endLineNumber})` : ''}`).join('\n')
|
||||
}
|
||||
|
||||
|
||||
export const chat_userMessageContent = async (instructions: string, currSelns: StagingSelectionItem[] | null) => {
|
||||
// export const chat_selectionsString = async (
|
||||
// prevSelns: StagingSelectionItem[] | null, currSelns: StagingSelectionItem[] | null,
|
||||
// voidModelService: IVoidModelService,
|
||||
// ) => {
|
||||
|
||||
const selnsStr = stringifySelectionNames(currSelns)
|
||||
// // ADD IN FILES AT TOP
|
||||
// const allSelections = [...currSelns || [], ...prevSelns || []]
|
||||
|
||||
let str = ''
|
||||
if (selnsStr) { str += `SELECTIONS\n${selnsStr}\n` }
|
||||
str += `\nINSTRUCTIONS\n${instructions}`
|
||||
return str;
|
||||
};
|
||||
// if (allSelections.length === 0) return null
|
||||
|
||||
export const chat_selectionsString = async (
|
||||
prevSelns: StagingSelectionItem[] | null, currSelns: StagingSelectionItem[] | null,
|
||||
voidModelService: IVoidModelService,
|
||||
// for (const selection of allSelections) {
|
||||
// if (selection.type === 'Selection') {
|
||||
// codeSelections.push(selection)
|
||||
// }
|
||||
// else if (selection.type === 'File') {
|
||||
// const fileSelection = selection
|
||||
// const path = fileSelection.fileURI.fsPath
|
||||
// if (!filesURIs.has(path)) {
|
||||
// filesURIs.add(path)
|
||||
// fileSelections.push(fileSelection)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// const filesStr = await stringifyFileSelections(fileSelections, voidModelService)
|
||||
// const selnsStr = stringifyCodeSelections(codeSelections)
|
||||
|
||||
// const fileContents = [filesStr, selnsStr].filter(Boolean).join('\n')
|
||||
// return fileContents || null
|
||||
// }
|
||||
|
||||
// export const chat_lastUserMessageWithFilesAdded = (userMessage: string, selectionsString: string | null) => {
|
||||
// if (userMessage) return `${userMessage}${selectionsString ? `\n${selectionsString}` : ''}`
|
||||
// else return userMessage
|
||||
// }
|
||||
|
||||
export const chat_userMessageContent = async (instructions: string, currSelns: StagingSelectionItem[] | null,
|
||||
opts: { type: 'references' } | { type: 'fullCode', voidModelService: IVoidModelService }
|
||||
) => {
|
||||
|
||||
// ADD IN FILES AT TOP
|
||||
const allSelections = [...currSelns || [], ...prevSelns || []]
|
||||
const lineNumAddition = (range: [number, number]) => ` (lines ${range[0]}:${range[1]})`
|
||||
let selnsStrs: string[] = []
|
||||
if (opts.type === 'references') {
|
||||
selnsStrs = currSelns?.map((s) => {
|
||||
if (s.type === 'File') return `${s.uri.fsPath}`
|
||||
if (s.type === 'CodeSelection') return `${s.uri.fsPath}${lineNumAddition(s.range)}`
|
||||
if (s.type === 'Folder') return `${s.uri.fsPath}/`
|
||||
return ''
|
||||
}) ?? []
|
||||
}
|
||||
if (opts.type === 'fullCode') {
|
||||
selnsStrs = await Promise.all(currSelns?.map(async (s) => {
|
||||
if (s.type === 'File' || s.type === 'CodeSelection') {
|
||||
const voidModelService = opts.voidModelService
|
||||
const { model } = await voidModelService.getModelSafe(s.uri)
|
||||
if (!model) return ''
|
||||
const val = model.getValue(EndOfLinePreference.LF)
|
||||
|
||||
if (allSelections.length === 0) return null
|
||||
|
||||
const codeSelections: CodeSelection[] = []
|
||||
const fileSelections: FileSelection[] = []
|
||||
const filesURIs = new Set<string>()
|
||||
|
||||
for (const selection of allSelections) {
|
||||
if (selection.type === 'Selection') {
|
||||
codeSelections.push(selection)
|
||||
}
|
||||
else if (selection.type === 'File') {
|
||||
const fileSelection = selection
|
||||
const path = fileSelection.fileURI.fsPath
|
||||
if (!filesURIs.has(path)) {
|
||||
filesURIs.add(path)
|
||||
fileSelections.push(fileSelection)
|
||||
const lineNumAdd = s.type === 'CodeSelection' ? lineNumAddition(s.range) : ''
|
||||
const str = `${s.uri.fsPath}${lineNumAdd}\n${tripleTick[0]}${s.language}\n${val}\n${tripleTick[1]}`
|
||||
return str
|
||||
}
|
||||
}
|
||||
if (s.type === 'Folder') {
|
||||
// TODO
|
||||
return ''
|
||||
}
|
||||
return ''
|
||||
}) ?? [])
|
||||
}
|
||||
|
||||
const filesStr = await stringifyFileSelections(fileSelections, voidModelService)
|
||||
const selnsStr = stringifyCodeSelections(codeSelections)
|
||||
|
||||
const fileContents = [filesStr, selnsStr].filter(Boolean).join('\n')
|
||||
return fileContents || null
|
||||
}
|
||||
|
||||
export const chat_lastUserMessageWithFilesAdded = (userMessage: string, selectionsString: string | null) => {
|
||||
if (userMessage) return `${userMessage}${selectionsString ? `\n${selectionsString}` : ''}`
|
||||
else return userMessage
|
||||
const selnsStr = selnsStrs.join('\n') ?? ''
|
||||
let str = ''
|
||||
str += `${instructions}`
|
||||
if (selnsStr) str += `\n---\nSELECTIONS\n${selnsStr}`
|
||||
return str;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
19
src/vs/workbench/contrib/void/common/storageKeys.ts
Normal file
19
src/vs/workbench/contrib/void/common/storageKeys.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
// past values:
|
||||
// 'void.settingsServiceStorage'
|
||||
// 'void.settingsServiceStorageI' // 1.0.2
|
||||
|
||||
// 1.0.3
|
||||
export const VOID_SETTINGS_STORAGE_KEY = 'void.settingsServiceStorageII'
|
||||
|
||||
|
||||
// past values:
|
||||
// 'void.chatThreadStorage'
|
||||
// 'void.chatThreadStorageI' // 1.0.2
|
||||
|
||||
// 1.0.3
|
||||
export const THREAD_STORAGE_KEY = 'void.chatThreadStorageII'
|
||||
|
|
@ -2,18 +2,18 @@ import { URI } from '../../../../base/common/uri.js'
|
|||
import { voidTools } from './prompt/prompts.js';
|
||||
|
||||
|
||||
export type ToolDirectoryItem = {
|
||||
|
||||
|
||||
export type TerminalResolveReason = { type: 'toofull' | 'timeout' | 'bgtask' } | { type: 'done', exitCode: number }
|
||||
|
||||
// Partial of IFileStat
|
||||
export type ShallowDirectoryItem = {
|
||||
uri: URI;
|
||||
name: string;
|
||||
isDirectory: boolean;
|
||||
isSymbolicLink: boolean;
|
||||
}
|
||||
|
||||
|
||||
export type TerminalResolveReason = { type: 'toofull' | 'timeout' | 'bgtask' } | { type: 'done', exitCode: number }
|
||||
|
||||
|
||||
|
||||
// we do this using Anthropic's style and convert to OpenAI style later
|
||||
export type InternalToolInfo = {
|
||||
name: string,
|
||||
|
|
@ -36,32 +36,36 @@ export const isAToolName = (toolName: string): toolName is ToolName => {
|
|||
}
|
||||
|
||||
|
||||
const toolNamesWithApproval = ['create_uri', 'delete_uri', 'edit', 'terminal_command'] as const satisfies readonly ToolName[]
|
||||
const toolNamesWithApproval = ['create_file_or_folder', 'delete_file_or_folder', 'edit_file', 'run_terminal_command'] as const satisfies readonly ToolName[]
|
||||
export type ToolNameWithApproval = typeof toolNamesWithApproval[number]
|
||||
export const toolNamesThatRequireApproval = new Set<ToolName>(toolNamesWithApproval)
|
||||
|
||||
// PARAMS OF TOOL CALL
|
||||
export type ToolCallParams = {
|
||||
'read_file': { uri: URI, pageNumber: number },
|
||||
'list_dir': { rootURI: URI, pageNumber: number },
|
||||
'pathname_search': { queryStr: string, pageNumber: number },
|
||||
'grep_search': { queryStr: string, pageNumber: number },
|
||||
'read_file': { uri: URI, startLine: number | null, endLine: number | null, pageNumber: number },
|
||||
'ls_dir': { rootURI: URI, pageNumber: number },
|
||||
'get_dir_structure': { rootURI: URI },
|
||||
'search_pathnames_only': { queryStr: string, include: string | null, pageNumber: number },
|
||||
'search_files': { queryStr: string, isRegex: boolean, searchInFolder: URI | null, pageNumber: number },
|
||||
// ---
|
||||
'edit': { uri: URI, changeDescription: string },
|
||||
'create_uri': { uri: URI, isFolder: boolean },
|
||||
'delete_uri': { uri: URI, isRecursive: boolean, isFolder: boolean },
|
||||
'terminal_command': { command: string, proposedTerminalId: string, waitForCompletion: boolean },
|
||||
'edit_file': { uri: URI, changeDescription: string },
|
||||
'create_file_or_folder': { uri: URI, isFolder: boolean },
|
||||
'delete_file_or_folder': { uri: URI, isRecursive: boolean, isFolder: boolean },
|
||||
'run_terminal_command': { command: string, proposedTerminalId: string, waitForCompletion: boolean },
|
||||
}
|
||||
|
||||
|
||||
// RESULT OF TOOL CALL
|
||||
export type ToolResultType = {
|
||||
'read_file': { fileContents: string, hasNextPage: boolean },
|
||||
'list_dir': { children: ToolDirectoryItem[] | null, hasNextPage: boolean, hasPrevPage: boolean, itemsRemaining: number },
|
||||
'pathname_search': { uris: URI[], hasNextPage: boolean },
|
||||
'grep_search': { uris: URI[], hasNextPage: boolean },
|
||||
'ls_dir': { children: ShallowDirectoryItem[] | null, hasNextPage: boolean, hasPrevPage: boolean, itemsRemaining: number },
|
||||
'get_dir_structure': { str: string, },
|
||||
'search_pathnames_only': { uris: URI[], hasNextPage: boolean },
|
||||
'search_files': { uris: URI[], hasNextPage: boolean },
|
||||
// ---
|
||||
'edit': Promise<void>,
|
||||
'create_uri': {},
|
||||
'delete_uri': {},
|
||||
'terminal_command': { terminalId: string, didCreateTerminal: boolean, result: string; resolveReason: TerminalResolveReason; },
|
||||
'edit_file': Promise<{ lintErrorsStr: string | null }>,
|
||||
'create_file_or_folder': {},
|
||||
'delete_file_or_folder': {},
|
||||
'run_terminal_command': { terminalId: string, didCreateTerminal: boolean, result: string; resolveReason: TerminalResolveReason; },
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export interface IVoidModelService {
|
|||
readonly _serviceBrand: undefined;
|
||||
initializeModel(uri: URI): Promise<void>;
|
||||
getModel(uri: URI): VoidModelType;
|
||||
getModelFromFsPath(fsPath: string): VoidModelType;
|
||||
getModelSafe(uri: URI): Promise<VoidModelType>;
|
||||
}
|
||||
|
||||
|
|
@ -37,8 +38,8 @@ class VoidModelService extends Disposable implements IVoidModelService {
|
|||
this._modelRefOfURI[uri.fsPath] = editorModelRef;
|
||||
};
|
||||
|
||||
getModel = (uri: URI): VoidModelType => {
|
||||
const editorModelRef = this._modelRefOfURI[uri.fsPath];
|
||||
getModelFromFsPath = (fsPath: string): VoidModelType => {
|
||||
const editorModelRef = this._modelRefOfURI[fsPath];
|
||||
if (!editorModelRef) {
|
||||
return { model: null, editorModel: null };
|
||||
}
|
||||
|
|
@ -52,6 +53,11 @@ class VoidModelService extends Disposable implements IVoidModelService {
|
|||
return { model, editorModel: editorModelRef.object };
|
||||
};
|
||||
|
||||
getModel = (uri: URI) => {
|
||||
return this.getModelFromFsPath(uri.fsPath)
|
||||
}
|
||||
|
||||
|
||||
getModelSafe = async (uri: URI): Promise<VoidModelType> => {
|
||||
if (!(uri.fsPath in this._modelRefOfURI)) await this.initializeModel(uri);
|
||||
return this.getModel(uri);
|
||||
|
|
|
|||
|
|
@ -12,13 +12,9 @@ import { createDecorator } from '../../../../platform/instantiation/common/insta
|
|||
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
|
||||
import { IMetricsService } from './metricsService.js';
|
||||
import { getModelCapabilities } from './modelCapabilities.js';
|
||||
import { VOID_SETTINGS_STORAGE_KEY } from './storageKeys.js';
|
||||
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, VoidModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, defaultProviderSettings, ModelSelectionOptions, OptionsOfModelSelection, ChatMode } from './voidSettingsTypes.js';
|
||||
|
||||
// past values:
|
||||
// 'void.settingsServiceStorage'
|
||||
|
||||
const STORAGE_KEY = 'void.settingsServiceStorageI'
|
||||
|
||||
|
||||
// name is the name in the dropdown
|
||||
export type ModelOption = { name: string, selection: ModelSelection }
|
||||
|
|
@ -38,7 +34,7 @@ type SetModelSelectionOfFeatureFn = <K extends FeatureName>(
|
|||
|
||||
type SetGlobalSettingFn = <T extends GlobalSettingName>(settingName: T, newVal: GlobalSettings[T]) => void;
|
||||
|
||||
type SetOptionsOfModelSelection = (providerName: ProviderName, modelName: string, newVal: Partial<ModelSelectionOptions>) => void
|
||||
type SetOptionsOfModelSelection = (featureName: FeatureName, providerName: ProviderName, modelName: string, newVal: Partial<ModelSelectionOptions>) => void
|
||||
|
||||
|
||||
export type VoidSettingsState = {
|
||||
|
|
@ -177,7 +173,7 @@ const defaultState = () => {
|
|||
settingsOfProvider: deepClone(defaultSettingsOfProvider),
|
||||
modelSelectionOfFeature: { 'Chat': null, 'Ctrl+K': null, 'Autocomplete': null, 'Apply': null },
|
||||
globalSettings: deepClone(defaultGlobalSettings),
|
||||
optionsOfModelSelection: {},
|
||||
optionsOfModelSelection: { 'Chat': {}, 'Ctrl+K': {}, 'Autocomplete': {}, 'Apply': {} },
|
||||
_modelOptions: [], // computed later
|
||||
}
|
||||
return d
|
||||
|
|
@ -227,7 +223,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
|||
|
||||
|
||||
private async _readState(): Promise<VoidSettingsState> {
|
||||
const encryptedState = this._storageService.get(STORAGE_KEY, StorageScope.APPLICATION)
|
||||
const encryptedState = this._storageService.get(VOID_SETTINGS_STORAGE_KEY, StorageScope.APPLICATION)
|
||||
|
||||
if (!encryptedState)
|
||||
return defaultState()
|
||||
|
|
@ -240,7 +236,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
|||
private async _storeState() {
|
||||
const state = this.state
|
||||
const encryptedState = await this._encryptionService.encrypt(JSON.stringify(state))
|
||||
this._storageService.store(STORAGE_KEY, encryptedState, StorageScope.APPLICATION, StorageTarget.USER);
|
||||
this._storageService.store(VOID_SETTINGS_STORAGE_KEY, encryptedState, StorageScope.APPLICATION, StorageTarget.USER);
|
||||
}
|
||||
|
||||
setSettingOfProvider: SetSettingOfProviderFn = async (providerName, settingName, newVal) => {
|
||||
|
|
@ -318,16 +314,19 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
|||
}
|
||||
|
||||
|
||||
setOptionsOfModelSelection = async (providerName: ProviderName, modelName: string, newVal: Partial<ModelSelectionOptions>) => {
|
||||
setOptionsOfModelSelection = async (featureName: FeatureName, providerName: ProviderName, modelName: string, newVal: Partial<ModelSelectionOptions>) => {
|
||||
const newState: VoidSettingsState = {
|
||||
...this.state,
|
||||
optionsOfModelSelection: {
|
||||
...this.state.optionsOfModelSelection,
|
||||
[providerName]: {
|
||||
...this.state.optionsOfModelSelection[providerName],
|
||||
[modelName]: {
|
||||
...this.state.optionsOfModelSelection[providerName]?.[modelName],
|
||||
...newVal
|
||||
[featureName]: {
|
||||
...this.state.optionsOfModelSelection[featureName],
|
||||
[providerName]: {
|
||||
...this.state.optionsOfModelSelection[featureName][providerName],
|
||||
[modelName]: {
|
||||
...this.state.optionsOfModelSelection[featureName][providerName]?.[modelName],
|
||||
...newVal
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -436,4 +436,11 @@ export type ModelSelectionOptions = {
|
|||
reasoningBudget?: number;
|
||||
}
|
||||
|
||||
export type OptionsOfModelSelection = Partial<{ [providerName in ProviderName]: { [modelName: string]: ModelSelectionOptions | undefined } }>
|
||||
export type OptionsOfModelSelection = {
|
||||
[featureName in FeatureName]: Partial<{
|
||||
[providerName in ProviderName]: {
|
||||
[modelName: string]:
|
||||
ModelSelectionOptions | undefined
|
||||
}
|
||||
}>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,17 +40,10 @@ const prepareMessages_normalize = ({ messages: messages_ }: { messages: LLMChatM
|
|||
const newMessages: LLMChatMessage[] = []
|
||||
if (messages.length >= 0) newMessages.push(messages[0])
|
||||
|
||||
// remove duplicate roles
|
||||
// remove duplicate roles - we used to do this, but we don't anymore
|
||||
for (let i = 1; i < messages.length; i += 1) {
|
||||
const curr = messages[i]
|
||||
// const prev = messages[i - 1]
|
||||
// // if found a repeated role, put the current content in the prev
|
||||
// if ((curr.role === 'assistant' && prev.role === 'assistant')) {
|
||||
// prev.content += '\n' + curr.content
|
||||
// continue
|
||||
// }
|
||||
// add the message
|
||||
newMessages.push(curr)
|
||||
const m = messages[i]
|
||||
newMessages.push(m)
|
||||
}
|
||||
const finalMessages = newMessages.map(m => ({ ...m, content: m.content.trim() }))
|
||||
return { messages: finalMessages }
|
||||
|
|
@ -61,6 +54,95 @@ const prepareMessages_normalize = ({ messages: messages_ }: { messages: LLMChatM
|
|||
|
||||
|
||||
|
||||
const CHARS_PER_TOKEN = 4
|
||||
const TRIM_TO_LEN = 60
|
||||
|
||||
const prepareMessages_fitIntoContext = ({ messages, contextWindow, maxOutputTokens }: { messages: LLMChatMessage[], contextWindow: number, maxOutputTokens: number }): { messages: LLMChatMessage[] } => {
|
||||
|
||||
// the higher the weight, the higher the desire to truncate
|
||||
const alreadyTrimmedIdxes = new Set<number>()
|
||||
const weight = (message: LLMChatMessage, messages: LLMChatMessage[], idx: number) => {
|
||||
const base = message.content.length
|
||||
|
||||
let multiplier: number
|
||||
if (message.role === 'system')
|
||||
return 0 // never erase system message
|
||||
|
||||
multiplier = 1 + (messages.length - 1 - idx) / messages.length // slow rampdown from 2 to 1 as index increases
|
||||
if (message.role === 'user') {
|
||||
multiplier *= 1
|
||||
}
|
||||
else {
|
||||
multiplier *= 10 // llm tokens are far less valuable than user tokens
|
||||
}
|
||||
|
||||
// 1st message, last 3 msgs, any already modified message should be low in weight
|
||||
if (idx === 0 || idx >= messages.length - 1 - 3 || alreadyTrimmedIdxes.has(idx)) {
|
||||
multiplier *= .05
|
||||
}
|
||||
|
||||
return base * multiplier
|
||||
|
||||
}
|
||||
const _findLargestByWeight = (messages: LLMChatMessage[]) => {
|
||||
let largestIndex = -1
|
||||
let largestWeight = -Infinity
|
||||
for (let i = 0; i < messages.length; i += 1) {
|
||||
const m = messages[i]
|
||||
const w = weight(m, messages, i)
|
||||
if (w > largestWeight) {
|
||||
largestWeight = w
|
||||
largestIndex = i
|
||||
}
|
||||
}
|
||||
return largestIndex
|
||||
}
|
||||
|
||||
let totalLen = 0
|
||||
for (const m of messages) { totalLen += m.content.length }
|
||||
const charsNeedToTrim = totalLen - (contextWindow - maxOutputTokens) * CHARS_PER_TOKEN
|
||||
if (charsNeedToTrim <= 0) return { messages }
|
||||
|
||||
// <----------------------------------------->
|
||||
// 0 | | |
|
||||
// | contextWindow |
|
||||
// contextWindow - maxOut|putTokens
|
||||
// |
|
||||
// totalLen
|
||||
|
||||
|
||||
// TRIM HIGHEST WEIGHT MESSAGES
|
||||
let remainingCharsToTrim = charsNeedToTrim
|
||||
let i = 0
|
||||
|
||||
while (remainingCharsToTrim > 0) {
|
||||
i += 1
|
||||
if (i > 100) break
|
||||
|
||||
const trimIdx = _findLargestByWeight(messages)
|
||||
const m = messages[trimIdx]
|
||||
|
||||
// if can finish here, do
|
||||
const numCharsWillTrim = m.content.length - TRIM_TO_LEN
|
||||
if (numCharsWillTrim > remainingCharsToTrim) {
|
||||
m.content = m.content.slice(0, m.content.length - remainingCharsToTrim)
|
||||
break
|
||||
}
|
||||
|
||||
remainingCharsToTrim -= numCharsWillTrim
|
||||
m.content = m.content.substring(0, TRIM_TO_LEN - 3) + '...'
|
||||
alreadyTrimmedIdxes.add(trimIdx)
|
||||
}
|
||||
|
||||
return { messages }
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// no matter whether the model supports a system message or not (or what format it supports), add it in some way
|
||||
const prepareMessages_systemMessage = ({
|
||||
|
|
@ -389,14 +471,21 @@ export const prepareMessages = ({
|
|||
supportsSystemMessage,
|
||||
supportsTools,
|
||||
supportsAnthropicReasoningSignature,
|
||||
contextWindow,
|
||||
maxOutputTokens,
|
||||
}: {
|
||||
messages: LLMChatMessage[],
|
||||
aiInstructions: string,
|
||||
supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated',
|
||||
supportsTools: false | 'anthropic-style' | 'openai-style' | 'mistral-style',
|
||||
supportsAnthropicReasoningSignature: boolean,
|
||||
contextWindow: number,
|
||||
maxOutputTokens: number | null | undefined,
|
||||
}) => {
|
||||
const { messages: messages1 } = prepareMessages_normalize({ messages })
|
||||
maxOutputTokens = maxOutputTokens ?? 4_096 // default to 4096
|
||||
|
||||
const { messages: messages0 } = prepareMessages_normalize({ messages })
|
||||
const { messages: messages1 } = prepareMessages_fitIntoContext({ messages: messages0, contextWindow, maxOutputTokens })
|
||||
const { messages: messages2 } = prepareMessages_anthropicContent({ messages: messages1, supportsAnthropicReasoningSignature })
|
||||
const { messages: messages3, separateSystemMessageStr } = prepareMessages_systemMessage({ messages: messages2, aiInstructions, supportsSystemMessage })
|
||||
const { messages: messages4 } = prepareMessages_tools({ messages: messages3, supportsTools })
|
||||
|
|
|
|||
|
|
@ -171,7 +171,8 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage
|
|||
modelName,
|
||||
supportsSystemMessage,
|
||||
supportsTools,
|
||||
// maxOutputTokens,
|
||||
contextWindow,
|
||||
maxOutputTokens,
|
||||
reasoningCapabilities,
|
||||
} = getModelCapabilities(providerName, modelName_)
|
||||
|
||||
|
|
@ -179,7 +180,7 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage
|
|||
|
||||
// reasoning
|
||||
const { canIOReasoning, openSourceThinkTags, } = reasoningCapabilities || {}
|
||||
const reasoningInfo = getSendableReasoningInfo(providerName, modelName_, modelSelectionOptions) // user's modelName_ here
|
||||
const reasoningInfo = getSendableReasoningInfo('Chat', providerName, modelName_, modelSelectionOptions) // user's modelName_ here
|
||||
const includeInPayload = providerReasoningIOSettings?.input?.includeInPayload?.(reasoningInfo) || {}
|
||||
|
||||
// tools
|
||||
|
|
@ -187,10 +188,10 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage
|
|||
const toolsObj = tools ? { tools: tools, tool_choice: 'auto', parallel_tool_calls: false, } as const : {}
|
||||
|
||||
// max tokens
|
||||
// const maxTokens = reasoningInfo?.isReasoningEnabled && reasoningCapabilities ? reasoningCapabilities.reasoningMaxOutputTokens : maxOutputTokens
|
||||
const maxTokens = reasoningInfo?.isReasoningEnabled && reasoningCapabilities ? reasoningCapabilities.reasoningMaxOutputTokens : maxOutputTokens
|
||||
|
||||
// instance
|
||||
const { messages } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsTools, supportsAnthropicReasoningSignature: false })
|
||||
const { messages } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsTools, supportsAnthropicReasoningSignature: false, contextWindow, maxOutputTokens: maxTokens })
|
||||
const openai: OpenAI = newOpenAICompatibleSDK({ providerName, settingsOfProvider, includeInPayload })
|
||||
const options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = {
|
||||
model: modelName,
|
||||
|
|
@ -330,6 +331,7 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM
|
|||
const {
|
||||
modelName,
|
||||
supportsSystemMessage,
|
||||
contextWindow,
|
||||
supportsTools,
|
||||
maxOutputTokens,
|
||||
reasoningCapabilities,
|
||||
|
|
@ -339,7 +341,7 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM
|
|||
const { providerReasoningIOSettings } = getProviderCapabilities(providerName)
|
||||
|
||||
// reasoning
|
||||
const reasoningInfo = getSendableReasoningInfo(providerName, modelName_, modelSelectionOptions) // user's modelName_ here
|
||||
const reasoningInfo = getSendableReasoningInfo('Chat', providerName, modelName_, modelSelectionOptions) // user's modelName_ here
|
||||
const includeInPayload = providerReasoningIOSettings?.input?.includeInPayload?.(reasoningInfo) || {}
|
||||
|
||||
// tools
|
||||
|
|
@ -353,7 +355,7 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM
|
|||
const maxTokens = reasoningInfo?.isReasoningEnabled && reasoningCapabilities ? reasoningCapabilities.reasoningMaxOutputTokens : maxOutputTokens
|
||||
|
||||
// instance
|
||||
const { messages, separateSystemMessageStr } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsTools, supportsAnthropicReasoningSignature: true })
|
||||
const { messages, separateSystemMessageStr } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsTools, supportsAnthropicReasoningSignature: true, contextWindow, maxOutputTokens: maxTokens })
|
||||
const anthropic = new Anthropic({
|
||||
apiKey: thisConfig.apiKey,
|
||||
dangerouslyAllowBrowser: true
|
||||
|
|
|
|||
Loading…
Reference in a new issue