Compare commits

..

8 Commits

Author SHA1 Message Date
Andrew Pareles 7098e9072e fix 2025-04-28 20:20:40 -07:00
Andrew Pareles 40aa5a2467 Merge branch 'rebase-new' into model-selection-with-rebase 2025-04-28 18:17:08 -07:00
Andrew Pareles d077eb9ca4 voideditor-test -> voideditor 2025-04-28 16:47:57 -07:00
Andrew Pareles 54da9fa0f9 finish rebase again 2025-04-28 16:37:50 -07:00
Andrew Pareles 19dda35ff7 finish rebase from voideditor-test/void 2025-04-28 16:03:29 -07:00
Andrew Pareles d15e559a73 re-add image changes 2025-04-21 04:12:53 -07:00
Andrew Pareles f949c05b14 update package lock 2025-04-21 01:58:29 -07:00
Andrew Pareles 121983ae6d rebase part 1 2025-04-21 01:55:32 -07:00
58 changed files with 6953 additions and 12760 deletions
-1
View File
@@ -1 +0,0 @@
blank_issues_enabled: false
+1 -1
View File
@@ -6,4 +6,4 @@ title: For VSCode-related issues (eg builds), please start the title with `[App]
1. Press `Cmd+Shift+P` in Void, and type `Help: About`. Please paste the information here. Also let us know any other relevant details, like the model and provider you're using if applicable.
2. Describe the issue/feature here!
2. Describe the issue/feature here.
-7
View File
@@ -21,10 +21,3 @@ vscode.db
product.overrides.json
*.snap.actual
.vscode-test
# Void added these:
.tmp/
.tmp2/
.tool-versions
src/vs/workbench/contrib/void/browser/react/out/**
src/vs/workbench/contrib/void/browser/react/src2/**
-10
View File
@@ -1,10 +0,0 @@
This is a fork of the VSCode repo called Void.
Most code we care about lives in src/vs/workbench/contrib/void.
You may often need to explore the full repo to find relevant parts of code.
Look for services and built-in functions that you might need to use to solve the problem.
In typescript, do NOT cast to types if not neccessary. NEVER lazily cast to 'any'. Find the correct type to apply and use it.
Do not add or remove semicolons to any of my files. Just go with convention and make the least number of changes.
+11 -14
View File
@@ -12,7 +12,7 @@ There are a few ways to contribute:
### Codebase Guide
We [highly recommend reading this](https://github.com/voideditor/void/blob/main/VOID_CODEBASE_GUIDE.md) guide that we put together on Void's sourcecode if you'd like to add new features.
We [highly recommend reading this](https://github.com/voideditor/void/blob/main/VOID_CODEBASE_GUIDE.md) guide that we put together on Void's sourcecode if you'd like to contribute!
The repo is not as intimidating as it first seems if you read the guide!
@@ -52,18 +52,17 @@ First, run `npm install -g node-gyp`. Then:
### d. Building Void from inside VSCode
1. `git clone https://github.com/voideditor/void` to clone the repo.
2. `npm install` to install all dependencies.
3. To build Void, open VSCode. Then:
- Windows: Press <kbd>Ctrl+Shift+B</kbd>.
- Mac: Press <kbd>Cmd+Shift+B</kbd>.
- Linux: Press <kbd>Ctrl+Shift+B</kbd>.
To build Void, open `void/` inside VSCode. Then open your terminal and run:
1. `npm install` to install all dependencies.
2. Build Void.
- Press <kbd>Cmd+Shift+B</kbd> (Mac).
- Press <kbd>Ctrl+Shift+B</kbd> (Windows/Linux).
- This step can take ~5 min. The build is done when you see two check marks (one of the items will continue spinning indefinitely - it compiles our React code).
4. To run Void:
- Windows: `./scripts/code.bat`.
- Mac: `./scripts/code.sh`.
- Linux: `./scripts/code.sh`.
5. Nice-to-knows.
3. Run Void.
- Run `./scripts/code.sh` (Mac/Linux).
- Run `./scripts/code.bat` (Windows).
4. Nice-to-knows.
- You can always press <kbd>Ctrl+R</kbd> (<kbd>Cmd+R</kbd>) inside the new window to reload and see your new changes. It's faster than <kbd>Ctrl+Shift+P</kbd> and `Reload Window`.
- You might want to add the flags `--user-data-dir ./.tmp/user-data --extensions-dir ./.tmp/extensions` to the above run command, which lets you delete the `.tmp` folder to reset any IDE changes you made when testing.
- You can kill any of the build scripts by pressing `Ctrl+D` in VSCode terminal. If you press `Ctrl+C` the script will close but will keep running in the background (to open all background scripts, just re-build).
@@ -86,8 +85,6 @@ To build Void from the terminal instead of from inside VSCode, follow the steps
- Make sure you followed the prerequisite steps above.
- Make sure you have Node version `20.18.2` (the version in `.nvmrc`)!
- You can do this easily without touching your base installation with [nvm](https://github.com/nvm-sh/nvm). Simply run `nvm install`, followed by `nvm use` and it will automatically install and use the version specified in `nvmrc`.
- Make sure that the path to your Void folder does not have any spaces in it.
- If you get `"TypeError: Failed to fetch dynamically imported module"`, make sure all imports end with `.js`.
- If you get an error with React, try running `NODE_OPTIONS="--max-old-space-size=8192" npm run buildreact`.
- If you see missing styles, wait a few seconds and then reload.
+13 -10
View File
@@ -11,27 +11,30 @@
Void is the open-source Cursor alternative.
Use AI agents on your codebase, checkpoint and visualize changes, and bring any model or host locally. Void sends messages directly to providers without retaining your data.
This repo contains the full sourcecode for Void. If you're new, welcome!
- 🧭 [Website](https://voideditor.com)
This repo contains the full sourcecode for Void. We are currently in [open beta](https://voideditor.com/email) for Discord members (see the `announcements` channel), with a waitlist for our official release. If you're new, welcome!
- 👋 [Discord](https://discord.gg/RSNjgaugJs)
- 🚙 [Project Board](https://github.com/orgs/voideditor/projects/2)
- 🔨 [Contribute](https://github.com/voideditor/void/blob/main/HOW_TO_CONTRIBUTE.md)
- 🚙 [Roadmap](https://github.com/orgs/voideditor/projects/2)
- 📝 [Changelog](https://voideditor.com/changelog)
- 🧭 [Codebase Guide](https://github.com/voideditor/void/blob/main/VOID_CODEBASE_GUIDE.md)
## Contributing
1. To get started working on Void, check out our Project Board! You can also see [HOW_TO_CONTRIBUTE](https://github.com/voideditor/void/blob/main/HOW_TO_CONTRIBUTE.md).
1. Feel free to attend a weekly meeting in our Discord channel if you'd like to contribute!
2. Feel free to attend a casual weekly meeting in our Discord channel!
2. To get started working on Void, see [`HOW_TO_CONTRIBUTE`](https://github.com/voideditor/void/blob/main/HOW_TO_CONTRIBUTE.md).
3. We're open to collaborations and suggestions of all types - just reach out.
## Reference
Void is a fork of the [vscode](https://github.com/microsoft/vscode) repository. For a guide to the codebase, see [VOID_CODEBASE_GUIDE](https://github.com/voideditor/void/blob/main/VOID_CODEBASE_GUIDE.md).
Void is a fork of the [vscode](https://github.com/microsoft/vscode) repository. For a guide to the VSCode/Void codebase, see [`VOID_CODEBASE_GUIDE`](https://github.com/voideditor/void/blob/main/VOID_CODEBASE_GUIDE.md).
## Support
You can always reach us in our Discord server or contact us via email: hello@voideditor.com.
Feel free to reach out in our Discord or contact us via email: hello@voideditor.com.
+8 -4
View File
@@ -132,24 +132,25 @@ If you want to know how our build pipeline works, see our build repo [here](http
## VSCode Codebase Guide
For additional references, the Void team put together this list of links to get up and running with VSCode.
<details>
The Void team put together this list of links to get up and running with VSCode's sourcecode, the foundation of Void. We hope it's helpful!
#### Links for Beginners
- [VSCode UI guide](https://code.visualstudio.com/docs/getstarted/userinterface) - covers auxbar, panels, etc.
- [UX guide](https://code.visualstudio.com/api/ux-guidelines/overview) - covers Containers, Views, Items, etc.
#### Links for Contributors
- [How VSCode's sourcecode is organized](https://github.com/microsoft/vscode/wiki/Source-Code-Organization) - this explains where the entry point files are, what `browser/` and `common/` mean, etc. This is the most important read on this whole list! We recommend reading the whole thing.
- [Built-in VSCode styles](https://code.visualstudio.com/api/references/theme-color) - CSS variables that are built into VSCode. Use `var(--vscode-{theme but replacing . with -})`. You can also see their [Webview theming guide](https://code.visualstudio.com/api/extension-guides/webview#theming-webview-content).
#### Misc
- [Every command](https://code.visualstudio.com/api/references/commands) built-in to VSCode - not used often, but here for reference.
- Note: VSCode's repo is the source code for the Monaco editor! An "editor" is a Monaco editor, and it shares the code for ITextModel, etc.
@@ -158,10 +159,13 @@ For additional references, the Void team put together this list of links to get
Void is no longer an extension, so these links are no longer required, but they might be useful if we ever build an extension again.
- [Files you need in an extension](https://code.visualstudio.com/api/get-started/extension-anatomy).
- [An extension's `package.json` schema](https://code.visualstudio.com/api/references/extension-manifest).
- ["Contributes" Guide](https://code.visualstudio.com/api/references/contribution-points) - the `"contributes"` part of `package.json` is how an extension mounts.
- [The Full VSCode Extension API](https://code.visualstudio.com/api/references/vscode-api) - look on the right side for organization. The [bottom](https://code.visualstudio.com/api/references/vscode-api#api-patterns) of the page is easy to miss but is useful - cancellation tokens, events, disposables.
- [Activation events](https://code.visualstudio.com/api/references/activation-events) you can define in `package.json` (not the most useful).
</details>
+1
View File
@@ -82,6 +82,7 @@ function buildWin32Setup(arch, target) {
productJson['target'] = target;
fs.writeFileSync(productJsonPath, JSON.stringify(productJson, undefined, '\t'));
console.log('RawVersion!!!!!!!!!!!!!!', pkg.version.replace(/-\w+$/, '')) // Void
const quality = product.quality || 'dev';
const definitions = {
NameLong: product.nameLong,
+1 -1
View File
@@ -71,7 +71,7 @@
"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}/void-reh-${os}-${arch}-${version}.tar.gz"
"default": "https://github.com/voideditor/binaries/releases/download/${version}.${release}/void-reh-${os}-${arch}-${version}.${release}.tar.gz"
},
"remote.SSH.remotePlatform": {
"type": "object",
+165 -165
View File
@@ -8,203 +8,203 @@ import { getVSCodeServerConfig } from './serverConfig';
import SSHConnection from './ssh/sshConnection';
export interface ServerInstallOptions {
id: string;
quality: string;
commit: string;
version: string;
release?: string; // void specific
extensionIds: string[];
envVariables: string[];
useSocketPath: boolean;
serverApplicationName: string;
serverDataFolderName: string;
serverDownloadUrlTemplate: string;
id: string;
quality: string;
commit: string;
version: string;
release?: string; // void specific
extensionIds: string[];
envVariables: string[];
useSocketPath: boolean;
serverApplicationName: string;
serverDataFolderName: string;
serverDownloadUrlTemplate: string;
}
export interface ServerInstallResult {
exitCode: number;
listeningOn: number | string;
connectionToken: string;
logFile: string;
osReleaseId: string;
arch: string;
platform: string;
tmpDir: string;
[key: string]: any;
exitCode: number;
listeningOn: number | string;
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);
}
constructor(message: string) {
super(message);
}
}
const DEFAULT_DOWNLOAD_URL_TEMPLATE = 'https://github.com/voideditor/binaries/releases/download/${version}/void-reh-${os}-${arch}-${version}.tar.gz';
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(conn: SSHConnection, serverDownloadUrlTemplate: string | undefined, extensionIds: string[], envVariables: string[], platform: string | undefined, useSocketPath: boolean, logger: Log): Promise<ServerInstallResult> {
let shell = 'powershell';
let shell = 'powershell';
// detect platform and shell for windows
if (!platform || platform === 'windows') {
const result = await conn.exec('uname -s');
// detect platform and shell for windows
if (!platform || platform === 'windows') {
const result = await conn.exec('uname -s');
if (result.stdout) {
if (result.stdout.includes('windows32')) {
platform = 'windows';
} else if (result.stdout.includes('MINGW64')) {
platform = 'windows';
shell = 'bash';
}
} else if (result.stderr) {
if (result.stderr.includes('FullyQualifiedErrorId : CommandNotFoundException')) {
platform = 'windows';
}
if (result.stdout) {
if (result.stdout.includes('windows32')) {
platform = 'windows';
} else if (result.stdout.includes('MINGW64')) {
platform = 'windows';
shell = 'bash';
}
} else if (result.stderr) {
if (result.stderr.includes('FullyQualifiedErrorId : CommandNotFoundException')) {
platform = 'windows';
}
if (result.stderr.includes('is not recognized as an internal or external command')) {
platform = 'windows';
shell = 'cmd';
}
}
if (result.stderr.includes('is not recognized as an internal or external command')) {
platform = 'windows';
shell = 'cmd';
}
}
if (platform) {
logger.trace(`Detected platform: ${platform}, ${shell}`);
}
}
if (platform) {
logger.trace(`Detected platform: ${platform}, ${shell}`);
}
}
const scriptId = crypto.randomBytes(12).toString('hex');
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,
useSocketPath,
serverApplicationName: vscodeServerConfig.serverApplicationName,
serverDataFolderName: vscodeServerConfig.serverDataFolderName,
serverDownloadUrlTemplate: serverDownloadUrlTemplate ?? vscodeServerConfig.serverDownloadUrlTemplate ?? DEFAULT_DOWNLOAD_URL_TEMPLATE,
};
const vscodeServerConfig = await getVSCodeServerConfig();
const installOptions: ServerInstallOptions = {
id: scriptId,
version: vscodeServerConfig.version,
commit: vscodeServerConfig.commit,
quality: vscodeServerConfig.quality,
release: vscodeServerConfig.release,
extensionIds,
envVariables,
useSocketPath,
serverApplicationName: vscodeServerConfig.serverApplicationName,
serverDataFolderName: vscodeServerConfig.serverDataFolderName,
serverDownloadUrlTemplate: serverDownloadUrlTemplate ?? vscodeServerConfig.serverDownloadUrlTemplate ?? DEFAULT_DOWNLOAD_URL_TEMPLATE,
};
let commandOutput: { stdout: string; stderr: string };
if (platform === 'windows') {
const installServerScript = generatePowerShellInstallScript(installOptions);
let commandOutput: { stdout: string; stderr: string };
if (platform === 'windows') {
const installServerScript = generatePowerShellInstallScript(installOptions);
logger.trace('Server install command:', installServerScript);
logger.trace('Server install command:', installServerScript);
const installDir = `$HOME\\${vscodeServerConfig.serverDataFolderName}\\install`;
const installScript = `${installDir}\\${vscodeServerConfig.commit}.ps1`;
const endRegex = new RegExp(`${scriptId}: end`);
// investigate if it's possible to use `-EncodedCommand` flag
// https://devblogs.microsoft.com/powershell/invoking-powershell-with-complex-expressions-using-scriptblocks/
let command = '';
if (shell === 'powershell') {
command = `md -Force ${installDir}; echo @'\n${installServerScript}\n'@ | Set-Content ${installScript}; powershell -ExecutionPolicy ByPass -File "${installScript}"`;
} else if (shell === 'bash') {
command = `mkdir -p ${installDir.replace(/\\/g, '/')} && echo '\n${installServerScript.replace(/'/g, '\'"\'"\'')}\n' > ${installScript.replace(/\\/g, '/')} && powershell -ExecutionPolicy ByPass -File "${installScript}"`;
} else if (shell === 'cmd') {
const script = installServerScript.trim()
// remove comments
.replace(/^#.*$/gm, '')
// remove empty lines
.replace(/\n{2,}/gm, '\n')
// remove leading spaces
.replace(/^\s*/gm, '')
// escape double quotes (from powershell/cmd)
.replace(/"/g, '"""')
// escape single quotes (from cmd)
.replace(/'/g, `''`)
// escape redirect (from cmd)
.replace(/>/g, `^>`)
// escape new lines (from powershell/cmd)
.replace(/\n/g, '\'`n\'');
const installDir = `$HOME\\${vscodeServerConfig.serverDataFolderName}\\install`;
const installScript = `${installDir}\\${vscodeServerConfig.commit}.ps1`;
const endRegex = new RegExp(`${scriptId}: end`);
// investigate if it's possible to use `-EncodedCommand` flag
// https://devblogs.microsoft.com/powershell/invoking-powershell-with-complex-expressions-using-scriptblocks/
let command = '';
if (shell === 'powershell') {
command = `md -Force ${installDir}; echo @'\n${installServerScript}\n'@ | Set-Content ${installScript}; powershell -ExecutionPolicy ByPass -File "${installScript}"`;
} else if (shell === 'bash') {
command = `mkdir -p ${installDir.replace(/\\/g, '/')} && echo '\n${installServerScript.replace(/'/g, '\'"\'"\'')}\n' > ${installScript.replace(/\\/g, '/')} && powershell -ExecutionPolicy ByPass -File "${installScript}"`;
} else if (shell === 'cmd') {
const script = installServerScript.trim()
// remove comments
.replace(/^#.*$/gm, '')
// remove empty lines
.replace(/\n{2,}/gm, '\n')
// remove leading spaces
.replace(/^\s*/gm, '')
// escape double quotes (from powershell/cmd)
.replace(/"/g, '"""')
// escape single quotes (from cmd)
.replace(/'/g, `''`)
// escape redirect (from cmd)
.replace(/>/g, `^>`)
// escape new lines (from powershell/cmd)
.replace(/\n/g, '\'`n\'');
command = `powershell "md -Force ${installDir}" && powershell "echo '${script}'" > ${installScript.replace('$HOME', '%USERPROFILE%')} && powershell -ExecutionPolicy ByPass -File "${installScript.replace('$HOME', '%USERPROFILE%')}"`;
command = `powershell "md -Force ${installDir}" && powershell "echo '${script}'" > ${installScript.replace('$HOME', '%USERPROFILE%')} && powershell -ExecutionPolicy ByPass -File "${installScript.replace('$HOME', '%USERPROFILE%')}"`;
logger.trace('Command length (8191 max):', command.length);
logger.trace('Command length (8191 max):', command.length);
if (command.length > 8191) {
throw new ServerInstallError(`Command line too long`);
}
} else {
throw new ServerInstallError(`Not supported shell: ${shell}`);
}
if (command.length > 8191) {
throw new ServerInstallError(`Command line too long`);
}
} else {
throw new ServerInstallError(`Not supported shell: ${shell}`);
}
commandOutput = await conn.execPartial(command, (stdout: string) => endRegex.test(stdout));
} else {
const installServerScript = generateBashInstallScript(installOptions);
commandOutput = await conn.execPartial(command, (stdout: string) => endRegex.test(stdout));
} else {
const installServerScript = generateBashInstallScript(installOptions);
logger.trace('Server install command:', installServerScript);
// 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
commandOutput = await conn.exec(`bash -c '${installServerScript.replace(/'/g, `'\\''`)}'`);
}
logger.trace('Server install command:', installServerScript);
// 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
commandOutput = await conn.exec(`bash -c '${installServerScript.replace(/'/g, `'\\''`)}'`);
}
if (commandOutput.stderr) {
logger.trace('Server install command stderr:', commandOutput.stderr);
}
logger.trace('Server install command stdout:', commandOutput.stdout);
if (commandOutput.stderr) {
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 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 vscode server on remote server, install script returned non-zero exit status`);
}
const exitCode = parseInt(resultMap.exitCode, 10);
if (exitCode !== 0) {
throw new ServerInstallError(`Couldn't install vscode server on remote server, install script returned non-zero exit status`);
}
const listeningOn = resultMap.listeningOn.match(/^\d+$/)
? parseInt(resultMap.listeningOn, 10)
: resultMap.listeningOn;
const listeningOn = resultMap.listeningOn.match(/^\d+$/)
? parseInt(resultMap.listeningOn, 10)
: resultMap.listeningOn;
const remoteEnvVars = Object.fromEntries(Object.entries(resultMap).filter(([key,]) => envVariables.includes(key)));
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
};
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 startResultStr = `${scriptId}: start`;
const endResultStr = `${scriptId}: end`;
const startResultIdx = str.indexOf(startResultStr);
if (startResultIdx < 0) {
return undefined;
}
const startResultIdx = str.indexOf(startResultStr);
if (startResultIdx < 0) {
return undefined;
}
const endResultIdx = str.indexOf(endResultStr, startResultIdx + startResultStr.length);
if (endResultIdx < 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 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;
}
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;
return resultMap;
}
function generateBashInstallScript({ id, quality, version, commit, release, extensionIds, envVariables, useSocketPath, serverApplicationName, serverDataFolderName, serverDownloadUrlTemplate }: ServerInstallOptions) {
const extensions = extensionIds.map(id => '--install-extension ' + id).join(' ');
return `
const extensions = extensionIds.map(id => '--install-extension ' + id).join(' ');
return `
# Server installation script
TMP_DIR="\${XDG_RUNTIME_DIR:-"/tmp"}"
@@ -427,16 +427,16 @@ print_install_results_and_exit 0
}
function generatePowerShellInstallScript({ id, quality, version, commit, release, extensionIds, envVariables, useSocketPath, serverApplicationName, serverDataFolderName, serverDownloadUrlTemplate }: ServerInstallOptions) {
const extensions = extensionIds.map(id => '--install-extension ' + id).join(' ');
const downloadUrl = serverDownloadUrlTemplate
.replace(/\$\{quality\}/g, quality)
.replace(/\$\{version\}/g, version)
.replace(/\$\{commit\}/g, commit)
.replace(/\$\{os\}/g, 'win32')
.replace(/\$\{arch\}/g, 'x64')
.replace(/\$\{release\}/g, release ?? '');
const extensions = extensionIds.map(id => '--install-extension ' + id).join(' ');
const downloadUrl = serverDownloadUrlTemplate
.replace(/\$\{quality\}/g, quality)
.replace(/\$\{version\}/g, version)
.replace(/\$\{commit\}/g, commit)
.replace(/\$\{os\}/g, 'win32')
.replace(/\$\{arch\}/g, 'x64')
.replace(/\$\{release\}/g, release ?? '');
return `
return `
# Server installation script
$TMP_DIR="$env:TEMP\\$([System.IO.Path]::GetRandomFileName())"
+1 -1
View File
@@ -38,7 +38,7 @@
"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}/void-reh-${os}-${arch}-${version}.tar.gz"
"default": "https://github.com/voideditor/binaries/releases/download/${version}.${release}/void-reh-${os}-${arch}-${version}.${release}.tar.gz"
}
}
},
@@ -39,7 +39,7 @@ export class ServerInstallError extends Error {
}
}
const DEFAULT_DOWNLOAD_URL_TEMPLATE = 'https://github.com/voideditor/binaries/releases/download/${version}/void-reh-${os}-${arch}-${version}.tar.gz';
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');
+3946 -7880
View File
File diff suppressed because it is too large Load Diff
+1 -3
View File
@@ -73,10 +73,9 @@
},
"dependencies": {
"@anthropic-ai/sdk": "^0.40.0",
"@azure/identity": "^4.9.1",
"@c4312/eventsource-umd": "^3.0.5",
"@floating-ui/react": "^0.27.8",
"@google/genai": "^0.13.0",
"@google/generative-ai": "^0.24.0",
"@microsoft/1ds-core-js": "^3.2.13",
"@microsoft/1ds-post-js": "^3.2.13",
"@mistralai/mistralai": "^1.6.0",
@@ -110,7 +109,6 @@
"cross-spawn": "^7.0.6",
"diff": "^7.0.0",
"eslint-plugin-react": "^7.37.5",
"google-auth-library": "^9.15.1",
"groq-sdk": "^0.20.1",
"http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^7.0.2",
+2 -5
View File
@@ -1,8 +1,7 @@
{
"nameShort": "Void",
"nameLong": "Void",
"voidVersion": "1.3.7",
"voidRelease": "0031",
"voidVersion": "1.2.8",
"applicationName": "void",
"dataFolderName": ".void-editor",
"win32MutexName": "voideditor",
@@ -39,8 +38,6 @@
"builtInExtensions": [],
"linkProtectionTrustedDomains": [
"https://voideditor.com",
"https://voideditor.dev",
"https://github.com/voideditor/void",
"https://ollama.com"
"https://voideditor.dev"
]
}
@@ -4,23 +4,3 @@
export const VOID_CTRL_L_ACTION_ID = 'void.ctrlLAction'
export const VOID_CTRL_K_ACTION_ID = 'void.ctrlKAction'
export const VOID_ACCEPT_DIFF_ACTION_ID = 'void.acceptDiff'
export const VOID_REJECT_DIFF_ACTION_ID = 'void.rejectDiff'
export const VOID_GOTO_NEXT_DIFF_ACTION_ID = 'void.goToNextDiff'
export const VOID_GOTO_PREV_DIFF_ACTION_ID = 'void.goToPrevDiff'
export const VOID_GOTO_NEXT_URI_ACTION_ID = 'void.goToNextUri'
export const VOID_GOTO_PREV_URI_ACTION_ID = 'void.goToPrevUri'
export const VOID_ACCEPT_FILE_ACTION_ID = 'void.acceptFile'
export const VOID_REJECT_FILE_ACTION_ID = 'void.rejectFile'
export const VOID_ACCEPT_ALL_DIFFS_ACTION_ID = 'void.acceptAllDiffs'
export const VOID_REJECT_ALL_DIFFS_ACTION_ID = 'void.rejectAllDiffs'
@@ -790,7 +790,6 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
console.log('starting autocomplete...', predictionType)
const featureName: FeatureName = 'Autocomplete'
const overridesOfModel = this._settingsService.state.overridesOfModel
const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName]
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] : undefined
@@ -808,7 +807,6 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
}),
modelSelection,
modelSelectionOptions,
overridesOfModel,
logging: { loggingName: 'Autocomplete' },
onText: () => { }, // unused in FIMMessage
// onText: async ({ fullText, newText }) => {
@@ -12,7 +12,7 @@ import { URI } from '../../../../base/common/uri.js';
import { Emitter, Event } from '../../../../base/common/event.js';
import { ILLMMessageService } from '../common/sendLLMMessageService.js';
import { chat_userMessageContent, ToolName, } from '../common/prompt/prompts.js';
import { AnthropicReasoning, getErrorMessage, RawToolCallObj, RawToolParamsObj } from '../common/sendLLMMessageTypes.js';
import { getErrorMessage, RawToolCallObj, RawToolParamsObj } from '../common/sendLLMMessageTypes.js';
import { generateUuid } from '../../../../base/common/uuid.js';
import { FeatureName, ModelSelection, ModelSelectionOptions } from '../common/voidSettingsTypes.js';
import { IVoidSettingsService } from '../common/voidSettingsService.js';
@@ -35,8 +35,6 @@ import { IConvertToLLMMessageService } from './convertToLLMMessageService.js';
import { timeout } from '../../../../base/common/async.js';
import { deepClone } from '../../../../base/common/objects.js';
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
import { IDirectoryStrService } from '../common/directoryStrService.js';
import { IFileService } from '../../../../platform/files/common/files.js';
// related to retrying when LLM message has error
@@ -102,13 +100,6 @@ const defaultMessageState: UserMessageState = {
// a 'thread' means a chat message history
type WhenMounted = {
textAreaRef: { current: HTMLTextAreaElement | null }; // the textarea that this thread has, gets set in SidebarChat
scrollToBottom: () => void;
}
export type ThreadType = {
id: string; // store the id here too
createdAt: string; // ISO string
@@ -129,15 +120,6 @@ export type ThreadType = {
[codespanName: string]: CodespanLocationLink
}
}
mountedInfo?: {
whenMounted: Promise<WhenMounted>
_whenMountedResolver: (res: WhenMounted) => void
mountedIsResolvedRef: { current: boolean };
}
};
}
@@ -198,7 +180,7 @@ export type ThreadStreamState = {
error?: undefined;
llmInfo?: undefined;
toolInfo?: undefined;
interrupt: 'not_needed' | Promise<() => void>; // calling this should have no effect on state - would be too confusing. it just cancels the tool
interrupt?: undefined;
}
}
@@ -253,7 +235,6 @@ export interface IChatThreadService {
isCurrentlyFocusingMessage(): boolean;
setCurrentlyFocusedMessageIdx(messageIdx: number | undefined): void;
popStagingSelections(numPops?: number): void;
addNewStagingSelection(newSelection: StagingSelectionItem): void;
dangerousSetState: (newState: ThreadsState) => void;
@@ -285,9 +266,6 @@ export interface IChatThreadService {
// jump to history
jumpToCheckpointBeforeMessageIdx(opts: { threadId: string, messageIdx: number, jumpToUserModified: boolean }): void;
focusCurrentChat: () => Promise<void>
blurCurrentChat: () => Promise<void>
}
export const IChatThreadService = createDecorator<IChatThreadService>('voidChatThreadService');
@@ -321,8 +299,6 @@ class ChatThreadService extends Disposable implements IChatThreadService {
@INotificationService private readonly _notificationService: INotificationService,
@IConvertToLLMMessageService private readonly _convertToLLMMessagesService: IConvertToLLMMessageService,
@IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService,
@IDirectoryStrService private readonly _directoryStringService: IDirectoryStrService,
@IFileService private readonly _fileService: IFileService,
) {
super()
this.state = { allThreads: {}, currentThreadId: null as unknown as string } // default state
@@ -356,26 +332,6 @@ class ChatThreadService extends Disposable implements IChatThreadService {
}
async focusCurrentChat() {
const threadId = this.state.currentThreadId
const thread = this.state.allThreads[threadId]
if (!thread) return
const s = await thread.state.mountedInfo?.whenMounted
if (!this.isCurrentlyFocusingMessage()) {
s?.textAreaRef.current?.focus()
}
}
async blurCurrentChat() {
const threadId = this.state.currentThreadId
const thread = this.state.allThreads[threadId]
if (!thread) return
const s = await thread.state.mountedInfo?.whenMounted
if (!this.isCurrentlyFocusingMessage()) {
s?.textAreaRef.current?.blur()
}
}
dangerousSetState = (newState: ThreadsState) => {
this.state = newState
@@ -420,7 +376,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
// this should be the only place this.state = ... appears besides constructor
private _setState(state: Partial<ThreadsState>, doNotRefreshMountInfo?: boolean) {
private _setState(state: Partial<ThreadsState>, affectsCurrent: boolean) {
const newState = {
...this.state,
...state
@@ -428,7 +384,8 @@ class ChatThreadService extends Disposable implements IChatThreadService {
this.state = newState
this._onDidChangeCurrentThread.fire()
if (affectsCurrent)
this._onDidChangeCurrentThread.fire()
// if we just switched to a thread, update its current stream state if it's not streaming to possibly streaming
@@ -450,27 +407,6 @@ class ChatThreadService extends Disposable implements IChatThreadService {
}
// if we did not just set the state to true, set mount info
if (doNotRefreshMountInfo) return
let whenMountedResolver: (w: WhenMounted) => void
const whenMountedPromise = new Promise<WhenMounted>((res) => whenMountedResolver = res)
this._setThreadState(threadId, {
mountedInfo: {
whenMounted: whenMountedPromise,
mountedIsResolvedRef: { current: false },
_whenMountedResolver: (w: WhenMounted) => {
whenMountedResolver(w)
const mountInfo = this.state.allThreads[threadId]?.state.mountedInfo
if (mountInfo) mountInfo.mountedIsResolvedRef.current = true
},
}
}, true) // do not trigger an update
}
@@ -540,7 +476,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
const { name, id, rawParams } = lastMsg
const errorMessage = this.toolErrMsgs.rejected
const errorMessage = this.errMsgs.rejected
this._updateLatestTool(threadId, { role: 'tool', type: 'rejected', params: params, name: name, content: errorMessage, result: null, id, rawParams })
this._setStreamState(threadId, undefined)
}
@@ -549,42 +485,34 @@ class ChatThreadService extends Disposable implements IChatThreadService {
const thread = this.state.allThreads[threadId]
if (!thread) return // should never happen
// interrupt any effects
const interrupt = await this.streamState[threadId]?.interrupt
interrupt?.()
// add assistant message
if (this.streamState[threadId]?.isRunning === 'LLM') {
const { displayContentSoFar, reasoningSoFar, toolCallSoFar } = this.streamState[threadId].llmInfo
this._addMessageToThread(threadId, { role: 'assistant', displayContent: displayContentSoFar, reasoning: reasoningSoFar, anthropicReasoning: null })
if (toolCallSoFar) this._addMessageToThread(threadId, { role: 'interrupted_streaming_tool', name: toolCallSoFar.name })
this._addUserCheckpoint({ threadId })
}
// add tool that's running
else if (this.streamState[threadId]?.isRunning === 'tool') {
const { toolName, toolParams, id, content: content_, rawParams } = this.streamState[threadId].toolInfo
const content = content_ || this.toolErrMsgs.interrupted
const { toolName, toolParams, id, content, rawParams } = this.streamState[threadId].toolInfo
this._updateLatestTool(threadId, { role: 'tool', name: toolName, params: toolParams, id, content, rawParams, type: 'rejected', result: null })
}
// reject the tool for the user if relevant
else if (this.streamState[threadId]?.isRunning === 'awaiting_user') {
this.rejectLatestToolRequest(threadId)
}
else if (this.streamState[threadId]?.isRunning === 'idle') {
// do nothing
}
this._addUserCheckpoint({ threadId })
// interrupt any effects
const interrupt = await this.streamState[threadId]?.interrupt
if (typeof interrupt === 'function')
interrupt()
this._setStreamState(threadId, undefined)
}
private readonly toolErrMsgs = {
private readonly errMsgs = {
rejected: 'Tool call was rejected by the user.',
interrupted: 'Tool call was interrupted by the user.',
errWhenStringifying: (error: any) => `Tool call succeeded, but there was an error stringifying the output.\n${getErrorMessage(error)}`
}
@@ -668,12 +596,15 @@ class ChatThreadService extends Disposable implements IChatThreadService {
this._updateLatestTool(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, id: toolId, rawParams: opts.unvalidatedToolParams })
return {}
}
finally {
this._setStreamState(threadId, undefined)
}
// 4. stringify the result to give to the LLM
try {
toolResultStr = this._toolsService.stringOfResult[toolName](toolParams as any, toolResult as any)
} catch (error) {
const errorMessage = this.toolErrMsgs.errWhenStringifying(error)
const errorMessage = this.errMsgs.errWhenStringifying(error)
this._updateLatestTool(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, id: toolId, rawParams: opts.unvalidatedToolParams })
return {}
}
@@ -700,13 +631,11 @@ class ChatThreadService extends Disposable implements IChatThreadService {
}) {
let interruptedWhenIdle = false
const idleInterruptor = Promise.resolve(() => { interruptedWhenIdle = true })
// _runToolCall does not need setStreamState({idle}) before it, but it needs it after it. (handles its own setStreamState)
// above just defines helpers, below starts the actual function
const { chatMode } = this._settingsService.state.globalSettings // should not change as we loop even if user changes it, so it goes here
const { overridesOfModel } = this._settingsService.state
// not running at start, clear state
this._setStreamState(threadId, { isRunning: 'idle' })
let nMessagesSent = 0
let shouldSendAnotherMessage = true
@@ -715,14 +644,9 @@ class ChatThreadService extends Disposable implements IChatThreadService {
// before enter loop, call tool
if (callThisToolFirst) {
const { interrupted } = await this._runToolCall(threadId, callThisToolFirst.name, callThisToolFirst.id, { preapproved: true, unvalidatedToolParams: callThisToolFirst.rawParams, validatedParams: callThisToolFirst.params })
if (interrupted) {
this._setStreamState(threadId, undefined)
this._addUserCheckpoint({ threadId })
}
this._setStreamState(threadId, undefined)
if (interrupted) { return }
}
this._setStreamState(threadId, { isRunning: 'idle', interrupt: 'not_needed' }) // just decorative, for clarity
// tool use loop
while (shouldSendAnotherMessage) {
@@ -731,8 +655,6 @@ class ChatThreadService extends Disposable implements IChatThreadService {
isRunningWhenEnd = undefined
nMessagesSent += 1
this._setStreamState(threadId, { isRunning: 'idle', interrupt: idleInterruptor })
const chatMessages = this.state.allThreads[threadId]?.messages ?? []
const { messages, separateSystemMessage } = await this._convertToLLMMessagesService.prepareLLMChatMessages({
chatMessages,
@@ -740,46 +662,63 @@ class ChatThreadService extends Disposable implements IChatThreadService {
chatMode
})
if (interruptedWhenIdle) {
this._setStreamState(threadId, undefined)
return
}
let shouldRetryLLM = true
let nAttempts = 0
while (shouldRetryLLM) {
if (this.streamState[threadId]?.isRunning) {
// if already streaming, stop
console.log('returning...', this.streamState[threadId])
return
}
shouldRetryLLM = false
nAttempts += 1
type ResTypes =
| { type: 'llmDone', toolCall?: RawToolCallObj, info: { fullText: string, fullReasoning: string, anthropicReasoning: AnthropicReasoning[] | null } }
| { type: 'llmError', error?: { message: string; fullError: Error | null; } }
| { type: 'llmAborted' }
let resMessageIsDonePromise: (res: ResTypes) => void // resolves when user approves this tool use (or if tool doesn't require approval)
const messageIsDonePromise = new Promise<ResTypes>((res, rej) => { resMessageIsDonePromise = res })
let resMessageIsDonePromise: (toolCall?: RawToolCallObj | undefined) => void // resolves when user approves this tool use (or if tool doesn't require approval)
const messageIsDonePromise = new Promise<RawToolCallObj | undefined>((res, rej) => { resMessageIsDonePromise = res })
let aborted = false
const llmCancelToken = this._llmMessageService.sendLLMMessage({
messagesType: 'chatMessages',
chatMode,
messages: messages,
modelSelection,
modelSelectionOptions,
overridesOfModel,
logging: { loggingName: `Chat - ${chatMode}`, loggingExtras: { threadId, nMessagesSent, chatMode } },
separateSystemMessage: separateSystemMessage,
onText: ({ fullText, fullReasoning, toolCall }) => {
this._setStreamState(threadId, { isRunning: 'LLM', llmInfo: { displayContentSoFar: fullText, reasoningSoFar: fullReasoning, toolCallSoFar: toolCall ?? null }, interrupt: Promise.resolve(() => { if (llmCancelToken) this._llmMessageService.abort(llmCancelToken) }) })
},
onFinalMessage: async ({ fullText, fullReasoning, toolCall, anthropicReasoning, }) => {
resMessageIsDonePromise({ type: 'llmDone', toolCall, info: { fullText, fullReasoning, anthropicReasoning } }) // resolve with tool calls
this._addMessageToThread(threadId, { role: 'assistant', displayContent: fullText, reasoning: fullReasoning, anthropicReasoning })
this._setStreamState(threadId, undefined)
resMessageIsDonePromise(toolCall) // resolve with tool calls
},
onError: async (error) => {
resMessageIsDonePromise({ type: 'llmError', error: error })
if (this.streamState[threadId]?.isRunning !== 'LLM') {
console.log('Unexpected onError when', this.streamState[threadId]?.isRunning)
return
}
if (nAttempts < CHAT_RETRIES) {
nAttempts += 1
shouldRetryLLM = true
this._setStreamState(threadId, undefined) // clear later so can be interrupted
resMessageIsDonePromise()
}
else {
// add assistant's message to chat history, and clear selection
const { displayContentSoFar, reasoningSoFar, toolCallSoFar } = this.streamState[threadId].llmInfo
this._addMessageToThread(threadId, { role: 'assistant', displayContent: displayContentSoFar, reasoning: reasoningSoFar, anthropicReasoning: null })
if (toolCallSoFar) this._addMessageToThread(threadId, { role: 'interrupted_streaming_tool', name: toolCallSoFar.name })
this._setStreamState(threadId, { isRunning: undefined, error })
resMessageIsDonePromise()
}
},
onAbort: () => {
// stop the loop to free up the promise, but don't modify state (already handled by whatever stopped it)
resMessageIsDonePromise({ type: 'llmAborted' })
aborted = true
this._setStreamState(threadId, { isRunning: 'idle' })
resMessageIsDonePromise()
this._metricsService.capture('Agent Loop Done (Aborted)', { nMessagesSent, chatMode })
},
})
@@ -791,64 +730,31 @@ class ChatThreadService extends Disposable implements IChatThreadService {
}
this._setStreamState(threadId, { isRunning: 'LLM', llmInfo: { displayContentSoFar: '', reasoningSoFar: '', toolCallSoFar: null }, interrupt: Promise.resolve(() => this._llmMessageService.abort(llmCancelToken)) })
const llmRes = await messageIsDonePromise // wait for message to complete
const toolCall = await messageIsDonePromise // wait for message to complete
// if something else started running in the meantime
if (this.streamState[threadId]?.isRunning !== 'LLM') {
// console.log('Chat thread interrupted by a newer chat thread', this.streamState[threadId]?.isRunning)
return
}
// llm res aborted
if (llmRes.type === 'llmAborted') {
if (aborted) {
this._setStreamState(threadId, undefined)
return
}
// llm res error
else if (llmRes.type === 'llmError') {
// error, should retry
if (nAttempts < CHAT_RETRIES) {
shouldRetryLLM = true
this._setStreamState(threadId, { isRunning: 'idle', interrupt: idleInterruptor })
await timeout(RETRY_DELAY)
if (interruptedWhenIdle) {
this._setStreamState(threadId, undefined)
return
}
else
continue // retry
}
// error, but too many attempts
else {
const { error } = llmRes
const { displayContentSoFar, reasoningSoFar, toolCallSoFar } = this.streamState[threadId].llmInfo
this._addMessageToThread(threadId, { role: 'assistant', displayContent: displayContentSoFar, reasoning: reasoningSoFar, anthropicReasoning: null })
if (toolCallSoFar) this._addMessageToThread(threadId, { role: 'interrupted_streaming_tool', name: toolCallSoFar.name })
this._setStreamState(threadId, { isRunning: undefined, error })
this._addUserCheckpoint({ threadId })
return
}
if (shouldRetryLLM) {
this._setStreamState(threadId, { isRunning: 'idle' })
await timeout(RETRY_DELAY)
continue
}
// llm res success
const { toolCall, info } = llmRes
this._addMessageToThread(threadId, { role: 'assistant', displayContent: info.fullText, reasoning: info.fullReasoning, anthropicReasoning: info.anthropicReasoning })
this._setStreamState(threadId, { isRunning: 'idle', interrupt: 'not_needed' }) // just decorative for clarity
this._setStreamState(threadId, { isRunning: 'idle' })
// call tool if there is one
if (toolCall) {
const { awaitingUserApproval, interrupted } = await this._runToolCall(threadId, toolCall.name, toolCall.id, { preapproved: false, unvalidatedToolParams: toolCall.rawParams })
if (interrupted) {
this._setStreamState(threadId, undefined)
return
}
this._setStreamState(threadId, { isRunning: 'idle' })
if (awaitingUserApproval) { isRunningWhenEnd = 'awaiting_user' }
else { shouldSendAnotherMessage = true }
this._setStreamState(threadId, { isRunning: 'idle', interrupt: 'not_needed' }) // just decorative, for clarity
}
} // end while (attempts)
@@ -858,7 +764,8 @@ class ChatThreadService extends Disposable implements IChatThreadService {
this._setStreamState(threadId, { isRunning: isRunningWhenEnd })
// add checkpoint before the next user message
if (!isRunningWhenEnd) this._addUserCheckpoint({ threadId })
if (!isRunningWhenEnd)
this._addUserCheckpoint({ threadId })
// capture number of messages sent
this._metricsService.capture('Agent Loop Done', { nMessagesSent, chatMode })
@@ -894,7 +801,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
}
}
this._storeAllThreads(newThreads)
this._setState({ allThreads: newThreads }) // the current thread just changed (it had a message added to it)
this._setState({ allThreads: newThreads }, true) // the current thread just changed (it had a message added to it)
}
@@ -1162,10 +1069,7 @@ We only need to do it for files that were edited since `from`, ie files between
class: undefined,
run: () => {
this.switchToThread(threadId)
// scroll to bottom
this.state.allThreads[threadId]?.state.mountedInfo?.whenMounted.then(m => {
m.scrollToBottom()
})
// TODO!!! scroll to bottom
}
}]
},
@@ -1191,6 +1095,7 @@ We only need to do it for files that were edited since `from`, ie files between
// interrupt existing stream
if (this.streamState[threadId]?.isRunning) {
console.log('stopping....')
await this.abortRunning(threadId)
}
@@ -1199,12 +1104,14 @@ We only need to do it for files that were edited since `from`, ie files between
this._addUserCheckpoint({ threadId })
}
const { chatMode } = this._settingsService.state.globalSettings
// add user's message to chat history
const instructions = userMessage
const currSelns: StagingSelectionItem[] = _chatSelections ?? thread.state.stagingSelections
const opts = chatMode !== 'normal' ? { type: 'references' } as const : { type: 'fullCode', voidModelService: this._voidModelService } as const
const userMessageContent = await chat_userMessageContent(instructions, currSelns, { directoryStrService: this._directoryStringService, fileService: this._fileService }) // user message + names of files (NOT content)
const userMessageContent = await chat_userMessageContent(instructions, currSelns, opts) // user message + names of files (NOT content)
const userHistoryElt: ChatMessage = { role: 'user', content: userMessageContent, displayContent: instructions, selections: currSelns, state: defaultMessageState }
this._addMessageToThread(threadId, userHistoryElt)
@@ -1214,11 +1121,6 @@ We only need to do it for files that were edited since `from`, ie files between
this._runChatAgent({ threadId, ...this._currentModelSelectionProps(), }),
threadId,
)
// scroll to bottom
this.state.allThreads[threadId]?.state.mountedInfo?.whenMounted.then(m => {
m.scrollToBottom()
})
}
@@ -1241,7 +1143,7 @@ We only need to do it for files that were edited since `from`, ie files between
}
};
this._storeAllThreads(newThreads);
this._setState({ allThreads: newThreads });
this._setState({ allThreads: newThreads }, true);
}
// Now call the original method to add the user message and stream the response
@@ -1271,7 +1173,7 @@ We only need to do it for files that were edited since `from`, ie files between
messages: slicedMessages
}
}
})
}, true)
// re-add the message and stream it
this._addUserMessageAndStreamResponse({ userMessage, _chatSelections: currSelns, threadId })
@@ -1544,7 +1446,7 @@ We only need to do it for files that were edited since `from`, ie files between
}
}
})
}, true)
}
@@ -1575,7 +1477,7 @@ We only need to do it for files that were edited since `from`, ie files between
}
switchToThread(threadId: string) {
this._setState({ currentThreadId: threadId })
this._setState({ currentThreadId: threadId }, true)
}
@@ -1598,7 +1500,7 @@ We only need to do it for files that were edited since `from`, ie files between
[newThread.id]: newThread
}
this._storeAllThreads(newThreads)
this._setState({ allThreads: newThreads, currentThreadId: newThread.id })
this._setState({ allThreads: newThreads, currentThreadId: newThread.id }, true)
}
@@ -1611,7 +1513,7 @@ We only need to do it for files that were edited since `from`, ie files between
// store the updated threads
this._storeAllThreads(newThreads);
this._setState({ ...this.state, allThreads: newThreads })
this._setState({ ...this.state, allThreads: newThreads }, true)
}
duplicateThread(threadId: string) {
@@ -1627,7 +1529,7 @@ We only need to do it for files that were edited since `from`, ie files between
[newThread.id]: newThread,
}
this._storeAllThreads(newThreads)
this._setState({ allThreads: newThreads })
this._setState({ allThreads: newThreads }, true)
}
@@ -1648,7 +1550,7 @@ We only need to do it for files that were edited since `from`, ie files between
}
}
this._storeAllThreads(newThreads)
this._setState({ allThreads: newThreads }) // the current thread just changed (it had a message added to it)
this._setState({ allThreads: newThreads }, true) // the current thread just changed (it had a message added to it)
}
// sets the currently selected message (must be undefined if no message is selected)
@@ -1669,7 +1571,7 @@ We only need to do it for files that were edited since `from`, ie files between
}
}
}
})
}, true)
// // when change focused message idx, jump - do not jump back when click edit, too confusing.
// if (messageIdx !== undefined)
@@ -1709,31 +1611,6 @@ We only need to do it for files that were edited since `from`, ie files between
}
// Pops the staging selections from the current thread's state
popStagingSelections(numPops: number): void {
numPops = numPops ?? 1;
const focusedMessageIdx = this.getCurrentFocusedMessageIdx()
// set the selections to the proper value
let selections: StagingSelectionItem[] = []
let setSelections = (s: StagingSelectionItem[]) => { }
if (focusedMessageIdx === undefined) {
selections = this.getCurrentThreadState().stagingSelections
setSelections = (s: StagingSelectionItem[]) => this.setCurrentThreadState({ stagingSelections: s })
} else {
selections = this.getCurrentMessageState(focusedMessageIdx).stagingSelections
setSelections = (s) => this.setCurrentMessageState(focusedMessageIdx, { stagingSelections: s })
}
setSelections([
...selections.slice(0, selections.length - numPops)
])
}
// set message.state
private _setCurrentMessageState(state: Partial<UserMessageState>, messageIdx: number): void {
@@ -1757,12 +1634,12 @@ We only need to do it for files that were edited since `from`, ie files between
)
}
}
})
}, true)
}
// set thread.state
private _setThreadState(threadId: string, state: Partial<ThreadType['state']>, doNotRefreshMountInfo?: boolean): void {
private _setThreadState(threadId: string, state: Partial<ThreadType['state']>): void {
const thread = this.state.allThreads[threadId]
if (!thread) return
@@ -1777,7 +1654,7 @@ We only need to do it for files that were edited since `from`, ie files between
}
}
}
}, doNotRefreshMountInfo)
}, true)
}
@@ -6,18 +6,17 @@ import { createDecorator } from '../../../../platform/instantiation/common/insta
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
import { IEditorService } from '../../../services/editor/common/editorService.js';
import { ChatMessage } from '../common/chatThreadServiceTypes.js';
import { getIsReasoningEnabledState, getReservedOutputTokenSpace, getModelCapabilities } from '../common/modelCapabilities.js';
import { getIsReasoningEnabledState, getMaxOutputTokens, getModelCapabilities } from '../common/modelCapabilities.js';
import { reParsedToolXMLString, chat_systemMessage, ToolName } from '../common/prompt/prompts.js';
import { AnthropicLLMChatMessage, AnthropicReasoning, GeminiLLMChatMessage, LLMChatMessage, LLMFIMMessage, OpenAILLMChatMessage, RawToolParamsObj } from '../common/sendLLMMessageTypes.js';
import { AnthropicLLMChatMessage, AnthropicReasoning, LLMChatMessage, LLMFIMMessage, OpenAILLMChatMessage, RawToolParamsObj } from '../common/sendLLMMessageTypes.js';
import { IVoidSettingsService } from '../common/voidSettingsService.js';
import { ChatMode, FeatureName, ModelSelection, ProviderName } from '../common/voidSettingsTypes.js';
import { IDirectoryStrService } from '../common/directoryStrService.js';
import { ChatMode, FeatureName, ModelSelection } from '../common/voidSettingsTypes.js';
import { IDirectoryStrService } from './directoryStrService.js';
import { ITerminalToolService } from './terminalToolService.js';
import { IVoidModelService } from '../common/voidModelService.js';
import { URI } from '../../../../base/common/uri.js';
import { EndOfLinePreference } from '../../../../editor/common/model.js';
export const EMPTY_MESSAGE = '(empty message)'
@@ -38,8 +37,11 @@ type SimpleLLMMessage = {
const CHARS_PER_TOKEN = 4 // assume abysmal chars per token
const TRIM_TO_LEN = 120
const EMPTY_MESSAGE = '(empty message)'
const CHARS_PER_TOKEN = 4
const TRIM_TO_LEN = 60
@@ -67,7 +69,7 @@ openai on developer system message - https://cdn.openai.com/spec/model-spec-2024
*/
const prepareMessages_openai_tools = (messages: SimpleLLMMessage[]): AnthropicOrOpenAILLMMessage[] => {
const prepareMessages_openai_tools = (messages: SimpleLLMMessage[]): LLMChatMessage[] => {
const newMessages: OpenAILLMChatMessage[] = [];
@@ -134,9 +136,8 @@ assistant: ...content, call(name, id, params)
user: ...content, result(id, content)
*/
type AnthropicOrOpenAILLMMessage = AnthropicLLMChatMessage | OpenAILLMChatMessage
const prepareMessages_anthropic_tools = (messages: SimpleLLMMessage[], supportsAnthropicReasoning: boolean): AnthropicOrOpenAILLMMessage[] => {
const prepareMessages_anthropic_tools = (messages: SimpleLLMMessage[], supportsAnthropicReasoning: boolean): LLMChatMessage[] => {
const newMessages: (AnthropicLLMChatMessage | (SimpleLLMMessage & { role: 'tool' }))[] = messages;
for (let i = 0; i < messages.length; i += 1) {
@@ -194,9 +195,9 @@ const prepareMessages_anthropic_tools = (messages: SimpleLLMMessage[], supportsA
}
const prepareMessages_XML_tools = (messages: SimpleLLMMessage[], supportsAnthropicReasoning: boolean): AnthropicOrOpenAILLMMessage[] => {
const prepareMessages_XML_tools = (messages: SimpleLLMMessage[], supportsAnthropicReasoning: boolean): LLMChatMessage[] => {
const llmChatMessages: AnthropicOrOpenAILLMMessage[] = [];
const llmChatMessages: LLMChatMessage[] = [];
for (let i = 0; i < messages.length; i += 1) {
const c = messages[i]
@@ -205,7 +206,7 @@ const prepareMessages_XML_tools = (messages: SimpleLLMMessage[], supportsAnthrop
if (c.role === 'assistant') {
// if called a tool (message after it), re-add its XML to the message
// alternatively, could just hold onto the original output, but this way requires less piping raw strings everywhere
let content: AnthropicOrOpenAILLMMessage['content'] = c.content
let content: LLMChatMessage['content'] = c.content
if (next?.role === 'tool') {
content = `${content}\n\n${reParsedToolXMLString(next.name, next.rawParams)}`
}
@@ -237,17 +238,33 @@ const prepareMessages_XML_tools = (messages: SimpleLLMMessage[], supportsAnthrop
}
const prepareMessages_providerSpecific = (messages: SimpleLLMMessage[], specialToolFormat: 'openai-style' | 'anthropic-style' | undefined, supportsAnthropicReasoning: boolean): LLMChatMessage[] => {
const llmChatMessages: LLMChatMessage[] = []
if (!specialToolFormat) { // XML tool behavior
return prepareMessages_XML_tools(messages, supportsAnthropicReasoning)
}
else if (specialToolFormat === 'anthropic-style') {
return prepareMessages_anthropic_tools(messages, supportsAnthropicReasoning)
}
else if (specialToolFormat === 'openai-style') {
return prepareMessages_openai_tools(messages)
}
return llmChatMessages
}
// --- CHAT ---
const prepareOpenAIOrAnthropicMessages = ({
messages: messages_,
const prepareMessages = ({
messages,
systemMessage,
aiInstructions,
supportsSystemMessage,
specialToolFormat,
supportsAnthropicReasoning,
contextWindow,
reservedOutputTokenSpace,
maxOutputTokens,
}: {
messages: SimpleLLMMessage[],
systemMessage: string,
@@ -256,35 +273,20 @@ const prepareOpenAIOrAnthropicMessages = ({
specialToolFormat: 'openai-style' | 'anthropic-style' | undefined,
supportsAnthropicReasoning: boolean,
contextWindow: number,
reservedOutputTokenSpace: number | null | undefined,
}): { messages: AnthropicOrOpenAILLMMessage[], separateSystemMessage: string | undefined } => {
reservedOutputTokenSpace = Math.max(
contextWindow * 1 / 2, // reserve at least 1/4 of the token window length
reservedOutputTokenSpace ?? 4_096 // defaults to 4096
)
let messages: (SimpleLLMMessage | { role: 'system', content: string })[] = deepClone(messages_)
// ================ system message ================
// A COMPLETE HACK: last message is system message for context purposes
const sysMsgParts: string[] = []
if (aiInstructions) sysMsgParts.push(`GUIDELINES (from the user's .voidrules file):\n${aiInstructions}`)
if (systemMessage) sysMsgParts.push(systemMessage)
const combinedSystemMessage = sysMsgParts.join('\n\n')
messages.unshift({ role: 'system', content: combinedSystemMessage })
maxOutputTokens: number | null | undefined,
}): { messages: LLMChatMessage[], separateSystemMessage: string | undefined } => {
maxOutputTokens = maxOutputTokens ?? 4_096 // default to 4096
// ================ trim ================
messages = messages.map(m => ({ ...m, content: m.role !== 'tool' ? m.content.trim() : m.content }))
type MesType = (typeof messages)[0]
messages = deepClone(messages)
messages = messages.map(m => ({ ...m, content: m.role !== 'tool' ? m.content.trim() : m.content }))
// ================ fit into context ================
// the higher the weight, the higher the desire to truncate - TRIM HIGHEST WEIGHT MESSAGES
const alreadyTrimmedIdxes = new Set<number>()
const weight = (message: MesType, messages: MesType[], idx: number) => {
const weight = (message: SimpleLLMMessage, messages: SimpleLLMMessage[], idx: number) => {
const base = message.content.length
let multiplier: number
@@ -292,30 +294,22 @@ const prepareOpenAIOrAnthropicMessages = ({
if (message.role === 'user') {
multiplier *= 1
}
else if (message.role === 'system') {
multiplier *= .01 // very low weight
}
else {
multiplier *= 10 // llm tokens are far less valuable than user tokens
}
// any already modified message should not be trimmed again
if (alreadyTrimmedIdxes.has(idx)) {
multiplier = 0
}
// 1st and last messages should be very low weight
if (idx <= 1 || idx >= messages.length - 1 - 3) {
// 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_: MesType[]) => {
const _findLargestByWeight = (messages: SimpleLLMMessage[]) => {
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)
const w = weight(m, messages, i)
if (w > largestWeight) {
largestWeight = w
largestIndex = i
@@ -326,11 +320,7 @@ const prepareOpenAIOrAnthropicMessages = ({
let totalLen = 0
for (const m of messages) { totalLen += m.content.length }
const charsNeedToTrim = totalLen - Math.max(
(contextWindow - reservedOutputTokenSpace) * CHARS_PER_TOKEN, // can be 0, in which case charsNeedToTrim=everything, bad
5_000 // ensure we don't trim at least 5k chars (just a random small value)
)
const charsNeedToTrim = totalLen - (contextWindow - maxOutputTokens) * CHARS_PER_TOKEN
// <----------------------------------------->
// 0 | | |
@@ -350,54 +340,41 @@ const prepareOpenAIOrAnthropicMessages = ({
// if can finish here, do
const numCharsWillTrim = m.content.length - TRIM_TO_LEN
if (numCharsWillTrim > remainingCharsToTrim) {
// trim remainingCharsToTrim + '...'.length chars
m.content = m.content.slice(0, m.content.length - remainingCharsToTrim - '...'.length).trim() + '...'
m.content = m.content.slice(0, m.content.length - remainingCharsToTrim).trim()
break
}
remainingCharsToTrim -= numCharsWillTrim
m.content = m.content.substring(0, TRIM_TO_LEN - '...'.length) + '...'
m.content = m.content.substring(0, TRIM_TO_LEN - 3) + '...'
alreadyTrimmedIdxes.add(trimIdx)
}
// ================ system message hack ================
const newSysMsg = messages.shift()!.content
// ================ tools and anthropicReasoning ================
// SYSTEM MESSAGE HACK: we shifted (removed) the system message role, so now SimpleLLMMessage[] is valid
const llmMessages: LLMChatMessage[] = prepareMessages_providerSpecific(messages, specialToolFormat, supportsAnthropicReasoning)
let llmChatMessages: AnthropicOrOpenAILLMMessage[] = []
if (!specialToolFormat) { // XML tool behavior
llmChatMessages = prepareMessages_XML_tools(messages as SimpleLLMMessage[], supportsAnthropicReasoning)
}
else if (specialToolFormat === 'anthropic-style') {
llmChatMessages = prepareMessages_anthropic_tools(messages as SimpleLLMMessage[], supportsAnthropicReasoning)
}
else if (specialToolFormat === 'openai-style') {
llmChatMessages = prepareMessages_openai_tools(messages as SimpleLLMMessage[])
}
const llmMessages = llmChatMessages
// ================ system message concat ================
// ================ system message add as first llmMessage ================
// find system messages and concatenate them
const newSystemMessage = aiInstructions ?
`${(systemMessage ? `${systemMessage}\n\n` : '')}GUIDELINES\n${aiInstructions}`
: systemMessage
let separateSystemMessageStr: string | undefined = undefined
// if supports system message
if (supportsSystemMessage) {
if (supportsSystemMessage === 'separated')
separateSystemMessageStr = newSysMsg
separateSystemMessageStr = newSystemMessage
else if (supportsSystemMessage === 'system-role')
llmMessages.unshift({ role: 'system', content: newSysMsg }) // add new first message
llmMessages.unshift({ role: 'system', content: newSystemMessage }) // add new first message
else if (supportsSystemMessage === 'developer-role')
llmMessages.unshift({ role: 'developer', content: newSysMsg }) // add new first message
llmMessages.unshift({ role: 'developer', content: newSystemMessage }) // add new first message
}
// if does not support system message
else {
const newFirstMessage = {
role: 'user',
content: `<SYSTEM_MESSAGE>\n${newSysMsg}\n</SYSTEM_MESSAGE>\n${llmMessages[0].content}`
content: `<SYSTEM_MESSAGE>\n${newSystemMessage}\n</SYSTEM_MESSAGE>\n${llmMessages[0].content}`
} as const
llmMessages.splice(0, 1) // delete first message
llmMessages.unshift(newFirstMessage) // add new first message
@@ -405,25 +382,14 @@ const prepareOpenAIOrAnthropicMessages = ({
// ================ no empty message ================
for (let i = 0; i < llmMessages.length; i += 1) {
const currMsg: AnthropicOrOpenAILLMMessage = llmMessages[i]
const nextMsg: AnthropicOrOpenAILLMMessage | undefined = llmMessages[i + 1]
for (const currMsg of llmMessages) {
if (currMsg.role === 'tool') continue
// if content is a string, replace string with empty msg
if (typeof currMsg.content === 'string') {
if (typeof currMsg.content === 'string')
currMsg.content = currMsg.content || EMPTY_MESSAGE
}
else {
// allowed to be empty if has a tool in it or following it
if (currMsg.content.find(c => c.type === 'tool_result' || c.type === 'tool_use')) {
currMsg.content = currMsg.content.filter(c => !(c.type === 'text' && !c.text)) as any
continue
}
if (nextMsg?.role === 'tool') continue
// replace any empty text entries with empty msg, and make sure there's at least 1 entry
// if content is an array, replace any empty text entries with empty msg, and make sure there's at least 1 entry
for (const c of currMsg.content) {
if (c.type === 'text') c.text = c.text || EMPTY_MESSAGE
}
@@ -440,79 +406,9 @@ const prepareOpenAIOrAnthropicMessages = ({
type GeminiUserPart = (GeminiLLMChatMessage & { role: 'user' })['parts'][0]
type GeminiModelPart = (GeminiLLMChatMessage & { role: 'model' })['parts'][0]
const prepareGeminiMessages = (messages: AnthropicLLMChatMessage[]) => {
let latestToolName: ToolName | undefined = undefined
const messages2: GeminiLLMChatMessage[] = messages.map((m): GeminiLLMChatMessage | null => {
if (m.role === 'assistant') {
if (typeof m.content === 'string') {
return { role: 'model', parts: [{ text: m.content }] }
}
else {
const parts: GeminiModelPart[] = m.content.map((c): GeminiModelPart | null => {
if (c.type === 'text') {
return { text: c.text }
}
else if (c.type === 'tool_use') {
latestToolName = c.name as ToolName
return { functionCall: { id: c.id, name: c.name as ToolName, args: c.input } }
}
else return null
}).filter(m => !!m)
return { role: 'model', parts, }
}
}
else if (m.role === 'user') {
if (typeof m.content === 'string') {
return { role: 'user', parts: [{ text: m.content }] } satisfies GeminiLLMChatMessage
}
else {
const parts: GeminiUserPart[] = m.content.map((c): GeminiUserPart | null => {
if (c.type === 'text') {
return { text: c.text }
}
else if (c.type === 'tool_result') {
if (!latestToolName) return null
return { functionResponse: { id: c.tool_use_id, name: latestToolName, response: { output: c.content } } }
}
else return null
}).filter(m => !!m)
return { role: 'user', parts, }
}
}
else return null
}).filter(m => !!m)
return messages2
}
const prepareMessages = (params: {
messages: SimpleLLMMessage[],
systemMessage: string,
aiInstructions: string,
supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated',
specialToolFormat: 'openai-style' | 'anthropic-style' | 'gemini-style' | undefined,
supportsAnthropicReasoning: boolean,
contextWindow: number,
reservedOutputTokenSpace: number | null | undefined,
providerName: ProviderName
}): { messages: LLMChatMessage[], separateSystemMessage: string | undefined } => {
const specialFormat = params.specialToolFormat // this is just for ts stupidness
// if need to convert to gemini style of messaes, do that (treat as anthropic style, then convert to gemini style)
if (params.providerName === 'gemini' || specialFormat === 'gemini-style') {
const res = prepareOpenAIOrAnthropicMessages({ ...params, specialToolFormat: specialFormat === 'gemini-style' ? 'anthropic-style' : undefined })
const messages = res.messages as AnthropicLLMChatMessage[]
const messages2 = prepareGeminiMessages(messages)
return { messages: messages2, separateSystemMessage: res.separateSystemMessage }
}
return prepareOpenAIOrAnthropicMessages({ ...params, specialToolFormat: specialFormat })
}
@@ -522,6 +418,7 @@ export interface IConvertToLLMMessageService {
prepareLLMSimpleMessages: (opts: { simpleMessages: SimpleLLMMessage[], systemMessage: string, modelSelection: ModelSelection | null, featureName: FeatureName }) => { messages: LLMChatMessage[], separateSystemMessage: string | undefined }
prepareLLMChatMessages: (opts: { chatMessages: ChatMessage[], chatMode: ChatMode, modelSelection: ModelSelection | null }) => Promise<{ messages: LLMChatMessage[], separateSystemMessage: string | undefined }>
prepareFIMMessage(opts: { messages: LLMFIMMessage, }): { prefix: string, suffix: string, stopTokens: string[] }
}
export const IConvertToLLMMessageService = createDecorator<IConvertToLLMMessageService>('ConvertToLLMMessageService');
@@ -573,7 +470,7 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess
// system message
private _generateChatMessagesSystemMessage = async (chatMode: ChatMode, specialToolFormat: 'openai-style' | 'anthropic-style' | 'gemini-style' | undefined) => {
private _generateChatMessagesSystemMessage = async (chatMode: ChatMode, specialToolFormat: 'openai-style' | 'anthropic-style' | undefined) => {
const workspaceFolders = this.workspaceContextService.getWorkspace().folders.map(f => f.uri.fsPath)
const openedURIs = this.modelService.getModels().filter(m => m.isAttachedToEditor()).map(m => m.uri.fsPath) || [];
@@ -584,7 +481,6 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess
`...Directories string cut off, use tools to read more...`
: `...Directories string cut off, ask user for more if necessary...`
})
const includeXMLToolDefinitions = !specialToolFormat
const persistentTerminalIDs = this.terminalToolService.listPersistentTerminalIds()
@@ -631,23 +527,20 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess
prepareLLMSimpleMessages: IConvertToLLMMessageService['prepareLLMSimpleMessages'] = ({ simpleMessages, systemMessage, modelSelection, featureName }) => {
if (modelSelection === null) return { messages: [], separateSystemMessage: undefined }
const { overridesOfModel } = this.voidSettingsService.state
const { providerName, modelName } = modelSelection
const {
specialToolFormat,
contextWindow,
supportsSystemMessage,
} = getModelCapabilities(providerName, modelName, overridesOfModel)
} = getModelCapabilities(providerName, modelName)
const modelSelectionOptions = this.voidSettingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName]
// Get combined AI instructions
const aiInstructions = this._getCombinedAIInstructions();
const isReasoningEnabled = getIsReasoningEnabledState(featureName, providerName, modelName, modelSelectionOptions, overridesOfModel)
const reservedOutputTokenSpace = getReservedOutputTokenSpace(providerName, modelName, { isReasoningEnabled, overridesOfModel })
const isReasoningEnabled = getIsReasoningEnabledState(featureName, providerName, modelName, modelSelectionOptions)
const maxOutputTokens = getMaxOutputTokens(providerName, modelName, { isReasoningEnabled })
const { messages, separateSystemMessage } = prepareMessages({
messages: simpleMessages,
@@ -657,22 +550,18 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess
specialToolFormat,
supportsAnthropicReasoning: providerName === 'anthropic',
contextWindow,
reservedOutputTokenSpace,
providerName,
maxOutputTokens,
})
return { messages, separateSystemMessage };
}
prepareLLMChatMessages: IConvertToLLMMessageService['prepareLLMChatMessages'] = async ({ chatMessages, chatMode, modelSelection }) => {
if (modelSelection === null) return { messages: [], separateSystemMessage: undefined }
const { overridesOfModel } = this.voidSettingsService.state
const { providerName, modelName } = modelSelection
const {
specialToolFormat,
contextWindow,
supportsSystemMessage,
} = getModelCapabilities(providerName, modelName, overridesOfModel)
} = getModelCapabilities(providerName, modelName)
const systemMessage = await this._generateChatMessagesSystemMessage(chatMode, specialToolFormat)
const modelSelectionOptions = this.voidSettingsService.state.optionsOfModelSelection['Chat'][modelSelection.providerName]?.[modelSelection.modelName]
@@ -680,8 +569,8 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess
// Get combined AI instructions
const aiInstructions = this._getCombinedAIInstructions();
const isReasoningEnabled = getIsReasoningEnabledState('Chat', providerName, modelName, modelSelectionOptions, overridesOfModel)
const reservedOutputTokenSpace = getReservedOutputTokenSpace(providerName, modelName, { isReasoningEnabled, overridesOfModel })
const isReasoningEnabled = getIsReasoningEnabledState('Chat', providerName, modelName, modelSelectionOptions)
const maxOutputTokens = getMaxOutputTokens(providerName, modelName, { isReasoningEnabled })
const llmMessages = this._chatMessagesToSimpleMessages(chatMessages)
const { messages, separateSystemMessage } = prepareMessages({
@@ -692,8 +581,7 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess
specialToolFormat,
supportsAnthropicReasoning: providerName === 'anthropic',
contextWindow,
reservedOutputTokenSpace,
providerName,
maxOutputTokens,
})
return { messages, separateSystemMessage };
}
@@ -7,28 +7,29 @@ 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, IFileStat } from '../../../../platform/files/common/files.js';
import { IFileService } from '../../../../platform/files/common/files.js';
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
import { ShallowDirectoryItem, ToolCallParams, ToolResultType } from './toolsServiceTypes.js';
import { MAX_CHILDREN_URIs_PAGE, MAX_DIRSTR_CHARS_TOTAL_BEGINNING, MAX_DIRSTR_CHARS_TOTAL_TOOL } from './prompt/prompts.js';
import { ShallowDirectoryItem, ToolCallParams, ToolResultType } from '../common/toolsServiceTypes.js';
import { IExplorerService } from '../../files/browser/files.js';
import { SortOrder } from '../../files/common/files.js';
import { ExplorerItem } from '../../files/common/explorerModel.js';
import { MAX_CHILDREN_URIs_PAGE, MAX_DIRSTR_CHARS_TOTAL_BEGINNING, MAX_DIRSTR_CHARS_TOTAL_TOOL } from '../common/prompt/prompts.js';
const MAX_FILES_TOTAL = 1000;
const START_MAX_DEPTH = Infinity;
const START_MAX_ITEMS_PER_DIR = Infinity; // Add start value as Infinity
const MAX_FILES_TOTAL = 300;
const DEFAULT_MAX_DEPTH = 3;
const DEFAULT_MAX_ITEMS_PER_DIR = 3;
const START_MAX_DEPTH = Infinity;
const START_MAX_ITEMS_PER_DIR = Infinity; // Add start value as Infinity
export interface IDirectoryStrService {
readonly _serviceBrand: undefined;
getDirectoryStrTool(uri: URI): Promise<string>
getAllDirectoriesStr(opts: { cutOffMessage: string }): Promise<string>
getAllURIsInDirectory(uri: URI, opts: { maxResults: number }): Promise<URI[]>
getDirectoryStrTool(uri: URI, options?: { maxItemsPerDir?: number }): Promise<string>
getAllDirectoriesStr(opts: { cutOffMessage: string, maxItemsPerDir?: number }): Promise<string>
}
export const IDirectoryStrService = createDecorator<IDirectoryStrService>('voidDirectoryStrService');
@@ -37,35 +38,35 @@ export const IDirectoryStrService = createDecorator<IDirectoryStrService>('voidD
// Check if it's a known filtered type like .git
const shouldExcludeDirectory = (name: string) => {
if (name === '.git' ||
name === 'node_modules' ||
name.startsWith('.') ||
name === 'dist' ||
name === 'build' ||
name === 'out' ||
name === 'bin' ||
name === 'coverage' ||
name === '__pycache__' ||
name === 'env' ||
name === 'venv' ||
name === 'tmp' ||
name === 'temp' ||
name === 'artifacts' ||
name === 'target' ||
name === 'obj' ||
name === 'vendor' ||
name === 'logs' ||
name === 'cache' ||
name === 'resource' ||
name === 'resources'
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' ||
item.name === 'resource' ||
item.name === 'resources'
) {
return true;
}
if (name.match(/\bout\b/)) return true
if (name.match(/\bbuild\b/)) return true
if (item.name.match(/\bout\b/)) return true
if (item.name.match(/\bbuild\b/)) return true
return false;
}
@@ -137,16 +138,10 @@ export const stringifyDirectoryTree1Deep = (params: ToolCallParams['ls_dir'], re
// ---------- IN GENERAL ----------
const resolveChildren = async (children: undefined | IFileStat[], fileService: IFileService): Promise<IFileStat[]> => {
const res = await fileService.resolveAll(children ?? [])
const stats = res.map(s => s.success ? s.stat : null).filter(s => !!s)
return stats
}
// Remove the old computeDirectoryTree function and replace with a combined version that handles both computation and rendering
const computeAndStringifyDirectoryTree = async (
eItem: IFileStat,
fileService: IFileService,
eItem: ExplorerItem,
explorerService: IExplorerService,
MAX_CHARS: number,
fileCount: { count: number } = { count: 0 },
options: { maxDepth?: number, currentDepth?: number, maxItemsPerDir?: number } = {}
@@ -186,13 +181,12 @@ const computeAndStringifyDirectoryTree = async (
let remainingChars = MAX_CHARS - nodeLine.length;
// Check if it's a directory we should skip
const isGitIgnoredDirectory = eItem.isDirectory && shouldExcludeDirectory(eItem.name);
const isGitIgnoredDirectory = eItem.isDirectory && shouldExcludeDirectory(eItem);
// Fetch and process children if not a filtered directory
if (eItem.isDirectory && !isGitIgnoredDirectory) {
// Fetch children with Modified sort order to show recently modified first
const eChildren = await resolveChildren(eItem.children, fileService)
const eChildren = await eItem.fetchChildren(SortOrder.Modified);
// Then recursively add all children with proper tree formatting
if (eChildren && eChildren.length > 0) {
@@ -200,7 +194,7 @@ const computeAndStringifyDirectoryTree = async (
eChildren,
remainingChars,
'',
fileService,
explorerService,
fileCount,
{ maxDepth, currentDepth, maxItemsPerDir } // Pass maxItemsPerDir to the render function
);
@@ -214,10 +208,10 @@ const computeAndStringifyDirectoryTree = async (
// Helper function to render children with proper tree formatting
const renderChildrenCombined = async (
children: IFileStat[],
children: ExplorerItem[],
maxChars: number,
parentPrefix: string,
fileService: IFileService,
explorerService: IExplorerService,
fileCount: { count: number },
options: { maxDepth: number, currentDepth: number, maxItemsPerDir?: number }
): Promise<{ childrenContent: string, childrenCutOff: boolean }> => {
@@ -269,12 +263,12 @@ const renderChildrenCombined = async (
const nextLevelPrefix = parentPrefix + (isLast ? ' ' : '│ ');
// Skip processing children for git ignored directories
const isGitIgnoredDirectory = child.isDirectory && shouldExcludeDirectory(child.name);
const isGitIgnoredDirectory = child.isDirectory && shouldExcludeDirectory(child);
// Create the prefix for the next level (continuation line or space)
if (child.isDirectory && !isGitIgnoredDirectory) {
// Fetch children with Modified sort order to show recently modified first
const eChildren = await resolveChildren(child.children, fileService)
const eChildren = await child.fetchChildren(SortOrder.Modified);
if (eChildren && eChildren.length > 0) {
const {
@@ -284,7 +278,7 @@ const renderChildrenCombined = async (
eChildren,
remainingChars,
nextLevelPrefix,
fileService,
explorerService,
fileCount,
{ maxDepth, currentDepth: nextDepth, maxItemsPerDir }
);
@@ -317,68 +311,7 @@ const renderChildrenCombined = async (
};
// ------------------------- FOLDERS -------------------------
export async function getAllUrisInDirectory(
directoryUri: URI,
maxResults: number,
fileService: IFileService,
): Promise<URI[]> {
const result: URI[] = [];
// Helper function to recursively collect URIs
async function visitAll(folderStat: IFileStat): Promise<boolean> {
// Stop if we've reached the limit
if (result.length >= maxResults) {
return false;
}
try {
if (!folderStat.isDirectory || !folderStat.children) {
return true;
}
const eChildren = await resolveChildren(folderStat.children, fileService)
// Process files first (common convention to list files before directories)
for (const child of eChildren) {
if (!child.isDirectory) {
result.push(child.resource);
// Check if we've hit the limit
if (result.length >= maxResults) {
return false;
}
}
}
// Then process directories recursively
for (const child of eChildren) {
const isGitIgnored = shouldExcludeDirectory(child.name)
if (child.isDirectory && !isGitIgnored) {
const shouldContinue = await visitAll(child);
if (!shouldContinue) {
return false;
}
}
}
return true;
} catch (error) {
console.error(`Error processing directory ${folderStat.resource.fsPath}: ${error}`);
return true; // Continue despite errors in a specific directory
}
}
const rootStat = await fileService.resolve(directoryUri)
await visitAll(rootStat);
return result;
}
// --------------------------------------------------
// ---------------------------------------------------
class DirectoryStrService extends Disposable implements IDirectoryStrService {
@@ -386,25 +319,21 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService {
constructor(
@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,
@IFileService private readonly fileService: IFileService,
@IExplorerService private readonly explorerService: IExplorerService,
) {
super();
}
async getAllURIsInDirectory(uri: URI, opts: { maxResults: number }): Promise<URI[]> {
return getAllUrisInDirectory(uri, opts.maxResults, this.fileService)
}
async getDirectoryStrTool(uri: URI) {
const eRoot = await this.fileService.resolve(uri)
async getDirectoryStrTool(uri: URI, options?: { maxItemsPerDir?: number }) {
const eRoot = this.explorerService.findClosest(uri)
if (!eRoot) throw new Error(`The folder ${uri.fsPath} does not exist.`)
const maxItemsPerDir = START_MAX_ITEMS_PER_DIR; // Use START_MAX_ITEMS_PER_DIR
const maxItemsPerDir = options?.maxItemsPerDir ?? START_MAX_ITEMS_PER_DIR; // Use START_MAX_ITEMS_PER_DIR
// First try with START_MAX_DEPTH
const { content: initialContent, wasCutOff: initialCutOff } = await computeAndStringifyDirectoryTree(
eRoot,
this.fileService,
this.explorerService,
MAX_DIRSTR_CHARS_TOTAL_TOOL,
{ count: 0 },
{ maxDepth: START_MAX_DEPTH, currentDepth: 0, maxItemsPerDir }
@@ -415,7 +344,7 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService {
if (initialCutOff) {
const result = await computeAndStringifyDirectoryTree(
eRoot,
this.fileService,
this.explorerService,
MAX_DIRSTR_CHARS_TOTAL_TOOL,
{ count: 0 },
{ maxDepth: DEFAULT_MAX_DEPTH, currentDepth: 0, maxItemsPerDir: DEFAULT_MAX_ITEMS_PER_DIR }
@@ -434,7 +363,7 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService {
return c
}
async getAllDirectoriesStr({ cutOffMessage, }: { cutOffMessage: string, }) {
async getAllDirectoriesStr({ cutOffMessage, maxItemsPerDir }: { cutOffMessage: string, maxItemsPerDir?: number }) {
let str: string = '';
let cutOff = false;
const folders = this.workspaceContextService.getWorkspace().folders;
@@ -442,7 +371,7 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService {
return '(NO WORKSPACE OPEN)';
// Use START_MAX_ITEMS_PER_DIR if not specified
const startMaxItemsPerDir = START_MAX_ITEMS_PER_DIR;
const startMaxItemsPerDir = maxItemsPerDir ?? START_MAX_ITEMS_PER_DIR;
for (let i = 0; i < folders.length; i += 1) {
if (i > 0) str += '\n';
@@ -452,13 +381,13 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService {
str += `Directory of ${f.uri.fsPath}:\n`;
const rootURI = f.uri;
const eRoot = await this.fileService.resolve(rootURI)
const eRoot = this.explorerService.findClosestRoot(rootURI);
if (!eRoot) continue;
// First try with START_MAX_DEPTH and startMaxItemsPerDir
const { content: initialContent, wasCutOff: initialCutOff } = await computeAndStringifyDirectoryTree(
eRoot,
this.fileService,
this.explorerService,
MAX_DIRSTR_CHARS_TOTAL_BEGINNING - str.length,
{ count: 0 },
{ maxDepth: START_MAX_DEPTH, currentDepth: 0, maxItemsPerDir: startMaxItemsPerDir }
@@ -469,7 +398,7 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService {
if (initialCutOff) {
const result = await computeAndStringifyDirectoryTree(
eRoot,
this.fileService,
this.explorerService,
MAX_DIRSTR_CHARS_TOTAL_BEGINNING - str.length,
{ count: 0 },
{ maxDepth: DEFAULT_MAX_DEPTH, currentDepth: 0, maxItemsPerDir: DEFAULT_MAX_ITEMS_PER_DIR }
@@ -488,8 +417,11 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService {
}
}
const ans = cutOff ? `${str.trimEnd()}\n${cutOffMessage}` : str
return ans
if (cutOff) {
return `${str.trimEnd()}\n${cutOffMessage}`
}
return str
}
}
@@ -14,6 +14,8 @@ import { ICodeEditorService } from '../../../../editor/browser/services/codeEdit
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';
import { Color, RGBA } from '../../../../base/common/color.js';
import { IModelService } from '../../../../editor/common/services/model.js';
import { IUndoRedoElement, IUndoRedoService, UndoRedoElementType } from '../../../../platform/undoRedo/common/undoRedo.js';
import { RenderOptions } from '../../../../editor/browser/widget/diffEditor/components/diffEditorViewZones/renderLines.js';
@@ -23,10 +25,7 @@ import * as dom from '../../../../base/browser/dom.js';
import { Widget } from '../../../../base/browser/ui/widget.js';
import { URI } from '../../../../base/common/uri.js';
import { IConsistentEditorItemService, IConsistentItemService } from './helperServices/consistentItemService.js';
import { voidPrefixAndSuffix, ctrlKStream_userMessage, ctrlKStream_systemMessage, defaultQuickEditFimTags, rewriteCode_systemMessage, rewriteCode_userMessage, searchReplaceGivenDescription_systemMessage, searchReplaceGivenDescription_userMessage, tripleTick, } from '../common/prompt/prompts.js';
import { IVoidCommandBarService } from './voidCommandBarService.js';
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
import { VOID_ACCEPT_DIFF_ACTION_ID, VOID_REJECT_DIFF_ACTION_ID } from './actionIDs.js';
import { voidPrefixAndSuffix, ctrlKStream_userMessage, ctrlKStream_systemMessage, defaultQuickEditFimTags, rewriteCode_systemMessage, rewriteCode_userMessage, searchReplaceGivenDescription_systemMessage, searchReplaceGivenDescription_userMessage, } from '../common/prompt/prompts.js';
import { mountCtrlK } from './react/out/quick-edit-tsx/index.js'
import { QuickEditPropsType } from './quickEditActions.js';
@@ -49,6 +48,27 @@ import { IConvertToLLMMessageService } from './convertToLLMMessageService.js';
// import { isMacintosh } from '../../../../base/common/platform.js';
// import { VOID_OPEN_SETTINGS_ACTION_ID } from './voidSettingsPane.js';
const configOfBG = (color: Color) => {
return { dark: color, light: color, hcDark: color, hcLight: color, }
}
// gets converted to --vscode-void-greenBG, see void.css, asCssVariable
const greenBG = new Color(new RGBA(155, 185, 85, .2)); // default is RGBA(155, 185, 85, .2)
registerColor('void.greenBG', configOfBG(greenBG), '', true);
const redBG = new Color(new RGBA(255, 0, 0, .2)); // default is RGBA(255, 0, 0, .2)
registerColor('void.redBG', configOfBG(redBG), '', true);
const sweepBG = new Color(new RGBA(100, 100, 100, .2));
registerColor('void.sweepBG', configOfBG(sweepBG), '', true);
const highlightBG = new Color(new RGBA(100, 100, 100, .1));
registerColor('void.highlightBG', configOfBG(highlightBG), '', true);
const sweepIdxBG = new Color(new RGBA(100, 100, 100, .5));
registerColor('void.sweepIdxBG', configOfBG(sweepIdxBG), '', true);
const numLinesOfStr = (str: string) => str.split('\n').length
@@ -109,42 +129,29 @@ const removeWhitespaceExceptNewlines = (str: string): string => {
// finds block.orig in fileContents and return its range in file
// startingAtLine is 1-indexed and inclusive
// returns 1-indexed lines
const findTextInCode = (text: string, fileContents: string, canFallbackToRemoveWhitespace: boolean, opts: { startingAtLine?: number, returnType: 'lines' }) => {
const findTextInCode = (text: string, fileContents: string, canFallbackToRemoveWhitespace: boolean, startingAtLine?: number) => {
const returnAns = (fileContents: string, idx: number) => {
const startLine = numLinesOfStr(fileContents.substring(0, idx + 1))
const numLines = numLinesOfStr(text)
const endLine = startLine + numLines - 1
return [startLine, endLine] as const
}
const startingAtLineIdx = (fileContents: string) => opts?.startingAtLine !== undefined ?
fileContents.split('\n').slice(0, opts.startingAtLine).join('\n').length // num characters in all lines before startingAtLine
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, startingAtLineIdx(fileContents))
// if idx was found
if (idx !== -1) {
return returnAns(fileContents, idx)
}
if (!canFallbackToRemoveWhitespace)
return 'Not found' as const
let idx = fileContents.indexOf(text, startLineIdx(fileContents))
// try to find it ignoring all whitespace this time
text = removeWhitespaceExceptNewlines(text)
fileContents = removeWhitespaceExceptNewlines(fileContents)
idx = fileContents.indexOf(text, startingAtLineIdx(fileContents));
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
return returnAns(fileContents, idx)
const startLine = fileContents.substring(0, idx).split('\n').length
const numLines = numLinesOfStr(text)
const endLine = startLine + numLines - 1
return [startLine, endLine] as const
}
@@ -269,11 +276,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
}
public processRawKeybindingText(keybindingStr: string): string {
return keybindingStr
.replace(/Enter/g, '↵') // ⏎
.replace(/Backspace/g, '⌫');
}
// private _notifyError = (e: Parameters<OnError>[0]) => {
// const details = errorDetails(e.fullError)
@@ -587,7 +590,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
}
else { throw new Error('Void 1') }
const buttonsWidget = this._instantiationService.createInstance(AcceptRejectInlineWidget, {
const buttonsWidget = new AcceptRejectInlineWidget({
editor,
onAccept: () => {
this.acceptDiff({ diffid })
@@ -1124,8 +1127,8 @@ class EditCodeService extends Disposable implements IEditCodeService {
return
}
public async callBeforeApplyOrEdit(givenURI: URI | 'current') {
const uri = this._uriOfGivenURI(givenURI)
public async callBeforeStartApplying(opts: CallBeforeStartApplyingOpts) {
const uri = this._getURIBeforeStartApplying(opts)
if (!uri) return
await this._voidModelService.initializeModel(uri)
await this._voidModelService.saveModel(uri) // save the URI
@@ -1182,25 +1185,14 @@ class EditCodeService extends Disposable implements IEditCodeService {
}
const onError = (e: { message: string; fullError: Error | null; }) => {
// this._notifyError(e)
onDone()
this._undoHistory(uri)
throw e.fullError || new Error(e.message)
}
this._instantlyApplySRBlocks(uri, searchReplaceBlocks)
try {
this._instantlyApplySRBlocks(uri, searchReplaceBlocks)
}
catch (e) {
onError({ message: e + '', fullError: null })
}
onDone()
}
public instantlyRewriteFile({ uri, newContent }: { uri: URI, newContent: string }) {
public instantlyApplyNewContent({ uri, newContent }: { uri: URI, newContent: string }) {
// start diffzone
const res = this._startStreamingDiffZone({
uri,
@@ -1220,7 +1212,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
onFinishEdit()
}
this._writeURIText(uri, newContent, 'wholeFileRange', { shouldRealignDiffAreas: true })
this._writeURIText(uri, newContent, 'wholeFileRange', { shouldRealignDiffAreas: false })
onDone()
}
@@ -1343,7 +1335,6 @@ class EditCodeService extends Disposable implements IEditCodeService {
const { from, } = opts
const featureName: FeatureName = opts.from === 'ClickApply' ? 'Apply' : 'Ctrl+K'
const overridesOfModel = this._settingsService.state.overridesOfModel
const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName]
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] : undefined
@@ -1455,7 +1446,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
// this._notifyError(e)
onDone()
this._undoHistory(uri)
throw e.fullError || new Error(e.message)
throw e.fullError
}
const extractText = (fullText: string, recentlyAddedTextLen: number) => {
@@ -1495,7 +1486,6 @@ class EditCodeService extends Disposable implements IEditCodeService {
messages,
modelSelection,
modelSelectionOptions,
overridesOfModel,
separateSystemMessage,
chatMode: null, // not chat
onText: (params) => {
@@ -1570,31 +1560,25 @@ class EditCodeService extends Disposable implements IEditCodeService {
}
/**
* Generates a human-readable error message for an invalid ORIGINAL search block.
*/
private _errContentOfInvalidStr = (
str: 'Not found' | 'Not unique' | 'Has overlap',
blockOrig: string,
): string => {
const problematicCode = `${tripleTick[0]}\n${JSON.stringify(blockOrig)}\n${tripleTick[1]}`
private _errContentOfInvalidStr = (str: 'Not found' | 'Not unique' | 'Has overlap', blockOrig: string) => {
const descStr = str === `Not found` ?
`The most recent ORIGINAL code could not be found in the file, so you were interrupted. The text in ORIGINAL must EXACTLY match lines of code. The problematic ORIGINAL code was:\n${JSON.stringify(blockOrig)}`
: str === `Not unique` ?
`The most recent ORIGINAL code shows up multiple times in the file, so you were interrupted. You might want to expand the ORIGINAL excerpt so it's unique. The problematic ORIGINAL code was:\n${JSON.stringify(blockOrig)}`
: str === 'Has overlap' ?
`The most recent ORIGINAL code has overlap with another ORIGINAL code block that you outputted. Do NOT output any overlapping edits. The problematic ORIGINAL code was:\n${JSON.stringify(blockOrig)}`
: ``
// string of <<<<< ORIGINAL >>>>> REPLACE blocks so far so LLM can understand what it currently has
// const blocksSoFarStr = blocks.slice(0, blockNum).map(block => `${ORIGINAL}\n${block.orig}\n${DIVIDER}\n${block.final}\n${FINAL}`).join('\n')
// const soFarStr = blocksSoFarStr ? `These are the Search/Replace blocks that have been applied so far:${tripleTick[0]}\n${blocksSoFarStr}\n${tripleTick[1]}` : ''
// const continueMsg = soFarStr ? `${soFarStr}Please continue outputting SEARCH/REPLACE blocks starting where this leaves off.` : ''
// const errMsg = `${descStr}${continueMsg ? `\n${continueMsg}` : ''}`
const soFarStr = 'All of your previous outputs have been ignored. Please re-output ALL SEARCH/REPLACE blocks starting from the first one, and avoid the error this time.'
const errMsg = `${descStr}\n${soFarStr}`
return errMsg
// use a switch for better readability / exhaustiveness check
let descStr: string
switch (str) {
case 'Not found':
descStr = `The edit was not applied. The text in ORIGINAL must EXACTLY match lines of code in the file, but there was no match for:\n${problematicCode}. Ensure you have the latest version of the file, and ensure the ORIGINAL code matches a code excerpt exactly.`
break
case 'Not unique':
descStr = `The edit was not applied. The text in ORIGINAL must be unique in the file being edited, but the following ORIGINAL code appears multiple times in the file:\n${problematicCode}. Ensure you have the latest version of the file, and ensure the ORIGINAL code is unique.`
break
case 'Has overlap':
descStr = `The edit was not applied. The text in the ORIGINAL blocks must not overlap, but the following ORIGINAL code had overlap with another ORIGINAL string:\n${problematicCode}. Ensure you have the latest version of the file, and ensure the ORIGINAL code blocks do not overlap.`
break
default:
descStr = ''
}
return descStr
}
@@ -1605,31 +1589,23 @@ class EditCodeService extends Disposable implements IEditCodeService {
const { model } = this._voidModelService.getModel(uri)
if (!model) throw new Error(`Error applying Search/Replace blocks: File does not exist.`)
const modelStr = model.getValue(EndOfLinePreference.LF)
// .split('\n').map(l => '\t' + l).join('\n') // for testing purposes only, remember to remove this
const modelStrLines = modelStr.split('\n')
const replacements: { origStart: number; origEnd: number; block: ExtractedSearchReplaceBlock }[] = []
for (const b of blocks) {
const res = findTextInCode(b.orig, modelStr, true, { returnType: 'lines' })
if (typeof res === 'string')
throw new Error(this._errContentOfInvalidStr(res, b.orig))
let [startLine, endLine] = res
startLine -= 1 // 0-index
endLine -= 1
const i = modelStr.indexOf(b.orig)
if (i === -1)
throw new Error(this._errContentOfInvalidStr('Not found', b.orig))
const j = modelStr.lastIndexOf(b.orig)
if (i !== j)
throw new Error(this._errContentOfInvalidStr('Not unique', b.orig))
// including newline before start
const origStart = (startLine !== 0 ?
modelStrLines.slice(0, startLine).join('\n') + '\n'
: '').length
// including endline at end
const origEnd = modelStrLines.slice(0, endLine + 1).join('\n').length - 1
replacements.push({ origStart, origEnd, block: b });
replacements.push({
origStart: i,
origEnd: i + b.orig.length - 1, // INCLUSIVE
block: b,
})
}
// sort in increasing order
replacements.sort((a, b) => a.origStart - b.origStart)
@@ -1651,12 +1627,12 @@ class EditCodeService extends Disposable implements IEditCodeService {
'wholeFileRange',
{ shouldRealignDiffAreas: true }
)
}
private _initializeSearchAndReplaceStream(opts: StartApplyingOpts & { from: 'ClickApply' }): [DiffZone, Promise<void>] | undefined {
const { from, applyStr, } = opts
const featureName: FeatureName = 'Apply'
const overridesOfModel = this._settingsService.state.overridesOfModel
const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName]
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] : undefined
@@ -1738,7 +1714,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
// this._notifyError(e)
onDone()
this._undoHistory(uri)
throw e.fullError || new Error(e.message)
throw e.fullError || new Error(e.message) // throw error h
}
// refresh now in case onText takes a while to get 1st message
@@ -1792,7 +1768,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, false, { startingAtLine, returnType: 'lines' })
const originalRange = findTextInCode(block.orig, originalFileCode, false, startingAtLine)
if (typeof originalRange !== 'string') {
const [startLine, _] = convertOriginalRangeToFinalRange(originalRange)
diffZone._streamState.line = startLine
@@ -1818,7 +1794,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
// if this is the first time we're seeing this block, add it as a diffarea so we can start streaming in it
if (!(blockNum in addedTrackingZoneOfBlockNum)) {
const originalBounds = findTextInCode(block.orig, originalFileCode, true, { returnType: 'lines' })
const originalBounds = findTextInCode(block.orig, originalFileCode, true)
// if error
// Check for overlap with existing modified ranges
const hasOverlap = addedTrackingZoneOfBlockNum.some(trackingZone => {
@@ -1837,10 +1813,9 @@ class EditCodeService extends Disposable implements IEditCodeService {
console.log('block.orig:', block.orig)
console.log('---------')
const content = this._errContentOfInvalidStr(errorMessage, block.orig)
const retryMsg = 'All of your previous outputs have been ignored. Please re-output ALL SEARCH/REPLACE blocks starting from the first one, and avoid the error this time.'
messages.push(
{ role: 'assistant', content: fullText }, // latest output
{ role: 'user', content: content + '\n' + retryMsg } // user explanation of what's wrong
{ role: 'user', content: content } // user explanation of what's wrong
)
// REVERT ALL BLOCKS
@@ -1936,7 +1911,6 @@ class EditCodeService extends Disposable implements IEditCodeService {
messages,
modelSelection,
modelSelectionOptions,
overridesOfModel,
separateSystemMessage,
chatMode: null, // not chat
onText: (params) => {
@@ -1950,8 +1924,6 @@ class EditCodeService extends Disposable implements IEditCodeService {
if (blocks.length === 0) {
this._notificationService.info(`Void: We ran Fast Apply, but the LLM didn't output any changes.`)
}
this._writeURIText(uri, originalFileCode, 'wholeFileRange', { shouldRealignDiffAreas: true })
try {
this._instantlyApplySRBlocks(uri, fullText)
@@ -2259,77 +2231,31 @@ registerSingleton(IEditCodeService, EditCodeService, InstantiationType.Eager);
class AcceptRejectInlineWidget extends Widget implements IOverlayWidget {
public getId(): string {
return this.ID || ''; // Ensure we always return a string
}
public getDomNode(): HTMLElement {
return this._domNode;
}
public getPosition() {
return null;
}
public getId() { return this.ID }
public getDomNode() { return this._domNode; }
public getPosition() { return null }
private readonly _domNode: HTMLElement; // Using the definite assignment assertion
private readonly editor: ICodeEditor;
private readonly ID: string;
private readonly startLine: number;
private readonly _domNode: HTMLElement;
private readonly editor
private readonly ID
private readonly startLine
constructor(
{ editor, onAccept, onReject, diffid, startLine, offsetLines }: {
editor: ICodeEditor;
onAccept: () => void;
onReject: () => void;
diffid: string,
startLine: number,
offsetLines: number
},
@IVoidCommandBarService private readonly _voidCommandBarService: IVoidCommandBarService,
@IKeybindingService private readonly _keybindingService: IKeybindingService,
@IEditCodeService private readonly _editCodeService: IEditCodeService,
) {
super();
constructor({ editor, onAccept, onReject, diffid, startLine, offsetLines }: { editor: ICodeEditor; onAccept: () => void; onReject: () => void; diffid: string, startLine: number, offsetLines: number }) {
super()
const uri = editor.getModel()?.uri;
// Initialize with default values
this.ID = ''
this.ID = editor.getModel()?.uri.fsPath + diffid;
this.editor = editor;
this.startLine = startLine;
if (!uri) {
const { dummyDiv } = dom.h('div@dummyDiv');
this._domNode = dummyDiv
return;
}
this.ID = uri.fsPath + diffid;
const lineHeight = editor.getOption(EditorOption.lineHeight);
const getAcceptRejectText = () => {
const acceptKeybinding = this._keybindingService.lookupKeybinding(VOID_ACCEPT_DIFF_ACTION_ID);
const rejectKeybinding = this._keybindingService.lookupKeybinding(VOID_REJECT_DIFF_ACTION_ID);
// Use the standalone function directly since we're in a nested class that
// can't access EditCodeService's methods
const acceptKeybindLabel = this._editCodeService.processRawKeybindingText(acceptKeybinding && acceptKeybinding.getLabel() || '');
const rejectKeybindLabel = this._editCodeService.processRawKeybindingText(rejectKeybinding && rejectKeybinding.getLabel() || '');
const commandBarStateAtUri = this._voidCommandBarService.stateOfURI[uri.fsPath];
const selectedDiffIdx = commandBarStateAtUri?.diffIdx ?? 0; // 0th item is selected by default
const thisDiffIdx = commandBarStateAtUri?.sortedDiffIds.indexOf(diffid) ?? null;
const showLabel = thisDiffIdx === selectedDiffIdx
const acceptText = `Accept${showLabel ? ` ` + acceptKeybindLabel : ''}`;
const rejectText = `Reject${showLabel ? ` ` + rejectKeybindLabel : ''}`;
return { acceptText, rejectText }
}
const { acceptText, rejectText } = getAcceptRejectText()
// Create container div with buttons
const { acceptButton, rejectButton, buttons } = dom.h('div@buttons', [
dom.h('button@acceptButton', []),
@@ -2343,14 +2269,11 @@ class AcceptRejectInlineWidget extends Widget implements IOverlayWidget {
buttons.style.paddingRight = '4px';
buttons.style.zIndex = '1';
buttons.style.transform = `translateY(${offsetLines * lineHeight}px)`;
buttons.style.justifyContent = 'flex-end';
buttons.style.width = '100%';
buttons.style.pointerEvents = 'none';
// Style accept button
acceptButton.onclick = onAccept;
acceptButton.textContent = acceptText;
acceptButton.textContent = 'Accept';
acceptButton.style.backgroundColor = acceptBg;
acceptButton.style.border = acceptBorder;
acceptButton.style.color = buttonTextColor;
@@ -2364,12 +2287,10 @@ class AcceptRejectInlineWidget extends Widget implements IOverlayWidget {
acceptButton.style.cursor = 'pointer';
acceptButton.style.height = '100%';
acceptButton.style.boxShadow = '0 2px 3px rgba(0,0,0,0.2)';
acceptButton.style.pointerEvents = 'auto';
// Style reject button
rejectButton.onclick = onReject;
rejectButton.textContent = rejectText;
rejectButton.textContent = 'Reject';
rejectButton.style.backgroundColor = rejectBg;
rejectButton.style.border = rejectBorder;
rejectButton.style.color = buttonTextColor;
@@ -2383,7 +2304,6 @@ class AcceptRejectInlineWidget extends Widget implements IOverlayWidget {
rejectButton.style.cursor = 'pointer';
rejectButton.style.height = '100%';
rejectButton.style.boxShadow = '0 2px 3px rgba(0,0,0,0.2)';
rejectButton.style.pointerEvents = 'auto';
@@ -2404,28 +2324,16 @@ class AcceptRejectInlineWidget extends Widget implements IOverlayWidget {
}
// Mount first, then update positions
setTimeout(() => {
updateTop()
updateLeft()
}, 0)
editor.addOverlayWidget(this);
updateTop()
updateLeft()
this._register(editor.onDidScrollChange(e => { updateTop() }))
this._register(editor.onDidChangeModelContent(e => { updateTop() }))
this._register(editor.onDidLayoutChange(e => { updateTop(); updateLeft() }))
// Listen for state changes in the command bar service
this._register(this._voidCommandBarService.onDidChangeState(e => {
if (uri && e.uri.fsPath === uri.fsPath) {
const { acceptText, rejectText } = getAcceptRejectText()
acceptButton.textContent = acceptText;
rejectButton.textContent = rejectText;
}
}));
// mount this widget
editor.addOverlayWidget(this);
@@ -2433,8 +2341,8 @@ class AcceptRejectInlineWidget extends Widget implements IOverlayWidget {
}
public override dispose(): void {
this.editor.removeOverlayWidget(this);
super.dispose();
this.editor.removeOverlayWidget(this)
super.dispose()
}
}
@@ -42,12 +42,10 @@ export const IEditCodeService = createDecorator<IEditCodeService>('editCodeServi
export interface IEditCodeService {
readonly _serviceBrand: undefined;
processRawKeybindingText(keybindingStr: string): string;
callBeforeApplyOrEdit(uri: URI | 'current'): Promise<void>;
callBeforeStartApplying(opts: CallBeforeStartApplyingOpts): Promise<void>;
startApplying(opts: StartApplyingOpts): [URI, Promise<void>] | null;
instantlyApplySearchReplaceBlocks(opts: { uri: URI; searchReplaceBlocks: string }): void;
instantlyRewriteFile(opts: { uri: URI; newContent: string }): void;
instantlyApplyNewContent(opts: { uri: URI; newContent: string }): void;
addCtrlKZone(opts: AddCtrlKOpts): number | undefined;
removeCtrlKZone(opts: { diffareaid: number }): void;
@@ -56,8 +54,6 @@ export interface IEditCodeService {
diffOfId: Record<string, Diff>;
acceptOrRejectAllDiffAreas(opts: { uri: URI, removeCtrlKs: boolean, behavior: 'reject' | 'accept', _addToHistory?: boolean }): void;
acceptDiff({ diffid }: { diffid: number }): void;
rejectDiff({ diffid }: { diffid: number }): void;
// events
onDidAddOrDeleteDiffZones: Event<{ uri: URI }>;
@@ -1,329 +0,0 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { VSBuffer } from '../../../../base/common/buffer.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { env } from '../../../../base/common/process.js';
import { URI } from '../../../../base/common/uri.js';
import { IFileService } from '../../../../platform/files/common/files.js';
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { TransferEditorType, TransferFilesInfo } from './extensionTransferTypes.js';
export interface IExtensionTransferService {
readonly _serviceBrand: undefined; // services need this, just leave it undefined
transferExtensions(os: 'mac' | 'windows' | 'linux' | null, fromEditor: TransferEditorType): Promise<string | undefined>
deleteBlacklistExtensions(os: 'mac' | 'windows' | 'linux' | null): Promise<void>
}
export const IExtensionTransferService = createDecorator<IExtensionTransferService>('ExtensionTransferService');
// Define extensions to skip when transferring
const extensionBlacklist = [
// ignore extensions
'ms-vscode-remote.remote', // ms-vscode-remote.remote-ssh, ms-vscode-remote.remote-wsl
'ms-vscode.remote', // ms-vscode.remote-explorer
// ignore other AI copilots that could conflict with Void keybindings
'sourcegraph.cody-ai',
'continue.continue',
'codeium.codeium',
'saoudrizwan.claude-dev', // cline
'rooveterinaryinc.roo-cline', // roo
'supermaven.supermaven' // supermaven
// 'github.copilot',
];
const isBlacklisted = (fsPath: string | undefined) => {
return extensionBlacklist.find(bItem => fsPath?.includes(bItem))
}
class ExtensionTransferService extends Disposable implements IExtensionTransferService {
_serviceBrand: undefined;
constructor(
@IFileService private readonly _fileService: IFileService,
) {
super()
}
async transferExtensions(os: 'mac' | 'windows' | 'linux' | null, fromEditor: TransferEditorType) {
const transferTheseFiles = transferTheseFilesOfOS(os, fromEditor)
const fileService = this._fileService
let errAcc = ''
for (const { from, to, isExtensions } of transferTheseFiles) {
// Check if the source file exists before attempting to copy
try {
if (!isExtensions) {
console.log('transferring item', from, to)
const exists = await fileService.exists(from)
if (exists) {
// Ensure the destination directory exists
const toParent = URI.joinPath(to, '..')
const toParentExists = await fileService.exists(toParent)
if (!toParentExists) {
await fileService.createFolder(toParent)
}
await fileService.copy(from, to, true)
} else {
console.log(`Skipping file that doesn't exist: ${from.toString()}`)
}
}
// extensions folder
else {
console.log('transferring extensions...', from, to)
const exists = await fileService.exists(from)
if (exists) {
const stat = await fileService.resolve(from)
const toParent = URI.joinPath(to) // extensions/
const toParentExists = await fileService.exists(toParent)
if (!toParentExists) {
await fileService.createFolder(toParent)
}
for (const extensionFolder of stat.children ?? []) {
const from = extensionFolder.resource
const to = URI.joinPath(toParent, extensionFolder.name)
const toStat = await fileService.resolve(from)
if (toStat.isDirectory) {
if (!isBlacklisted(extensionFolder.resource.fsPath)) {
await fileService.copy(from, to, true)
}
}
else if (toStat.isFile) {
if (extensionFolder.name === 'extensions.json') {
try {
const contentsStr = await fileService.readFile(from)
const json: any = JSON.parse(contentsStr.value.toString())
const j2 = json.filter((entry: { identifier?: { id?: string } }) => !isBlacklisted(entry?.identifier?.id))
const jsonStr = JSON.stringify(j2)
await fileService.writeFile(to, VSBuffer.fromString(jsonStr))
}
catch {
console.log('Error copying extensions.json, skipping')
}
}
}
}
} else {
console.log(`Skipping file that doesn't exist: ${from.toString()}`)
}
console.log('done transferring extensions.')
}
}
catch (e) {
console.error('Error copying file:', e)
errAcc += `Error copying ${from.toString()}: ${e}\n`
}
}
if (errAcc) return errAcc
return undefined
}
async deleteBlacklistExtensions(os: 'mac' | 'windows' | 'linux' | null) {
const fileService = this._fileService
const extensionsURI = getExtensionsFolder(os)
if (!extensionsURI) return
const eURI = await fileService.resolve(extensionsURI)
for (const child of eURI.children ?? []) {
try {
if (child.isDirectory) {
// if is blacklisted
if (isBlacklisted(child.resource.fsPath)) {
console.log('Deleting extension', child.resource.fsPath)
await fileService.del(child.resource, { recursive: true, useTrash: true })
}
}
else if (child.isFile) {
// if is extensions.json
if (child.name === 'extensions.json') {
console.log('Updating extensions.json', child.resource.fsPath)
try {
const contentsStr = await fileService.readFile(child.resource)
const json: any = JSON.parse(contentsStr.value.toString())
const j2 = json.filter((entry: { identifier?: { id?: string } }) => !isBlacklisted(entry?.identifier?.id))
const jsonStr = JSON.stringify(j2)
await fileService.writeFile(child.resource, VSBuffer.fromString(jsonStr))
}
catch {
console.log('Error copying extensions.json, skipping')
}
}
}
}
catch (e) {
console.error('Could not delete extension', child.resource.fsPath, e)
}
}
}
}
registerSingleton(IExtensionTransferService, ExtensionTransferService, InstantiationType.Eager); // lazily loaded, even if Eager
const transferTheseFilesOfOS = (os: 'mac' | 'windows' | 'linux' | null, fromEditor: TransferEditorType = 'VS Code'): TransferFilesInfo => {
if (os === null)
throw new Error(`One-click switch is not possible in this environment.`)
if (os === 'mac') {
const homeDir = env['HOME']
if (!homeDir) throw new Error(`$HOME not found`)
if (fromEditor === 'VS Code') {
return [{
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Code', 'User', 'settings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'settings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Code', 'User', 'keybindings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'keybindings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.vscode', 'extensions'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'),
isExtensions: true,
}]
} else if (fromEditor === 'Cursor') {
return [{
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Cursor', 'User', 'settings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'settings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Cursor', 'User', 'keybindings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'keybindings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.cursor', 'extensions'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'),
isExtensions: true,
}]
} else if (fromEditor === 'Windsurf') {
return [{
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Windsurf', 'User', 'settings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'settings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Windsurf', 'User', 'keybindings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'keybindings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.windsurf', 'extensions'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'),
isExtensions: true,
}]
}
}
if (os === 'linux') {
const homeDir = env['HOME']
if (!homeDir) throw new Error(`variable for $HOME location not found`)
if (fromEditor === 'VS Code') {
return [{
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Code', 'User', 'settings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'settings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Code', 'User', 'keybindings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'keybindings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.vscode', 'extensions'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'),
isExtensions: true,
}]
} else if (fromEditor === 'Cursor') {
return [{
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Cursor', 'User', 'settings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'settings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Cursor', 'User', 'keybindings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'keybindings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.cursor', 'extensions'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'),
isExtensions: true,
}]
} else if (fromEditor === 'Windsurf') {
return [{
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Windsurf', 'User', 'settings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'settings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Windsurf', 'User', 'keybindings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'keybindings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.windsurf', 'extensions'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'),
isExtensions: true,
}]
}
}
if (os === 'windows') {
const appdata = env['APPDATA']
if (!appdata) throw new Error(`variable for %APPDATA% location not found`)
const userprofile = env['USERPROFILE']
if (!userprofile) throw new Error(`variable for %USERPROFILE% location not found`)
if (fromEditor === 'VS Code') {
return [{
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Code', 'User', 'settings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'settings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Code', 'User', 'keybindings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'keybindings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.vscode', 'extensions'),
to: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.void-editor', 'extensions'),
isExtensions: true,
}]
} else if (fromEditor === 'Cursor') {
return [{
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Cursor', 'User', 'settings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'settings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Cursor', 'User', 'keybindings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'keybindings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.cursor', 'extensions'),
to: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.void-editor', 'extensions'),
isExtensions: true,
}]
} else if (fromEditor === 'Windsurf') {
return [{
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Windsurf', 'User', 'settings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'settings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Windsurf', 'User', 'keybindings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'keybindings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.windsurf', 'extensions'),
to: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.void-editor', 'extensions'),
isExtensions: true,
}]
}
}
throw new Error(`os '${os}' not recognized or editor type '${fromEditor}' not supported for this OS`)
}
const getExtensionsFolder = (os: 'mac' | 'windows' | 'linux' | null) => {
const t = transferTheseFilesOfOS(os, 'VS Code') // from editor doesnt matter
return t.find(f => f.isExtensions)?.to
}
@@ -1,11 +0,0 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { URI } from '../../../../base/common/uri.js'
export type TransferEditorType = 'VS Code' | 'Cursor' | 'Windsurf'
// https://github.com/VSCodium/vscodium/blob/master/docs/index.md#migrating-from-visual-studio-code-to-vscodium
// https://code.visualstudio.com/docs/editor/extension-marketplace#_where-are-extensions-installed
export type TransferFilesInfo = { from: URI, to: URI, isExtensions?: boolean }[]
@@ -1,78 +0,0 @@
import { localize2 } from '../../../../nls.js';
import { URI } from '../../../../base/common/uri.js';
import { Action2, registerAction2, MenuId } from '../../../../platform/actions/common/actions.js';
import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';
import { INotificationService } from '../../../../platform/notification/common/notification.js';
import { IFileService } from '../../../../platform/files/common/files.js';
import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js';
import { IDirectoryStrService } from '../common/directoryStrService.js';
import { messageOfSelection } from '../common/prompt/prompts.js';
import { IVoidModelService } from '../common/voidModelService.js';
class FilePromptActionService extends Action2 {
private static readonly VOID_COPY_FILE_PROMPT_ID = 'void.copyfileprompt'
constructor() {
super({
id: FilePromptActionService.VOID_COPY_FILE_PROMPT_ID,
title: localize2('voidCopyPrompt', 'Void: Copy Prompt'),
menu: [{
id: MenuId.ExplorerContext,
group: '8_void',
order: 1,
}]
});
}
async run(accessor: ServicesAccessor, uri: URI): Promise<void> {
try {
const fileService = accessor.get(IFileService);
const clipboardService = accessor.get(IClipboardService)
const directoryStrService = accessor.get(IDirectoryStrService)
const voidModelService = accessor.get(IVoidModelService)
const stat = await fileService.stat(uri)
const folderOpts = {
maxChildren: 1000,
maxCharsPerFile: 2_000_000,
} as const
let m: string = 'No contents detected'
if (stat.isFile) {
m = await messageOfSelection({
type: 'File',
uri,
language: (await voidModelService.getModelSafe(uri)).model?.getLanguageId() || '',
state: { wasAddedAsCurrentFile: false, },
}, {
folderOpts,
directoryStrService,
fileService,
})
}
if (stat.isDirectory) {
m = await messageOfSelection({
type: 'Folder',
uri,
}, {
folderOpts,
fileService,
directoryStrService,
})
}
await clipboardService.writeText(m)
} catch (error) {
const notificationService = accessor.get(INotificationService)
notificationService.error(error + '')
}
}
}
registerAction2(FilePromptActionService)
@@ -1,49 +0,0 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { Disposable } from '../../../../base/common/lifecycle.js';
import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js';
import { IExtensionTransferService } from './extensionTransferService.js';
import { os } from '../common/helpers/systemInfo.js';
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
import { timeout } from '../../../../base/common/async.js';
import { getActiveWindow } from '../../../../base/browser/dom.js';
// Onboarding contribution that mounts the component at startup
export class MiscWorkbenchContribs extends Disposable implements IWorkbenchContribution {
static readonly ID = 'workbench.contrib.voidMiscWorkbenchContribs';
constructor(
@IExtensionTransferService private readonly extensionTransferService: IExtensionTransferService,
@IStorageService private readonly storageService: IStorageService,
) {
super();
this.initialize();
}
private initialize(): void {
// delete blacklisted extensions once (this is for people who already installed them)
const deleteExtensionsStorageId = 'void-deleted-blacklist-2'
const alreadyDeleted = this.storageService.get(deleteExtensionsStorageId, StorageScope.APPLICATION)
if (!alreadyDeleted) {
this.storageService.store(deleteExtensionsStorageId, 'true', StorageScope.APPLICATION, StorageTarget.MACHINE)
this.extensionTransferService.deleteBlacklistExtensions(os)
}
// after some time, trigger a resize event for the blank screen error
timeout(5_000).then(() => {
// Get the active window reference for multi-window support
const targetWindow = getActiveWindow();
// Trigger a window resize event to ensure proper layout calculations
targetWindow.dispatchEvent(new Event('resize'))
})
}
}
registerWorkbenchContribution2(MiscWorkbenchContribs.ID, MiscWorkbenchContribs, WorkbenchPhase.Eventually);
@@ -3,14 +3,14 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { useState, useEffect, useCallback, useRef, Fragment } from 'react'
import { useAccessor, useChatThreadsState, useChatThreadsStreamState, useCommandBarState, useCommandBarURIListener, useSettingsState } from '../util/services.js'
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, Terminal } from 'lucide-react'
import { Check, X, Square, Copy, Play, } from 'lucide-react'
import { getBasename, ListableToolItem, voidOpenFileFn, ToolChildrenWrapper } from '../sidebar-tsx/SidebarChat.js'
import { getBasename, ListableToolItem, ToolChildrenWrapper } from '../sidebar-tsx/SidebarChat.js'
import { PlacesType, VariantType } from 'react-tooltip'
enum CopyButtonText {
@@ -24,9 +24,8 @@ type IconButtonProps = {
Icon: LucideIcon
}
export const IconShell1 = ({ onClick, Icon, disabled, className, ...props }: IconButtonProps & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
return <button
export const IconShell1 = ({ onClick, Icon, disabled, className, ...props }: IconButtonProps & React.ButtonHTMLAttributes<HTMLButtonElement>) => (
<button
disabled={disabled}
onClick={(e) => {
e.preventDefault();
@@ -35,19 +34,19 @@ export const IconShell1 = ({ onClick, Icon, disabled, className, ...props }: Ico
}}
// border border-void-border-1 rounded
className={`
size-[18px]
p-[2px]
flex items-center justify-center
text-sm text-void-fg-3
hover:brightness-110
disabled:opacity-50 disabled:cursor-not-allowed
${className}
size-[18px]
p-[2px]
flex items-center justify-center
text-sm text-void-fg-3
hover:brightness-110
disabled:opacity-50 disabled:cursor-not-allowed
${className}
`}
{...props}
>
<Icon />
</button>
}
)
// export const IconShell2 = ({ onClick, title, Icon, disabled, className }: IconButtonProps) => (
@@ -109,7 +108,7 @@ export const JumpToFileButton = ({ uri, ...props }: { uri: URI | 'current' } & R
<IconShell1
Icon={FileSymlink}
onClick={() => {
voidOpenFileFn(uri, accessor)
commandService.executeCommand('vscode.open', uri, { preview: true })
}}
{...tooltipPropsForApplyBlock({ tooltipName: 'Go to file' })}
{...props}
@@ -132,41 +131,48 @@ export const JumpToTerminalButton = ({ onClick }: { onClick: () => void }) => {
// 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 applyingURIOfApplyBoxIdRef: { current: { [applyBoxId: string]: URI | undefined } } = { current: {} }
const getUriBeingApplied = (applyBoxId: string) => {
return _applyingURIOfApplyBoxIdRef.current[applyBoxId] ?? null
return applyingURIOfApplyBoxIdRef.current[applyBoxId] ?? null
}
export const useApplyStreamState = ({ applyBoxId }: { applyBoxId: string }) => {
export const useApplyButtonState = ({ applyBoxId, uri }: { applyBoxId: string, uri: URI | 'current' }) => {
const settingsState = useSettingsState()
const isDisabled = !!isFeatureNameDisabled('Apply', settingsState) || !applyBoxId
const accessor = useAccessor()
const voidCommandBarService = accessor.get('IVoidCommandBarService')
const [_, rerender] = useState(0)
const getStreamState = useCallback(() => {
const uri = getUriBeingApplied(applyBoxId)
if (!uri) return 'idle-no-changes'
return voidCommandBarService.getStreamState(uri)
}, [voidCommandBarService, applyBoxId])
const [currStreamStateRef, setStreamState] = useRefState(getStreamState())
const setApplying = useCallback((uri: URI | undefined) => {
_applyingURIOfApplyBoxIdRef.current[applyBoxId] = uri ?? undefined
setStreamState(getStreamState())
}, [setStreamState, getStreamState, applyBoxId])
// listen for stream updates on this box
useCommandBarURIListener(useCallback((uri_) => {
const uri = getUriBeingApplied(applyBoxId)
if (uri?.fsPath === uri_.fsPath) {
setStreamState(getStreamState())
const shouldUpdate = (
getUriBeingApplied(applyBoxId)?.fsPath === uri_.fsPath
|| (uri !== 'current' && uri.fsPath === uri_.fsPath)
)
if (shouldUpdate) {
rerender(c => c + 1)
}
}, [setStreamState, applyBoxId, getStreamState]))
}, [applyBoxId, uri]))
const currStreamState = getStreamState()
return { currStreamStateRef, setApplying }
return {
getStreamState,
isDisabled,
currStreamState,
}
}
@@ -196,24 +202,10 @@ const tooltipPropsForApplyBlock = ({ tooltipName, color = undefined, position =
'data-tooltip-offset': offset,
})
export const useEditToolStreamState = ({ applyBoxId, uri }: { applyBoxId: string, uri: URI }) => {
const accessor = useAccessor()
const voidCommandBarService = accessor.get('IVoidCommandBarService')
const [streamState, setStreamState] = useState(voidCommandBarService.getStreamState(uri))
// listen for stream updates on this box
useCommandBarURIListener(useCallback((uri_) => {
const shouldUpdate = uri.fsPath === uri_.fsPath
if (shouldUpdate) { setStreamState(voidCommandBarService.getStreamState(uri)) }
}, [voidCommandBarService, applyBoxId, uri]))
return { streamState, }
}
export const StatusIndicatorForApplyButton = ({ applyBoxId, uri }: { applyBoxId: string, uri: URI | 'current' } & React.HTMLAttributes<HTMLDivElement>) => {
const { currStreamStateRef } = useApplyStreamState({ applyBoxId })
const currStreamState = currStreamStateRef.current
const { currStreamState } = useApplyButtonState({ applyBoxId, uri })
const color = (
currStreamState === 'idle-no-changes' ? 'dark' :
@@ -239,255 +231,82 @@ export const StatusIndicatorForApplyButton = ({ applyBoxId, uri }: { applyBoxId:
}
const terminalLanguages = new Set([
'bash',
'shellscript',
'shell',
'powershell',
'bat',
'zsh',
'sh',
'fish',
'nushell',
'ksh',
'xonsh',
'elvish',
])
const ApplyButtonsForTerminal = ({
codeStr,
applyBoxId,
uri,
language,
}: {
codeStr: string,
applyBoxId: string,
language?: string,
uri: URI | 'current';
}) => {
const accessor = useAccessor()
const metricsService = accessor.get('IMetricsService')
const terminalToolService = accessor.get('ITerminalToolService')
const settingsState = useSettingsState()
const [isShellRunning, setIsShellRunning] = useState<boolean>(false)
const interruptToolRef = useRef<(() => void) | null>(null)
const isDisabled = isShellRunning
const onClickSubmit = useCallback(async () => {
if (isShellRunning) return
try {
setIsShellRunning(true)
const terminalId = await terminalToolService.createPersistentTerminal({ cwd: null })
const { interrupt } = await terminalToolService.runCommand(
codeStr,
{ type: 'persistent', persistentTerminalId: terminalId }
);
interruptToolRef.current = interrupt
metricsService.capture('Execute Shell', { length: codeStr.length })
} catch (e) {
setIsShellRunning(false)
console.error('Failed to execute in terminal:', e)
}
}, [codeStr, uri, applyBoxId, metricsService, terminalToolService, isShellRunning])
if (isShellRunning) {
return (
<IconShell1
Icon={X}
onClick={() => {
interruptToolRef.current?.();
setIsShellRunning(false);
}}
{...tooltipPropsForApplyBlock({ tooltipName: 'Stop' })}
/>
);
}
if (isDisabled) {
return null
}
return <IconShell1
Icon={Play}
onClick={onClickSubmit}
{...tooltipPropsForApplyBlock({ tooltipName: 'Apply' })}
/>
}
const ApplyButtonsForEdit = ({
codeStr,
applyBoxId,
uri,
language,
}: {
codeStr: string,
applyBoxId: string,
language?: string,
uri: URI | 'current';
}) => {
export const ApplyButtonsHTML = ({ codeStr, applyBoxId, uri }: { codeStr: string, applyBoxId: string, uri: URI | 'current' }) => {
const accessor = useAccessor()
const editCodeService = accessor.get('IEditCodeService')
const metricsService = accessor.get('IMetricsService')
const notificationService = accessor.get('INotificationService')
const settingsState = useSettingsState()
const isDisabled = !!isFeatureNameDisabled('Apply', settingsState) || !applyBoxId
const { currStreamStateRef, setApplying } = useApplyStreamState({ applyBoxId })
const {
currStreamState,
isDisabled,
getStreamState,
} = useApplyButtonState({ applyBoxId, uri })
const onClickSubmit = useCallback(async () => {
if (currStreamStateRef.current === 'streaming') return
await editCodeService.callBeforeApplyOrEdit(uri)
const [newApplyingUri, applyDonePromise] = editCodeService.startApplying({
if (isDisabled) return
if (getStreamState() === 'streaming') return
const opts = {
from: 'ClickApply',
applyStr: codeStr,
uri: uri,
startBehavior: 'reject-conflicts',
}) ?? []
setApplying(newApplyingUri)
} as const
if (!applyDonePromise) {
notificationService.info(`Void Error: We couldn't run Apply here. ${uri === 'current' ? 'This Apply block wants to run on the current file, but you might not have a file open.' : `This Apply block wants to run on ${uri.fsPath}, but it might not exist.`}`)
}
await editCodeService.callBeforeStartApplying(opts)
const [newApplyingUri, applyDonePromise] = editCodeService.startApplying(opts) ?? []
// catch any errors by interrupting the stream
applyDonePromise?.catch(e => {
const uri = getUriBeingApplied(applyBoxId)
if (uri) editCodeService.interruptURIStreaming({ uri: uri })
notificationService.info(`Void Error: There was a problem running Apply: ${e}.`)
})
applyingURIOfApplyBoxIdRef.current[applyBoxId] = newApplyingUri ?? undefined
// rerender(c => c + 1)
metricsService.capture('Apply Code', { length: codeStr.length }) // capture the length only
}, [setApplying, currStreamStateRef, editCodeService, codeStr, uri, applyBoxId, metricsService, notificationService])
}, [isDisabled, getStreamState, editCodeService, codeStr, uri, applyBoxId, metricsService])
const onClickStop = useCallback(() => {
if (currStreamStateRef.current !== 'streaming') return
const onInterrupt = useCallback(() => {
if (getStreamState() !== 'streaming') return
const uri = getUriBeingApplied(applyBoxId)
if (!uri) return
editCodeService.interruptURIStreaming({ uri })
metricsService.capture('Stop Apply', {})
}, [currStreamStateRef, applyBoxId, editCodeService, metricsService])
}, [getStreamState, applyBoxId, editCodeService, metricsService])
const onAccept = useCallback(() => {
const uri = getUriBeingApplied(applyBoxId)
if (uri) editCodeService.acceptOrRejectAllDiffAreas({ uri: uri, behavior: 'accept', removeCtrlKs: false })
}, [uri, applyBoxId, editCodeService])
if (uri) editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false })
}, [applyBoxId, editCodeService])
const onReject = useCallback(() => {
const uri = getUriBeingApplied(applyBoxId)
if (uri) editCodeService.acceptOrRejectAllDiffAreas({ uri: uri, behavior: 'reject', removeCtrlKs: false })
}, [uri, applyBoxId, editCodeService])
if (uri) editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false })
}, [applyBoxId, editCodeService])
const currStreamState = currStreamStateRef.current
if (currStreamState === 'streaming') {
return <IconShell1
Icon={Square}
onClick={onClickStop}
onClick={onInterrupt}
{...tooltipPropsForApplyBlock({ tooltipName: 'Stop' })}
/>
}
if (isDisabled) {
return null
}
if (currStreamState === 'idle-no-changes') {
return <IconShell1
Icon={Play}
onClick={onClickSubmit}
{...tooltipPropsForApplyBlock({ tooltipName: 'Apply' })}
/>
}
if (currStreamState === 'idle-has-changes') {
return <Fragment>
<IconShell1
Icon={X}
onClick={onReject}
{...tooltipPropsForApplyBlock({ tooltipName: 'Remove' })}
/>
<IconShell1
Icon={Check}
onClick={onAccept}
{...tooltipPropsForApplyBlock({ tooltipName: 'Keep' })}
/>
</Fragment>
}
}
export const ApplyButtonsHTML = (params: {
codeStr: string,
applyBoxId: string,
language?: string,
uri: URI | 'current';
}) => {
const { language } = params
const isShellLanguage = !!language && terminalLanguages.has(language)
if (isShellLanguage) {
return <ApplyButtonsForTerminal {...params} />
}
else {
return <ApplyButtonsForEdit {...params} />
}
}
export const EditToolAcceptRejectButtonsHTML = ({
codeStr,
applyBoxId,
uri,
type,
threadId,
}: {
codeStr: string,
applyBoxId: string,
} & ({
uri: URI,
type: 'edit_file' | 'rewrite_file',
threadId: string,
})
) => {
const accessor = useAccessor()
const editCodeService = accessor.get('IEditCodeService')
const metricsService = accessor.get('IMetricsService')
const { streamState } = useEditToolStreamState({ applyBoxId, uri })
const settingsState = useSettingsState()
const chatThreadsStreamState = useChatThreadsStreamState(threadId)
const isRunning = chatThreadsStreamState?.isRunning
const isDisabled = !!isFeatureNameDisabled('Chat', settingsState) || !applyBoxId
const onAccept = useCallback(() => {
editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false })
}, [uri, applyBoxId, editCodeService])
const onReject = useCallback(() => {
editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false })
}, [uri, applyBoxId, editCodeService])
if (isDisabled) return null
if (streamState === 'idle-no-changes') {
return null
}
if (streamState === 'idle-has-changes') {
if (isRunning === 'LLM' || isRunning === 'tool') return null
return <>
<IconShell1
Icon={X}
@@ -506,13 +325,13 @@ export const EditToolAcceptRejectButtonsHTML = ({
export const BlockCodeApplyWrapper = ({
children,
codeStr,
initValue,
applyBoxId,
language,
canApply,
uri,
}: {
codeStr: string;
initValue: string;
children: React.ReactNode;
applyBoxId: string;
canApply: boolean;
@@ -521,8 +340,7 @@ export const BlockCodeApplyWrapper = ({
}) => {
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
const { currStreamStateRef } = useApplyStreamState({ applyBoxId })
const currStreamState = currStreamStateRef.current
const { currStreamState } = useApplyButtonState({ applyBoxId, uri })
const name = uri !== 'current' ?
@@ -530,7 +348,7 @@ export const BlockCodeApplyWrapper = ({
name={<span className='not-italic'>{getBasename(uri.fsPath)}</span>}
isSmall={true}
showDot={false}
onClick={() => { voidOpenFileFn(uri, accessor) }}
onClick={() => { commandService.executeCommand('vscode.open', uri, { preview: true }) }}
/>
: <span>{language}</span>
@@ -546,8 +364,8 @@ export const BlockCodeApplyWrapper = ({
</div>
<div className={`${canApply ? '' : 'hidden'} flex items-center gap-1`}>
<JumpToFileButton uri={uri} />
{currStreamState === 'idle-no-changes' && <CopyButton codeStr={codeStr} toolTipName='Copy' />}
<ApplyButtonsHTML uri={uri} applyBoxId={applyBoxId} codeStr={codeStr} language={language} />
{currStreamState === 'idle-no-changes' && <CopyButton codeStr={initValue} toolTipName='Copy' />}
<ApplyButtonsHTML uri={uri} applyBoxId={applyBoxId} codeStr={initValue} />
</div>
</div>
@@ -9,12 +9,12 @@ import { marked, MarkedToken, Token } from 'marked'
import { convertToVscodeLang, detectLanguage } from '../../../../common/helpers/languageHelpers.js'
import { BlockCodeApplyWrapper } from './ApplyBlockHoverButtons.js'
import { useAccessor } from '../util/services.js'
import { ScrollType } from '../../../../../../../editor/common/editorCommon.js'
import { URI } from '../../../../../../../base/common/uri.js'
import { isAbsolute } from '../../../../../../../base/common/path.js'
import { separateOutFirstLine } from '../../../../common/helpers/util.js'
import { BlockCode } from '../util/inputs.js'
import { CodespanLocationLink } from '../../../../common/chatThreadServiceTypes.js'
import { getBasename, getRelative, voidOpenFileFn } from '../sidebar-tsx/SidebarChat.js'
export type ChatMessageLocation = {
@@ -89,18 +89,13 @@ const LatexRender = ({ latex }: { latex: string }) => {
// }
}
const Codespan = ({ text, className, onClick, tooltip }: { text: string, className?: string, onClick?: () => void, tooltip?: string }) => {
const Codespan = ({ text, className, onClick }: { text: string, className?: string, onClick?: () => void }) => {
// TODO compute this once for efficiency. we should use `labels.ts/shorten` to display duplicates properly
return <code
className={`font-mono font-medium rounded-sm bg-void-bg-1 px-1 ${className}`}
onClick={onClick}
{...tooltip ? {
'data-tooltip-id': 'void-tooltip',
'data-tooltip-content': tooltip,
'data-tooltip-place': 'top',
} : {}}
>
{text}
</code>
@@ -120,11 +115,8 @@ const CodespanWithLink = ({ text, rawText, chatMessageLocation }: { text: string
const [didComputeCodespanLink, setDidComputeCodespanLink] = useState<boolean>(false)
let link: CodespanLocationLink | undefined = undefined
let tooltip: string | undefined = undefined
let displayText = text
if (rawText.endsWith('`')) { // if codespan was completed
if (rawText.endsWith('`')) {
// get link from cache
link = chatThreadService.getCodespanLink({ codespanStr: text, messageIdx, threadId })
@@ -135,33 +127,41 @@ const CodespanWithLink = ({ text, rawText, chatMessageLocation }: { text: string
chatThreadService.addCodespanLink({ newLinkText: text, newLinkLocation: link, messageIdx, threadId })
setDidComputeCodespanLink(true) // rerender
})
}
if (link?.displayText) {
displayText = link.displayText
}
if (isValidUri(displayText)) {
tooltip = getRelative(URI.file(displayText), accessor) // Full path as tooltip
displayText = getBasename(displayText)
}
}
const onClick = () => {
if (!link) return;
// Use the updated voidOpenFileFn to open the file and handle selection
if (link.selection)
voidOpenFileFn(link.uri, accessor, [link.selection.startLineNumber, link.selection.endLineNumber]);
else
voidOpenFileFn(link.uri, accessor);
const selection = link.selection
// open the file
commandService.executeCommand('vscode.open', link.uri).then(() => {
// select the text
setTimeout(() => {
if (!selection) return;
const editor = editorService.getActiveCodeEditor()
if (!editor) return;
editor.setSelection(selection)
editor.revealRange(selection, ScrollType.Immediate)
}, 50) // needed when document was just opened and needs to initialize
})
}
return <Codespan
text={displayText}
// text={link?.displayText || text}
text={link?.displayText || text}
onClick={onClick}
className={link ? 'underline hover:brightness-90 transition-all duration-200 cursor-pointer' : ''}
tooltip={tooltip || undefined}
/>
}
@@ -319,12 +319,12 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
return <BlockCodeApplyWrapper
canApply={isCodeblockClosed}
applyBoxId={applyBoxId}
codeStr={contents}
initValue={contents}
language={language}
uri={uri || 'current'}
>
<BlockCode
initValue={contents.trimEnd()} // \n\n adds a permanent newline which creates a flash
initValue={contents}
language={language}
/>
</BlockCodeApplyWrapper>
@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------*/
import React, { useEffect, useState } from 'react'
import { useIsDark } from '../util/services.js'
import { useIsDark, useSidebarState } from '../util/services.js'
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
import { QuickEditChat } from './QuickEditChat.js'
import { QuickEditPropsType } from '../../../quickEditActions.js'
@@ -71,7 +71,7 @@ export const QuickEditChat = ({
startBehavior: 'keep-conflicts',
} as const
await editCodeService.callBeforeApplyOrEdit(opts)
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 }) })
@@ -3,7 +3,7 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { useIsDark } from '../util/services.js';
import { useIsDark, useSidebarState } from '../util/services.js';
// import { SidebarThreadSelector } from './SidebarThreadSelector.js';
// import { SidebarChat } from './SidebarChat.js';
@@ -12,6 +12,8 @@ import { SidebarChat } from './SidebarChat.js';
import ErrorBoundary from './ErrorBoundary.js';
export const Sidebar = ({ className }: { className: string }) => {
const sidebarState = useSidebarState()
const { currentTab: tab } = sidebarState
const isDark = useIsDark()
return <div
@@ -27,12 +29,34 @@ export const Sidebar = ({ className }: { className: string }) => {
`}
>
<div className={`w-full h-full`}>
{/* <span onClick={() => {
const tabs = ['chat', 'settings', 'threadSelector']
const index = tabs.indexOf(tab)
sidebarStateService.setState({ currentTab: tabs[(index + 1) % tabs.length] as any })
}}>clickme {tab}</span> */}
{/* <div className={`w-full h-auto mb-2 ${isHistoryOpen ? '' : 'hidden'} ring-2 ring-widget-shadow z-10`}>
<ErrorBoundary>
<SidebarThreadSelector />
</ErrorBoundary>
</div> */}
<div className={`w-full h-full ${tab === 'chat' ? '' : 'hidden'}`}>
<ErrorBoundary>
<SidebarChat />
</ErrorBoundary>
{/* <ErrorBoundary>
<ModelSelectionSettings />
</ErrorBoundary> */}
</div>
{/* <div className={`w-full h-full ${tab === 'settings' ? '' : 'hidden'}`}>
<ErrorBoundary>
<VoidProviderSettings />
</ErrorBoundary>
</div> */}
</div>
</div>
File diff suppressed because it is too large Load Diff
@@ -11,6 +11,138 @@ import { Check, Copy, Icon, LoaderCircle, MessageCircleQuestion, Trash2, UserChe
import { IsRunningType, ThreadType } from '../../../chatThreadService.js';
export const OldSidebarThreadSelector = () => {
const accessor = useAccessor()
const sidebarStateService = accessor.get('ISidebarStateService')
return (
<div className="flex p-2 flex-col gap-y-1 max-h-[200px] overflow-y-auto">
<div className="w-full relative flex justify-center items-center">
{/* title */}
<h2 className='font-bold text-lg'>{`History`}</h2>
{/* X button at top right */}
<button
type='button'
className='absolute top-0 right-0'
onClick={() => sidebarStateService.setState({ isHistoryOpen: false })}
>
<IconX
size={16}
className="p-[1px] stroke-[2] opacity-80 text-void-fg-3 hover:brightness-95"
/>
</button>
</div>
{/* a list of all the past threads */}
{/* <OldPastThreadsList /> */}
</div>
)
}
const truncate = (s: string) => {
let len = s.length
const TRUNC_AFTER = 16
if (len >= TRUNC_AFTER)
s = s.substring(0, TRUNC_AFTER) + '...'
return s
}
const OldPastThreadsList = () => {
const accessor = useAccessor()
const chatThreadsService = accessor.get('IChatThreadService')
const sidebarStateService = accessor.get('ISidebarStateService')
const threadsState = useChatThreadsState()
const { allThreads } = threadsState
// sorted by most recent to least recent
const sortedThreadIds = Object.keys(allThreads ?? {})
.sort((threadId1, threadId2) => (allThreads[threadId1]?.lastModified ?? 0) > (allThreads[threadId2]?.lastModified ?? 0) ? -1 : 1)
.filter(threadId => (allThreads![threadId]?.messages.length ?? 0) !== 0)
return <div className="px-1">
<ul className="flex flex-col gap-y-0.5 overflow-y-auto list-disc">
{sortedThreadIds.length === 0
? <div key="nothreads" className="text-center text-void-fg-3 brightness-90 text-root">{`There are no chat threads yet.`}</div>
: sortedThreadIds.map((threadId) => {
if (!allThreads) {
return <li key="error" className="text-void-warning">{`Error accessing chat history.`}</li>;
}
const pastThread = allThreads[threadId];
if (!pastThread) {
return <li key="error" className="text-void-warning">{`Error accessing chat history.`}</li>;
}
let firstMsg = null;
// let secondMsg = null;
const firstUserMsgIdx = pastThread.messages.findIndex((msg) => msg.role === 'user');
if (firstUserMsgIdx !== -1) {
// firstMsg = truncate(pastThread.messages[firstMsgIdx].displayContent ?? '');
const firsUsertMsgObj = pastThread.messages[firstUserMsgIdx]
firstMsg = firsUsertMsgObj.role === 'user' && firsUsertMsgObj.displayContent || '';
} else {
firstMsg = '""';
}
// const secondMsgIdx = pastThread.messages.findIndex(
// (msg, i) => msg.role !== 'system' && !!msg.displayContent && i > firstMsgIdx
// );
// if (secondMsgIdx !== -1) {
// secondMsg = truncate(pastThread.messages[secondMsgIdx].displayContent ?? '');
// }
const numMessages = pastThread.messages.filter((msg) => msg.role === 'assistant' || msg.role === 'user').length;
return (
<li key={pastThread.id}>
<button
type='button'
className={`
hover:bg-void-bg-1
${threadsState.currentThreadId === pastThread.id ? 'bg-void-bg-1' : ''}
rounded-sm px-2 py-1
w-full
text-left
flex items-center
`}
onClick={() => {
chatThreadsService.switchToThread(pastThread.id);
sidebarStateService.setState({ isHistoryOpen: false })
}}
title={new Date(pastThread.lastModified).toLocaleString()}
>
<div className='truncate'>{`${firstMsg}`}</div>
<div>{`\u00A0(${numMessages})`}</div>
</button>
</li>
);
})
}
</ul>
</div>
}
const numInitialThreads = 3
export const PastThreadsList = ({ className = '' }: { className?: string }) => {
@@ -181,6 +313,7 @@ const PastThreadElement = ({ pastThread, idx, hoveredIdx, setHoveredIdx, isRunni
const accessor = useAccessor()
const chatThreadsService = accessor.get('IChatThreadService')
const sidebarStateService = accessor.get('ISidebarStateService')
// const settingsState = useSettingsState()
// const convertService = accessor.get('IConvertToLLMMessageService')
@@ -220,14 +353,13 @@ const PastThreadElement = ({ pastThread, idx, hoveredIdx, setHoveredIdx, isRunni
const numMessages = pastThread.messages.filter((msg) => msg.role === 'assistant' || msg.role === 'user').length;
const detailsHTML = <span
className='gap-1 inline-flex items-center'
// data-tooltip-id='void-tooltip'
// data-tooltip-content={`Last modified ${formatTime(new Date(pastThread.lastModified))}`}
// data-tooltip-place='top'
>
<span className='opacity-60'>{numMessages}</span>
{` `}
{/* <span>{numMessages}</span> */}
{formatDate(new Date(pastThread.lastModified))}
{/* {` messages `} */}
</span>
return <div
@@ -237,6 +369,7 @@ const PastThreadElement = ({ pastThread, idx, hoveredIdx, setHoveredIdx, isRunni
`}
onClick={() => {
chatThreadsService.switchToThread(pastThread.id);
sidebarStateService.setState({ isHistoryOpen: false });
}}
onMouseEnter={() => setHoveredIdx(idx)}
onMouseLeave={() => setHoveredIdx(null)}
@@ -244,19 +377,15 @@ const PastThreadElement = ({ pastThread, idx, hoveredIdx, setHoveredIdx, isRunni
<div className="flex items-center justify-between gap-1">
<span className="flex items-center gap-2 min-w-0 overflow-hidden">
{/* spinner */}
{isRunning === 'LLM' || isRunning === 'tool' || isRunning === 'idle' ? <LoaderCircle className="animate-spin bg-void-stroke-1 flex-shrink-0 flex-grow-0" size={14} />
{isRunning === 'LLM' || isRunning === 'tool' ? <LoaderCircle className="animate-spin bg-void-stroke-1 flex-shrink-0 flex-grow-0" size={14} />
:
isRunning === 'awaiting_user' ? <MessageCircleQuestion className="bg-void-stroke-1 flex-shrink-0 flex-grow-0" size={14} />
:
null}
{/* name */}
<span className="truncate overflow-hidden text-ellipsis"
data-tooltip-id='void-tooltip'
data-tooltip-content={numMessages + ' messages'}
data-tooltip-place='top'
>{firstMsg}</span>
<span className="truncate overflow-hidden text-ellipsis">{firstMsg}</span>
{/* <span className='opacity-60'>{`(${numMessages})`}</span> */}
<span className='opacity-60'>{`(${numMessages})`}</span>
</span>
<div className="flex items-center gap-x-1 opacity-60">
@@ -9,13 +9,10 @@ type ReturnType<T> = [
// use this if state might be too slow to catch
export const useRefState = <T,>(initVal: T): ReturnType<T> => {
// this actually makes a difference being an int, not a boolean.
// if it's a boolean and changes happen to fast, it goes with old values and leads to *very* weird bugs (like returning JSX, but not actually rendering it)
const [_s, _setState] = useState(0)
const [_, _setState] = useState(false)
const ref = useRef<T>(initVal)
const setState = useCallback((newVal: T) => {
_setState(n => n + 1) // call rerender
_setState(n => !n) // call rerender
ref.current = newVal
}, [])
return [ref, setState]
@@ -17,7 +17,7 @@ import { asCssVariable } from '../../../../../../../platform/theme/common/colorU
import { inputBackground, inputForeground } from '../../../../../../../platform/theme/common/colorRegistry.js';
import { useFloating, autoUpdate, offset, flip, shift, size, autoPlacement } from '@floating-ui/react';
import { URI } from '../../../../../../../base/common/uri.js';
import { getBasename, getFolderName } from '../sidebar-tsx/SidebarChat.js';
import { getBasename } from '../sidebar-tsx/SidebarChat.js';
import { ChevronRight, File, Folder, FolderClosed, LucideProps } from 'lucide-react';
import { StagingSelectionItem } from '../../../../common/chatThreadServiceTypes.js';
@@ -55,13 +55,12 @@ export const WidgetComponent = <CtorParams extends any[], Instance>({ ctor, prop
type GenerateNextOptions = (optionText: string) => Promise<Option[]>
type Option = {
fullName: string,
abbreviatedName: string,
nameInMenu: string,
iconInMenu: ForwardRefExoticComponent<Omit<LucideProps, "ref"> & RefAttributes<SVGSVGElement>>, // type for lucide-react components
} & (
| { leafNodeType?: undefined, nextOptions: Option[], generateNextOptions?: undefined, }
| { leafNodeType?: undefined, nextOptions?: undefined, generateNextOptions: GenerateNextOptions, }
| { leafNodeType: 'File' | 'Folder', uri: URI, nextOptions?: undefined, generateNextOptions?: undefined, }
| { nextOptions: Option[], generateNextOptions?: undefined, nameToPaste?: undefined }
| { nextOptions?: undefined, generateNextOptions: GenerateNextOptions, nameToPaste?: undefined }
| { leafNodeType: 'File' | 'Folder', nameToPaste: string, uri: URI, nextOptions?: undefined, generateNextOptions?: undefined, }
)
@@ -132,7 +131,7 @@ const scoreSubsequence = (text: string, pattern: string): number => {
}
function getRelativeWorkspacePath(accessor: ReturnType<typeof useAccessor>, uri: URI): string {
export function getRelativeWorkspacePath(accessor: ReturnType<typeof useAccessor>, uri: URI): string {
const workspaceService = accessor.get('IWorkspaceContextService');
const workspaceFolders = workspaceService.getWorkspace().folders;
@@ -160,7 +159,7 @@ function getRelativeWorkspacePath(accessor: ReturnType<typeof useAccessor>, uri:
if (relativePath.startsWith('/')) {
relativePath = relativePath.slice(1);
}
// console.log({ folderPath, relativePath, uriPath });
console.log({ folderPath, relativePath, uriPath });
return relativePath;
}
@@ -174,19 +173,10 @@ function getRelativeWorkspacePath(accessor: ReturnType<typeof useAccessor>, uri:
const numOptionsToShow = 100
// TODO make this unique based on other options
const getAbbreviatedName = (relativePath: string) => {
return getBasename(relativePath, 1)
}
const getOptionsAtPath = async (accessor: ReturnType<typeof useAccessor>, path: string[], optionText: string): Promise<Option[]> => {
const toolsService = accessor.get('IToolsService')
const searchForFilesOrFolders = async (t: string, searchFor: 'files' | 'folders') => {
try {
@@ -203,8 +193,8 @@ const getOptionsAtPath = async (accessor: ReturnType<typeof useAccessor>, path:
leafNodeType: 'File',
uri: uri,
iconInMenu: File,
fullName: relativePath,
abbreviatedName: getAbbreviatedName(relativePath),
nameInMenu: relativePath,
nameToPaste: getBasename(relativePath, 2),
}
})
return res
@@ -251,6 +241,7 @@ const getOptionsAtPath = async (accessor: ReturnType<typeof useAccessor>, path:
for (let i = 0; i < pathParts.length - 1; i++) {
currentPath = i === 0 ? `/${pathParts[i]}` : `${currentPath}/${pathParts[i]}`;
console.log('filepath', currentPath);
// Create a proper directory URI
const directoryUri = URI.joinPath(
@@ -267,8 +258,8 @@ const getOptionsAtPath = async (accessor: ReturnType<typeof useAccessor>, path:
leafNodeType: 'Folder',
uri: uri,
iconInMenu: Folder, // Folder
fullName: relativePath,
abbreviatedName: getAbbreviatedName(relativePath),
nameInMenu: relativePath,
nameToPaste: getBasename(relativePath, 2)
})) satisfies Option[];
}
} catch (error) {
@@ -280,15 +271,13 @@ const getOptionsAtPath = async (accessor: ReturnType<typeof useAccessor>, path:
const allOptions: Option[] = [
{
fullName: 'files',
abbreviatedName: 'files',
nameInMenu: 'files',
iconInMenu: File,
generateNextOptions: async (t) => (await searchForFilesOrFolders(t, 'files')) || [],
},
{
fullName: 'folders',
abbreviatedName: 'folders',
iconInMenu: Folder,
nameInMenu: 'folders',
iconInMenu: FolderClosed,
generateNextOptions: async (t) => (await searchForFilesOrFolders(t, 'folders')) || [],
},
]
@@ -300,7 +289,7 @@ const getOptionsAtPath = async (accessor: ReturnType<typeof useAccessor>, path:
for (const pn of path) {
const selectedOption = nextOptionsAtPath.find(o => o.fullName.toLowerCase() === pn.toLowerCase())
const selectedOption = nextOptionsAtPath.find(o => o.nameInMenu.toLowerCase() === pn.toLowerCase())
if (!selectedOption) return [];
@@ -311,20 +300,14 @@ const getOptionsAtPath = async (accessor: ReturnType<typeof useAccessor>, path:
if (generateNextOptionsAtPath) {
nextOptionsAtPath = await generateNextOptionsAtPath(optionText)
}
else if (path.length === 0 && optionText.trim().length > 0) { // (special case): directly search for both files and folders if optionsPath is empty and there's a search term
const filesResults = await searchForFilesOrFolders(optionText, 'files') || [];
const foldersResults = await searchForFilesOrFolders(optionText, 'folders') || [];
nextOptionsAtPath = [...foldersResults, ...filesResults,]
}
const optionsAtPath = nextOptionsAtPath
.filter(o => isSubsequence(o.fullName, optionText))
.filter(o => isSubsequence(o.nameInMenu, optionText))
.sort((a, b) => { // this is a hack but good for now
const scoreA = scoreSubsequence(a.fullName, optionText);
const scoreB = scoreSubsequence(b.fullName, optionText);
const scoreA = scoreSubsequence(a.nameInMenu, optionText);
const scoreB = scoreSubsequence(b.nameInMenu, optionText);
return scoreB - scoreA;
})
.slice(0, numOptionsToShow) // should go last because sorting/filtering should happen on all datapoints
@@ -371,14 +354,6 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
const [optionIdx, setOptionIdx] = useState<number>(0);
const [options, setOptions] = useState<Option[]>([]);
const [optionText, setOptionText] = useState<string>('');
const [didLoadInitialOptions, setDidLoadInitialOptions] = useState(false);
const currentPathRef = useRef<string>(JSON.stringify([]));
// dont show breadcrums if first page and user hasnt typed anything
const isTypingEnabled = true
const isBreadcrumbsShowing = optionPath.length === 0 && !optionText ? false : true
const insertTextAtCursor = (text: string) => {
const textarea = textAreaRef.current;
if (!textarea) return;
@@ -386,21 +361,9 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
// Focus the textarea first
textarea.focus();
// delete the @ and set the cursor position
// Get cursor position
const startPos = textarea.selectionStart;
const endPos = textarea.selectionEnd;
// Get the text before the cursor, excluding the @ symbol that triggered the menu
const textBeforeCursor = textarea.value.substring(0, startPos - 1);
const textAfterCursor = textarea.value.substring(endPos);
// Replace the text including the @ symbol with the selected option
textarea.value = textBeforeCursor + textAfterCursor;
// Set cursor position after the inserted text
const newCursorPos = textBeforeCursor.length;
textarea.setSelectionRange(newCursorPos, newCursorPos);
// The most reliable way to simulate typing is to use execCommand
// which will trigger all the appropriate native events
document.execCommand('insertText', false, text + ' '); // add space after too
// React's onChange relies on a SyntheticEvent system
// The best way to ensure it runs is to call callbacks directly
@@ -416,64 +379,51 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
if (!options.length) { return; }
const option = options[optionIdx];
const newPath = [...optionPath, option.fullName]
const newPath = [...optionPath, option.nameInMenu]
const isLastOption = !option.generateNextOptions && !option.nextOptions
setDidLoadInitialOptions(false)
setOptionPath(newPath)
setOptionText('')
setOptionIdx(0)
if (isLastOption) {
setIsMenuOpen(false)
insertTextAtCursor(option.abbreviatedName)
insertTextAtCursor(option.nameToPaste)
let newSelection: StagingSelectionItem
if (option.leafNodeType === 'File') newSelection = {
const newSelection: StagingSelectionItem = option.leafNodeType === 'File' ? {
type: 'File',
uri: option.uri,
language: languageService.guessLanguageIdByFilepathOrFirstLine(option.uri) || '',
state: { wasAddedAsCurrentFile: false },
}
else if (option.leafNodeType === 'Folder') newSelection = {
state: { wasAddedAsCurrentFile: false }
} : option.leafNodeType === 'Folder' ? {
type: 'Folder',
uri: option.uri,
language: undefined,
state: undefined,
}
else throw new Error(`Unexpected leafNodeType ${option.leafNodeType}`)
} : (undefined as never)
chatThreadService.addNewStagingSelection(newSelection)
console.log('selected', option.uri?.fsPath)
}
else {
currentPathRef.current = JSON.stringify(newPath);
const newOpts = await getOptionsAtPath(accessor, newPath, '') || []
if (currentPathRef.current !== JSON.stringify(newPath)) { return; }
setOptionPath(newPath)
setOptionText('')
setOptionIdx(0)
setOptions(newOpts)
setDidLoadInitialOptions(true)
}
}
const onRemoveOption = async () => {
const newPath = [...optionPath.slice(0, optionPath.length - 1)]
currentPathRef.current = JSON.stringify(newPath);
const newOpts = await getOptionsAtPath(accessor, newPath, '') || []
if (currentPathRef.current !== JSON.stringify(newPath)) { return; }
setOptionPath(newPath)
setOptionText('')
setOptionIdx(0)
const newOpts = await getOptionsAtPath(accessor, newPath, '') || []
setOptions(newOpts)
}
const onOpenOptionMenu = async () => {
const newPath: [] = []
currentPathRef.current = JSON.stringify([]);
const newOpts = await getOptionsAtPath(accessor, [], '') || []
if (currentPathRef.current !== JSON.stringify([])) { return; }
setOptionPath(newPath)
setOptionPath([])
setOptionText('')
setIsMenuOpen(true);
setOptionIdx(0);
const newOpts = await getOptionsAtPath(accessor, [], '') || []
setOptions(newOpts);
}
const onCloseOptionMenu = () => {
@@ -516,36 +466,24 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
};
}, []);
// debounced, but immediate if text is empty
// debounced
const onPathTextChange = useCallback((newStr: string) => {
setOptionText(newStr);
if (debounceTimerRef.current !== null) {
window.clearTimeout(debounceTimerRef.current);
}
currentPathRef.current = JSON.stringify(optionPath);
const fetchOptions = async () => {
// Set a new timeout to fetch options after a delay
debounceTimerRef.current = window.setTimeout(async () => {
const newOpts = await getOptionsAtPath(accessor, optionPath, newStr) || [];
if (currentPathRef.current !== JSON.stringify(optionPath)) { return; }
setOptions(newOpts);
setOptionIdx(0);
debounceTimerRef.current = null;
};
// If text is empty, run immediately without debouncing
if (newStr.trim() === '') {
fetchOptions();
} else {
// Otherwise, set a new timeout to fetch options after a delay
debounceTimerRef.current = window.setTimeout(fetchOptions, 300);
}
}, 300);
}, [optionPath, accessor]);
const onMenuKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
const isCommandKeyPressed = e.altKey || e.ctrlKey || e.metaKey;
@@ -599,9 +537,7 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
// do nothing
}
else { // letter
if (isTypingEnabled) {
onPathTextChange(optionText + e.key)
}
onPathTextChange(optionText + e.key)
}
}
@@ -779,16 +715,6 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
return;
}
if (e.key === 'Backspace') { // TODO allow user to undo this.
if (!e.currentTarget.value || (e.currentTarget.selectionStart === 0 && e.currentTarget.selectionEnd === 0)) { // if there is no text or cursor is at position 0, remove a selection
if (e.metaKey || e.ctrlKey) { // Ctrl+Backspace = remove all
chatThreadService.popStagingSelections(Number.MAX_SAFE_INTEGER)
} else { // Backspace = pop 1 selection
chatThreadService.popStagingSelections(1)
}
return;
}
}
if (e.key === 'Enter') {
// Shift + Enter when multiline = newline
const shouldAddNewline = e.shiftKey && multiline
@@ -814,25 +740,25 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
onWheel={(e) => e.stopPropagation()}
>
{/* Breadcrumbs Header */}
{isBreadcrumbsShowing && <div className="px-2 py-1 text-void-fg-1 bg-void-bg-2-alt border-b border-void-border-3 sticky top-0 bg-void-bg-1 z-10 select-none pointer-events-none">
{optionText ?
<div className="px-2 py-1 text-void-fg-3 bg-void-bg-2-alt text-sm border-b border-void-border-3 sticky top-0 bg-void-bg-1 z-10 select-none pointer-events-none">
{optionPath.length || optionText ?
<div className="flex items-center">
{/* {optionPath.map((path, index) => (
{optionPath.map((path, index) => (
<React.Fragment key={index}>
<span>{path}</span>
<ChevronRight size={12} className="mx-1" />
</React.Fragment>
))} */}
))}
<span>{optionText}</span>
</div>
: <div className='opacity-50'>Enter text to filter...</div>
: <div className='opacity-60'>Enter text to filter...</div>
}
</div>}
</div>
{/* Options list */}
<div className='max-h-[400px] w-full max-w-full overflow-y-auto overflow-x-auto'>
<div className="w-max min-w-full flex flex-col gap-0 text-nowrap flex-nowrap">
<div className="w-max min-w-full flex flex-col gap-0 text-nowrap flex-nowrap text-sm opacity-70">
{options.length === 0 ?
<div className="text-void-fg-3 px-3 py-0.5">No results found</div>
: options.map((o, oIdx) => {
@@ -841,25 +767,20 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
// Option
<div
ref={oIdx === optionIdx ? selectedOptionRef : null}
key={o.fullName}
key={o.nameInMenu}
className={`
flex items-center gap-2
px-3 py-1 cursor-pointer
${oIdx === optionIdx ? 'bg-blue-500 text-white/80' : 'bg-void-bg-2-alt text-void-fg-1'}
px-3 py-0.5 cursor-pointer bg-void-bg-2-alt
${oIdx === optionIdx ? 'bg-void-bg-2-hover' : ''}
`}
onClick={() => { onSelectOption(); }}
onMouseMove={() => { setOptionIdx(oIdx) }}
onMouseOver={() => { setOptionIdx(oIdx) }}
>
{<o.iconInMenu size={12} />}
<span>{o.abbreviatedName}</span>
{o.fullName && o.fullName !== o.abbreviatedName && <span className="opacity-60 text-sm">{o.fullName}</span>}
<span className="text-void-fg-1">{o.nameInMenu}</span>
{o.nextOptions || o.generateNextOptions ? (
<ChevronRight size={12} />
) : null}
</div>
)
})
@@ -882,44 +803,15 @@ export const VoidSimpleInputBox = ({ value, onChangeValue, placeholder, classNam
compact?: boolean;
passwordBlur?: boolean;
} & React.InputHTMLAttributes<HTMLInputElement>) => {
// Create a ref for the input element to maintain the same DOM node between renders
const inputRef = useRef<HTMLInputElement>(null);
// Track if we need to restore selection
const selectionRef = useRef<{ start: number | null, end: number | null }>({
start: null,
end: null
});
// Handle value changes without recreating the input
useEffect(() => {
const input = inputRef.current;
if (input && input.value !== value) {
// Store current selection positions
selectionRef.current.start = input.selectionStart;
selectionRef.current.end = input.selectionEnd;
// Update the value
input.value = value;
// Restore selection if we had it before
if (selectionRef.current.start !== null && selectionRef.current.end !== null) {
input.setSelectionRange(selectionRef.current.start, selectionRef.current.end);
}
}
}, [value]);
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
onChangeValue(e.target.value);
}, [onChangeValue]);
return (
<input
ref={inputRef}
defaultValue={value} // Use defaultValue instead of value to avoid recreation
onChange={handleChange}
value={value}
onChange={(e) => onChangeValue(e.target.value)}
placeholder={placeholder}
disabled={disabled}
// className='max-w-44 w-full border border-void-border-2 bg-void-bg-1 text-void-fg-3 text-root'
// className={`w-full resize-none text-void-fg-1 placeholder:text-void-fg-3 px-2 py-1 rounded-sm
className={`w-full resize-none bg-void-bg-1 text-void-fg-1 placeholder:text-void-fg-3 border border-void-border-2 focus:border-void-border-1
${compact ? 'py-1 px-2' : 'py-2 px-4 '}
rounded
@@ -1437,7 +1329,7 @@ export const VoidCustomDropdownBox = <T extends NonNullable<any>>({
key={optionName}
className={`flex items-center px-2 py-1 pr-4 cursor-pointer whitespace-nowrap
transition-all duration-100
${thisOptionIsSelected ? 'bg-blue-500 text-white/80' : 'hover:bg-blue-500 hover:text-white/80'}
${thisOptionIsSelected ? 'bg-void-bg-2-hover' : 'bg-void-bg-2-alt hover:bg-void-bg-2-hover'}
`}
onClick={() => {
onChangeOption(option);
@@ -1459,7 +1351,7 @@ export const VoidCustomDropdownBox = <T extends NonNullable<any>>({
</div>
<span className="flex justify-between items-center w-full gap-x-1">
<span>{optionName}</span>
<span className='opacity-60'>{optionDetail}</span>
<span className='text-void-fg-4 opacity-60'>{optionDetail}</span>
</span>
</div>
);
@@ -6,6 +6,7 @@
import React, { useState, useEffect, useCallback } from 'react'
import { RefreshableProviderName, SettingsOfProvider } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'
import { IDisposable } from '../../../../../../../base/common/lifecycle.js'
import { VoidSidebarState } from '../../../sidebarStateService.js'
import { VoidSettingsState } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js'
import { ColorScheme } from '../../../../../../../platform/theme/common/theme.js'
import { RefreshModelStateOfProvider } from '../../../../../../../workbench/contrib/void/common/refreshModelService.js'
@@ -21,8 +22,8 @@ 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 { IExtensionTransferService } from '../../../../../../../workbench/contrib/void/browser/extensionTransferService.js'
import { ISidebarStateService } from '../../../sidebarStateService.js';
import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js'
import { ICodeEditorService } from '../../../../../../../editor/browser/services/codeEditorService.js'
import { ICommandService } from '../../../../../../../platform/commands/common/commands.js'
@@ -48,9 +49,8 @@ import { INativeHostService } from '../../../../../../../platform/native/common/
import { IEditCodeService } from '../../../editCodeServiceInterface.js'
import { IToolsService } from '../../../toolsService.js'
import { IConvertToLLMMessageService } from '../../../convertToLLMMessageService.js'
import { ITerminalService } from '../../../../../terminal/browser/terminal.js'
import { ISearchService } from '../../../../../../services/search/common/search.js'
import { IExtensionManagementService } from '../../../../../../../platform/extensionManagement/common/extensionManagement.js'
import { ITerminalService } from '../../../../../terminal/browser/terminal.js'
// normally to do this you'd use a useEffect that calls .onDidChangeState(), but useEffect mounts too late and misses initial state changes
@@ -58,6 +58,9 @@ import { IExtensionManagementService } from '../../../../../../../platform/exten
// even if React hasn't mounted yet, the variables are always updated to the latest state.
// React listens by adding a setState function to these listeners.
let sidebarState: VoidSidebarState
const sidebarStateListeners: Set<(s: VoidSidebarState) => void> = new Set()
let chatThreadsState: ThreadsState
const chatThreadsStateListeners: Set<(s: ThreadsState) => void> = new Set()
@@ -88,6 +91,7 @@ export const _registerServices = (accessor: ServicesAccessor) => {
_registerAccessor(accessor)
const stateServices = {
sidebarStateService: accessor.get(ISidebarStateService),
chatThreadsStateService: accessor.get(IChatThreadService),
settingsStateService: accessor.get(IVoidSettingsService),
refreshModelService: accessor.get(IRefreshModelService),
@@ -97,10 +101,15 @@ export const _registerServices = (accessor: ServicesAccessor) => {
modelService: accessor.get(IModelService),
}
const { settingsStateService, chatThreadsStateService, refreshModelService, themeService, editCodeService, voidCommandBarService, modelService } = stateServices
const { sidebarStateService, settingsStateService, chatThreadsStateService, refreshModelService, themeService, editCodeService, voidCommandBarService, modelService } = stateServices
sidebarState = sidebarStateService.state
disposables.push(
sidebarStateService.onDidChangeState(() => {
sidebarState = sidebarStateService.state
sidebarStateListeners.forEach(l => l(sidebarState))
})
)
chatThreadsState = chatThreadsStateService.state
disposables.push(
@@ -138,8 +147,8 @@ export const _registerServices = (accessor: ServicesAccessor) => {
colorThemeState = themeService.getColorTheme().type
disposables.push(
themeService.onDidColorThemeChange(({ type }) => {
colorThemeState = type
themeService.onDidColorThemeChange(({ theme }) => {
colorThemeState = theme.type
colorThemeStateListeners.forEach(l => l(colorThemeState))
})
)
@@ -184,6 +193,7 @@ const getReactAccessor = (accessor: ServicesAccessor) => {
IRefreshModelService: accessor.get(IRefreshModelService),
IVoidSettingsService: accessor.get(IVoidSettingsService),
IEditCodeService: accessor.get(IEditCodeService),
ISidebarStateService: accessor.get(ISidebarStateService),
IChatThreadService: accessor.get(IChatThreadService),
IInstantiationService: accessor.get(IInstantiationService),
@@ -213,8 +223,6 @@ const getReactAccessor = (accessor: ServicesAccessor) => {
IToolsService: accessor.get(IToolsService),
IConvertToLLMMessageService: accessor.get(IConvertToLLMMessageService),
ITerminalService: accessor.get(ITerminalService),
IExtensionManagementService: accessor.get(IExtensionManagementService),
IExtensionTransferService: accessor.get(IExtensionTransferService),
} as const
return reactAccessor
@@ -242,6 +250,16 @@ export const useAccessor = () => {
// -- state of services --
export const useSidebarState = () => {
const [s, ss] = useState(sidebarState)
useEffect(() => {
ss(sidebarState)
sidebarStateListeners.add(ss)
return () => { sidebarStateListeners.delete(ss) }
}, [ss])
return s
}
export const useSettingsState = () => {
const [s, ss] = useState(settingsState)
useEffect(() => {
@@ -9,19 +9,9 @@ import { useAccessor, useCommandBarState, useIsDark } from '../util/services.js'
import '../styles.css'
import { useCallback, useEffect, useState, useRef } from 'react';
import { ScrollType } from '../../../../../../../editor/common/editorCommon.js';
import { acceptAllBg, acceptBorder, buttonFontSize, buttonTextColor, rejectAllBg, rejectBg, rejectBorder } from '../../../../common/helpers/colors.js';
import { acceptAllBg, acceptBorder, buttonFontSize, buttonTextColor, rejectBg, rejectBorder } from '../../../../common/helpers/colors.js';
import { VoidCommandBarProps } from '../../../voidCommandBarService.js';
import { Check, EllipsisVertical, Menu, MoveDown, MoveLeft, MoveRight, MoveUp, X } from 'lucide-react';
import {
VOID_GOTO_NEXT_DIFF_ACTION_ID,
VOID_GOTO_PREV_DIFF_ACTION_ID,
VOID_GOTO_NEXT_URI_ACTION_ID,
VOID_GOTO_PREV_URI_ACTION_ID,
VOID_ACCEPT_FILE_ACTION_ID,
VOID_REJECT_FILE_ACTION_ID,
VOID_ACCEPT_ALL_DIFFS_ACTION_ID,
VOID_REJECT_ALL_DIFFS_ACTION_ID
} from '../../../actionIDs.js';
import { AcceptAllButtonWrapper, RejectAllButtonWrapper } from '../sidebar-tsx/SidebarChat.js';
export const VoidCommandBarMain = ({ uri, editor }: VoidCommandBarProps) => {
const isDark = useIsDark()
@@ -35,55 +25,14 @@ export const VoidCommandBarMain = ({ uri, editor }: VoidCommandBarProps) => {
export const AcceptAllButtonWrapper = ({ text, onClick, className, ...props }: { text: string, onClick: () => void, className?: string } & React.ButtonHTMLAttributes<HTMLButtonElement>) => (
<button
className={`
px-2 py-0.5
flex items-center gap-1
text-white text-[11px] text-nowrap
h-full rounded-none
cursor-pointer
${className}
`}
style={{
backgroundColor: 'var(--vscode-button-background)',
color: 'var(--vscode-button-foreground)',
border: 'none',
}}
type='button'
onClick={onClick}
{...props}
>
{text ? <span>{text}</span> : <Check size={16} />}
</button>
)
export const RejectAllButtonWrapper = ({ text, onClick, className, ...props }: { text: string, onClick: () => void, className?: string } & React.ButtonHTMLAttributes<HTMLButtonElement>) => (
<button
className={`
px-2 py-0.5
flex items-center gap-1
text-white text-[11px] text-nowrap
h-full rounded-none
cursor-pointer
${className}
`}
style={{
backgroundColor: 'var(--vscode-button-secondaryBackground)',
color: 'var(--vscode-button-secondaryForeground)',
border: 'none',
}}
type='button'
onClick={onClick}
{...props}
>
{text ? <span>{text}</span> : <X size={16} />}
</button>
)
const stepIdx = (currIdx: number | null, len: number, step: -1 | 1) => {
if (len === 0) return null
return ((currIdx ?? 0) + step + len) % len // for some reason, small negatives are kept negative. just add len to offset
}
export const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
const accessor = useAccessor()
const editCodeService = accessor.get('IEditCodeService')
const editorService = accessor.get('ICodeEditorService')
@@ -91,9 +40,12 @@ export const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
const commandService = accessor.get('ICommandService')
const commandBarService = accessor.get('IVoidCommandBarService')
const voidModelService = accessor.get('IVoidModelService')
const keybindingService = accessor.get('IKeybindingService')
const { stateOfURI: commandBarState, sortedURIs: sortedCommandBarURIs } = useCommandBarState()
const [showAcceptRejectAllButtons, setShowAcceptRejectAllButtons] = useState(false)
// useEffect(() => {
// console.log('MOUNTING!!!')
// }, [])
// latestUriIdx is used to remember place in leftRight
const _latestValidUriIdxRef = useRef<number | null>(null)
@@ -118,18 +70,56 @@ export const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
const s = commandBarService.stateOfURI[uri.fsPath]
if (!s) return
const { diffIdx } = s
commandBarService.goToDiffIdx(diffIdx ?? 0)
goToDiffIdx(diffIdx ?? 0)
}, 50)
}, [uri, commandBarService])
if (uri?.scheme !== 'file') return null // don't show in editors that we made, they must be files
// Using service methods directly
const getNextDiffIdx = (step: 1 | -1) => {
// check undefined
if (!uri) return null
const s = commandBarState[uri.fsPath]
if (!s) return null
const { diffIdx, sortedDiffIds } = s
// get next idx
const nextDiffIdx = stepIdx(diffIdx, sortedDiffIds.length, step)
return nextDiffIdx
}
const goToDiffIdx = (idx: number | null) => {
if (idx === null) return
// check undefined
if (!uri) return
const s = commandBarState[uri.fsPath]
if (!s) return
const { sortedDiffIds } = s
// reveal
const diffid = sortedDiffIds[idx]
if (diffid === undefined) return
const diff = editCodeService.diffOfId[diffid]
if (!diff) return
editor.revealLineNearTop(diff.startLine - 1, ScrollType.Immediate)
commandBarService.setDiffIdx(uri, idx)
}
const getNextUriIdx = (step: 1 | -1) => {
return stepIdx(uriIdxInStepper, sortedCommandBarURIs.length, step)
}
const goToURIIdx = async (idx: number | null) => {
if (idx === null) return
const nextURI = sortedCommandBarURIs[idx]
editCodeService.diffAreasOfURI
const { model } = await voidModelService.getModelSafe(nextURI)
if (model) {
// switch to the URI
editorService.openCodeEditor({ resource: model.uri, options: { revealIfVisible: true } }, editor)
}
}
const currDiffIdx = uri ? commandBarState[uri.fsPath]?.diffIdx ?? null : null
const sortedDiffIds = uri ? commandBarState[uri.fsPath]?.sortedDiffIds ?? [] : []
const sortedDiffZoneIds = uri ? commandBarState[uri.fsPath]?.sortedDiffZoneIds ?? [] : []
const isADiffInThisFile = sortedDiffIds.length !== 0
const isADiffZoneInThisFile = sortedDiffZoneIds.length !== 0
const isADiffZoneInAnyFile = sortedCommandBarURIs.length !== 0
@@ -137,247 +127,191 @@ export const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
const streamState = uri ? commandBarService.getStreamState(uri) : null
const showAcceptRejectAll = streamState === 'idle-has-changes'
const nextDiffIdx = commandBarService.getNextDiffIdx(1)
const prevDiffIdx = commandBarService.getNextDiffIdx(-1)
const nextURIIdx = commandBarService.getNextUriIdx(1)
const prevURIIdx = commandBarService.getNextUriIdx(-1)
const nextDiffIdx = getNextDiffIdx(1)
const prevDiffIdx = getNextDiffIdx(-1)
const nextURIIdx = getNextUriIdx(1)
const prevURIIdx = getNextUriIdx(-1)
const upDownDisabled = prevDiffIdx === null || nextDiffIdx === null
const leftRightDisabled = prevURIIdx === null || nextURIIdx === null
const leftRightDisabled = prevURIIdx === null || nextURIIdx === null // || (sortedCommandBarURIs.length === 1 && isADiffZoneInThisFile)
const upButton = <button
className={`
size-6 rounded cursor-default
hover:bg-void-bg-1-alt
`}// --border border-void-border-3 focus:border-void-border-1
disabled={upDownDisabled}
onClick={() => { goToDiffIdx(prevDiffIdx) }}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
goToDiffIdx(prevDiffIdx);
}
}}
></button>
const downButton = <button
className={`
size-6 rounded cursor-default
hover:bg-void-bg-1-alt
`}
disabled={upDownDisabled}
onClick={() => { goToDiffIdx(nextDiffIdx) }}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
goToDiffIdx(nextDiffIdx);
}
}}
></button>
const leftButton = <button
className={`
size-6 rounded cursor-default
hover:bg-void-bg-1-alt
`}
disabled={leftRightDisabled}
onClick={() => goToURIIdx(prevURIIdx)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
goToURIIdx(prevURIIdx);
}
}}
></button>
const rightButton = <button
className={`
size-6 rounded cursor-default
hover:bg-void-bg-1-alt
`}
disabled={leftRightDisabled}
onClick={() => goToURIIdx(nextURIIdx)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
goToURIIdx(nextURIIdx);
}
}}
></button>
// accept/reject if current URI has changes
const onAcceptFile = () => {
const onAcceptAll = () => {
if (!uri) return
editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false, _addToHistory: true })
metricsService.capture('Accept File', {})
metricsService.capture('Accept All', {})
}
const onRejectFile = () => {
const onRejectAll = () => {
if (!uri) return
editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false, _addToHistory: true })
metricsService.capture('Reject File', {})
}
const onAcceptAll = () => {
commandBarService.acceptOrRejectAllFiles({ behavior: 'accept' });
metricsService.capture('Accept All', {})
setShowAcceptRejectAllButtons(false);
}
const onRejectAll = () => {
commandBarService.acceptOrRejectAllFiles({ behavior: 'reject' });
metricsService.capture('Reject All', {})
setShowAcceptRejectAllButtons(false);
}
const _upKeybinding = keybindingService.lookupKeybinding(VOID_GOTO_PREV_DIFF_ACTION_ID);
const _downKeybinding = keybindingService.lookupKeybinding(VOID_GOTO_NEXT_DIFF_ACTION_ID);
const _leftKeybinding = keybindingService.lookupKeybinding(VOID_GOTO_PREV_URI_ACTION_ID);
const _rightKeybinding = keybindingService.lookupKeybinding(VOID_GOTO_NEXT_URI_ACTION_ID);
const _acceptFileKeybinding = keybindingService.lookupKeybinding(VOID_ACCEPT_FILE_ACTION_ID);
const _rejectFileKeybinding = keybindingService.lookupKeybinding(VOID_REJECT_FILE_ACTION_ID);
const _acceptAllKeybinding = keybindingService.lookupKeybinding(VOID_ACCEPT_ALL_DIFFS_ACTION_ID);
const _rejectAllKeybinding = keybindingService.lookupKeybinding(VOID_REJECT_ALL_DIFFS_ACTION_ID);
const upKeybindLabel = editCodeService.processRawKeybindingText(_upKeybinding?.getLabel() || '');
const downKeybindLabel = editCodeService.processRawKeybindingText(_downKeybinding?.getLabel() || '');
const leftKeybindLabel = editCodeService.processRawKeybindingText(_leftKeybinding?.getLabel() || '');
const rightKeybindLabel = editCodeService.processRawKeybindingText(_rightKeybinding?.getLabel() || '');
const acceptFileKeybindLabel = editCodeService.processRawKeybindingText(_acceptFileKeybinding?.getAriaLabel() || '');
const rejectFileKeybindLabel = editCodeService.processRawKeybindingText(_rejectFileKeybinding?.getAriaLabel() || '');
const acceptAllKeybindLabel = editCodeService.processRawKeybindingText(_acceptAllKeybinding?.getAriaLabel() || '');
const rejectAllKeybindLabel = editCodeService.processRawKeybindingText(_rejectAllKeybinding?.getAriaLabel() || '');
if (!isADiffZoneInAnyFile) return null
// For pages without a current file index, show a simplified command bar
if (currFileIdx === null) {
return (
<div className="pointer-events-auto">
<div className="flex bg-void-bg-2 shadow-md border border-void-border-2 [&>*:first-child]:pl-3 [&>*:last-child]:pr-3 [&>*]:border-r [&>*]:border-void-border-2 [&>*:last-child]:border-r-0">
<div className="flex items-center px-3">
<span className="text-xs whitespace-nowrap">
{`${sortedCommandBarURIs.length} file${sortedCommandBarURIs.length === 1 ? '' : 's'} changed`}
</span>
</div>
<button
className="text-xs whitespace-nowrap cursor-pointer flex items-center justify-center gap-1 bg-[var(--vscode-button-background)] text-[var(--vscode-button-foreground)] hover:opacity-90 h-full px-3"
onClick={() => commandBarService.goToURIIdx(nextURIIdx)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
commandBarService.goToURIIdx(nextURIIdx);
}
}}
>
Next <MoveRight className='size-3 my-1' />
</button>
</div>
// const acceptAllButton = <button
// className='text-nowrap'
// onClick={onAcceptAll}
// style={{
// backgroundColor: acceptAllBg,
// border: acceptBorder,
// color: buttonTextColor,
// fontSize: buttonFontSize,
// padding: '2px 4px',
// borderRadius: '6px',
// cursor: 'pointer'
// }}
// >
// Accept File
// </button>
// const rejectAllButton = <button
// className='text-nowrap'
// onClick={onRejectAll}
// style={{
// backgroundColor: rejectBg,
// border: rejectBorder,
// color: 'white',
// fontSize: buttonFontSize,
// padding: '2px 4px',
// borderRadius: '6px',
// cursor: 'pointer'
// }}
// >
// Reject File
// </button>
const acceptAllButton = <AcceptAllButtonWrapper
text={'Keep Changes'}
onClick={onAcceptAll}
/>
const rejectAllButton = <RejectAllButtonWrapper
text={'Reject All'}
onClick={onRejectAll}
/>
const acceptRejectAllButtons = <div className="flex items-center gap-1 text-sm">
{acceptAllButton}
{rejectAllButton}
</div>
// const closeCommandBar = useCallback(() => {
// commandService.executeCommand('void.hideCommandBar');
// }, [commandService]);
// const hideButton = <button
// className='ml-auto pointer-events-auto'
// onClick={closeCommandBar}
// style={{
// color: buttonTextColor,
// fontSize: buttonFontSize,
// padding: '2px 4px',
// borderRadius: '6px',
// cursor: 'pointer'
// }}
// title="Close command bar"
// >x
// </button>
const leftRightUpDownButtons = <div className='p-1 gap-1 flex flex-col items-center bg-void-bg-2 rounded shadow-md border border-void-border-2 w-full'>
<div className="flex flex-col gap-1">
{/* Changes in file */}
<div className={`${!isADiffZoneInThisFile ? 'hidden' : ''} flex items-center ${upDownDisabled ? 'opacity-50' : ''}`}>
{upButton}
{downButton}
<span className="min-w-16 px-2 text-xs leading-[1]">
{isADiffInThisFile ?
`Diff ${(currDiffIdx ?? 0) + 1} of ${sortedDiffIds.length}`
: streamState === 'streaming' ?
'No changes yet'
: `No changes`
}
</span>
</div>
);
}
return (
<div className="pointer-events-auto">
{/* Accept All / Reject All buttons that appear when the vertical ellipsis is clicked */}
{showAcceptRejectAllButtons && showAcceptRejectAll && (
<div className="flex justify-end mb-1">
<div className="inline-flex bg-void-bg-2 rounded shadow-md border border-void-border-2 overflow-hidden">
<div className="flex items-center [&>*]:border-r [&>*]:border-void-border-2 [&>*:last-child]:border-r-0">
<AcceptAllButtonWrapper
// text={`Accept All${acceptAllKeybindLabel ? ` ${acceptAllKeybindLabel}` : ''}`}
text={`Accept All`}
data-tooltip-id='void-tooltip'
data-tooltip-content={acceptAllKeybindLabel}
data-tooltip-delay-show={500}
onClick={onAcceptAll}
/>
<RejectAllButtonWrapper
// text={`Reject All${rejectAllKeybindLabel ? ` ${rejectAllKeybindLabel}` : ''}`}
text={`Reject All`}
data-tooltip-id='void-tooltip'
data-tooltip-content={rejectAllKeybindLabel}
data-tooltip-delay-show={500}
onClick={onRejectAll}
/>
</div>
</div>
</div>
)}
<div className="flex items-center bg-void-bg-2 rounded shadow-md border border-void-border-2 [&>*:first-child]:pl-3 [&>*:last-child]:pr-3 [&>*]:px-3 [&>*]:border-r [&>*]:border-void-border-2 [&>*:last-child]:border-r-0">
{/* Diff Navigation Group */}
<div className="flex items-center py-0.5">
<button
className="cursor-pointer"
disabled={upDownDisabled}
onClick={() => commandBarService.goToDiffIdx(prevDiffIdx)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
commandBarService.goToDiffIdx(prevDiffIdx);
}
}}
data-tooltip-id="void-tooltip"
data-tooltip-content={`${upKeybindLabel ? `${upKeybindLabel}` : ''}`}
data-tooltip-delay-show={500}
>
<MoveUp className='size-3 transition-opacity duration-200 opacity-70 hover:opacity-100' />
</button>
<span className={`text-xs whitespace-nowrap px-1 ${!isADiffInThisFile ? 'opacity-70' : ''}`}>
{isADiffInThisFile
? `Diff ${(currDiffIdx ?? 0) + 1} of ${sortedDiffIds.length}`
: streamState === 'streaming'
? 'No changes yet'
: 'No changes'
}
</span>
<button
className="cursor-pointer"
disabled={upDownDisabled}
onClick={() => commandBarService.goToDiffIdx(nextDiffIdx)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
commandBarService.goToDiffIdx(nextDiffIdx);
}
}}
data-tooltip-id="void-tooltip"
data-tooltip-content={`${downKeybindLabel ? `${downKeybindLabel}` : ''}`}
data-tooltip-delay-show={500}
>
<MoveDown className='size-3 transition-opacity duration-200 opacity-70 hover:opacity-100' />
</button>
</div>
{/* File Navigation Group */}
<div className="flex items-center py-0.5">
<button
className="cursor-pointer"
disabled={leftRightDisabled}
onClick={() => commandBarService.goToURIIdx(prevURIIdx)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
commandBarService.goToURIIdx(prevURIIdx);
}
}}
data-tooltip-id="void-tooltip"
data-tooltip-content={`${leftKeybindLabel ? `${leftKeybindLabel}` : ''}`}
data-tooltip-delay-show={500}
>
<MoveLeft className='size-3 transition-opacity duration-200 opacity-70 hover:opacity-100' />
</button>
<span className="text-xs whitespace-nowrap px-1 mx-0.5">
{currFileIdx !== null
? `File ${currFileIdx + 1} of ${sortedCommandBarURIs.length}`
: `${sortedCommandBarURIs.length} file${sortedCommandBarURIs.length === 1 ? '' : 's'}`
}
</span>
<button
className="cursor-pointer"
disabled={leftRightDisabled}
onClick={() => commandBarService.goToURIIdx(nextURIIdx)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
commandBarService.goToURIIdx(nextURIIdx);
}
}}
data-tooltip-id="void-tooltip"
data-tooltip-content={`${rightKeybindLabel ? `${rightKeybindLabel}` : ''}`}
data-tooltip-delay-show={500}
>
<MoveRight className='size-3 transition-opacity duration-200 opacity-70 hover:opacity-100' />
</button>
</div>
{/* Accept/Reject buttons - only shown when appropriate */}
{showAcceptRejectAll && (
<div className='flex self-stretch gap-0 !px-0 !py-0'>
<AcceptAllButtonWrapper
// text={`Accept File${acceptFileKeybindLabel ? ` ${acceptFileKeybindLabel}` : ''}`}
text={`Accept File`}
data-tooltip-id='void-tooltip'
data-tooltip-content={acceptFileKeybindLabel}
data-tooltip-delay-show={500}
onClick={onAcceptFile}
/>
<RejectAllButtonWrapper
// text={`Reject File${rejectFileKeybindLabel ? ` ${rejectFileKeybindLabel}` : ''}`}
text={`Reject File`}
data-tooltip-id='void-tooltip'
data-tooltip-content={rejectFileKeybindLabel}
data-tooltip-delay-show={500}
onClick={onRejectFile}
/>
</div>
)}
{/* Triple colon menu button */}
{showAcceptRejectAll && <div className='!px-0 !py-0 self-stretch flex justify-center items-center'>
<div
className="cursor-pointer px-1 self-stretch flex justify-center items-center"
onClick={() => setShowAcceptRejectAllButtons(!showAcceptRejectAllButtons)}
>
<EllipsisVertical
className="size-3"
/>
</div>
</div>}
{/* Files */}
<div className={`${!isADiffZoneInAnyFile ? 'hidden' : ''} flex items-center ${leftRightDisabled ? 'opacity-50' : ''}`}>
{leftButton}
{/* <div className="w-px h-3 bg-void-border-3 mx-0.5 shadow-sm"></div> */}
{rightButton}
{/* <div className="w-px h-3 bg-void-border-3 mx-0.5 shadow-sm"></div> */}
<span className="min-w-16 px-2 text-xs leading-[1]">
{currFileIdx !== null ?
`File ${currFileIdx + 1} of ${sortedCommandBarURIs.length}`
: `${sortedCommandBarURIs.length} file${sortedCommandBarURIs.length === 1 ? '' : 's'} changed`
}
</span>
</div>
</div>
)
</div>
return <div className={`flex flex-col items-center gap-y-2 pointer-events-auto`}>
{showAcceptRejectAll && acceptRejectAllButtons}
{leftRightUpDownButtons}
</div>
}
@@ -6,9 +6,10 @@
import { useEffect, useRef, useState } from 'react';
import { useAccessor, useIsDark, useSettingsState } from '../util/services.js';
import { Brain, Check, ChevronRight, DollarSign, ExternalLink, Lock, X } from 'lucide-react';
import { displayInfoOfProviderName, ProviderName, providerNames, localProviderNames, featureNames, FeatureName, isFeatureNameDisabled } from '../../../../common/voidSettingsTypes.js';
import { displayInfoOfProviderName, ProviderName, providerNames, refreshableProviderNames } from '../../../../common/voidSettingsTypes.js';
import { getModelCapabilities, ollamaRecommendedModels } from '../../../../common/modelCapabilities.js';
import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js';
import { OllamaSetupInstructions, OneClickSwitchButton, SettingsForProvider, ModelDump } from '../void-settings-tsx/Settings.js';
import { AddModelInputBox, AnimatedCheckmarkButton, OllamaSetupInstructions, OneClickSwitchButton, SettingsForProvider } from '../void-settings-tsx/Settings.js';
import { ColorScheme } from '../../../../../../../platform/theme/common/theme.js';
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js';
import { isLinux } from '../../../../../../../base/common/platform.js';
@@ -26,10 +27,9 @@ export const VoidOnboarding = () => {
<div className={`@@void-scope ${isDark ? 'dark' : ''}`}>
<div
className={`
bg-void-bg-3 fixed top-0 right-0 bottom-0 left-0 width-full z-[99999]
bg-void-bg-3 fixed top-0 right-0 bottom-0 left-0 width-full h-full z-[99999]
transition-all duration-1000 ${isOnboardingComplete ? 'opacity-0 pointer-events-none' : 'opacity-100 pointer-events-auto'}
`}
style={{ height: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
>
<ErrorBoundary>
<VoidOnboardingContent />
@@ -90,187 +90,6 @@ const FadeIn = ({ children, className, delayMs = 0, durationMs, ...props }: { ch
}
// Onboarding
// =============================================
// New AddProvidersPage Component and helpers
// =============================================
const tabNames = ['Free', 'Paid', 'Local'] as const;
type TabName = typeof tabNames[number] | 'Cloud/Other';
// Data for cloud providers tab
const cloudProviders: ProviderName[] = ['googleVertex', 'liteLLM', 'microsoftAzure', 'openAICompatible'];
// Data structures for provider tabs
const providerNamesOfTab: Record<TabName, ProviderName[]> = {
Free: ['gemini', 'openRouter'],
Local: localProviderNames,
Paid: providerNames.filter(pn => !(['gemini', 'openRouter', ...localProviderNames, ...cloudProviders] as string[]).includes(pn)) as ProviderName[],
'Cloud/Other': cloudProviders,
};
const descriptionOfTab: Record<TabName, string> = {
Free: `Providers with a 100% free tier. Add as many as you'd like!`,
Paid: `Connect directly with any provider (bring your own key).`,
Local: `Active providers should appear automatically. Add as many as you'd like! `,
'Cloud/Other': `Add as many as you'd like! Reach out for custom configuration requests.`,
};
const featureNameMap: { display: string, featureName: FeatureName }[] = [
{ display: 'Chat', featureName: 'Chat' },
{ display: 'Quick Edit', featureName: 'Ctrl+K' },
{ display: 'Autocomplete', featureName: 'Autocomplete' },
{ display: 'Fast Apply', featureName: 'Apply' },
];
const AddProvidersPage = ({ pageIndex, setPageIndex }: { pageIndex: number, setPageIndex: (index: number) => void }) => {
const [currentTab, setCurrentTab] = useState<TabName>('Free');
const settingsState = useSettingsState();
const [errorMessage, setErrorMessage] = useState<string | null>(null);
// Clear error message after 5 seconds
useEffect(() => {
let timeoutId: NodeJS.Timeout | null = null;
if (errorMessage) {
timeoutId = setTimeout(() => {
setErrorMessage(null);
}, 5000);
}
// Cleanup function to clear the timeout if component unmounts or error changes
return () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
};
}, [errorMessage]);
return (<div className="flex flex-col md:flex-row w-full h-[80vh] gap-6 max-w-[900px] mx-auto relative">
{/* Left Column */}
<div className="md:w-1/4 w-full flex flex-col gap-6 p-6 border-none border-void-border-2 h-full overflow-y-auto">
{/* Tab Selector */}
<div className="flex md:flex-col gap-2">
{[...tabNames, 'Cloud/Other'].map(tab => (
<button
key={tab}
className={`py-2 px-4 rounded-md text-left ${currentTab === tab
? 'bg-[#0e70c0]/80 text-white font-medium shadow-sm'
: 'bg-void-bg-2 hover:bg-void-bg-2/80 text-void-fg-1'
} transition-all duration-200`}
onClick={() => {
setCurrentTab(tab as TabName);
setErrorMessage(null); // Reset error message when changing tabs
}}
>
{tab}
</button>
))}
</div>
{/* Feature Checklist */}
<div className="flex flex-col gap-1 mt-4 text-sm opacity-80">
{featureNameMap.map(({ display, featureName }) => {
const hasModel = settingsState.modelSelectionOfFeature[featureName] !== null;
return (
<div key={featureName} className="flex items-center gap-2">
{hasModel ? (
<Check className="w-4 h-4 text-emerald-500" />
) : (
<div className="w-3 h-3 rounded-full flex items-center justify-center">
<div className="w-1 h-1 rounded-full bg-white/70"></div>
</div>
)}
<span>{display}</span>
</div>
);
})}
</div>
</div>
{/* Right Column */}
<div className="flex-1 flex flex-col items-center justify-start p-6 h-full overflow-y-auto">
<div className="text-5xl mb-2 text-center w-full">Add a Provider</div>
<div className="w-full max-w-xl mt-4 mb-10">
<div className="text-4xl font-light my-4 w-full">{currentTab}</div>
<div className="text-sm opacity-80 text-void-fg-3 my-4 w-full">{descriptionOfTab[currentTab]}</div>
</div>
{providerNamesOfTab[currentTab].map((providerName) => (
<div key={providerName} className="w-full max-w-xl mb-10">
<div className="text-xl mb-2">
Add {displayInfoOfProviderName(providerName).title}
{providerName === 'gemini' && (
<span
data-tooltip-id="void-tooltip-provider-info"
data-tooltip-content="Gemini 2.5 Pro offers 25 free messages a day, and Gemini 2.5 Flash offers 500. We recommend using models down the line as you run out of free credits."
data-tooltip-place="right"
className="ml-1 text-xs align-top text-blue-400"
>*</span>
)}
{providerName === 'openRouter' && (
<span
data-tooltip-id="void-tooltip-provider-info"
data-tooltip-content="OpenRouter offers 50 free messages a day, and 1000 if you deposit $10. Only applies to models labeled ':free'."
data-tooltip-place="right"
className="ml-1 text-xs align-top text-blue-400"
>*</span>
)}
</div>
<div>
<SettingsForProvider providerName={providerName} showProviderTitle={false} showProviderSuggestions={true} />
</div>
{providerName === 'ollama' && <OllamaSetupInstructions />}
</div>
))}
{(currentTab === 'Local' || currentTab === 'Cloud/Other') && (
<div className="w-full max-w-xl mt-8 bg-void-bg-2/50 rounded-lg p-6 border border-void-border-4">
<div className="flex items-center gap-2 mb-4">
<div className="text-xl font-medium">Models</div>
</div>
{currentTab === 'Local' && (
<div className="text-sm opacity-80 text-void-fg-3 my-4 w-full">Local models should be detected automatically. You can add custom models below.</div>
)}
{currentTab === 'Local' && <ModelDump filteredProviders={localProviderNames} />}
{currentTab === 'Cloud/Other' && <ModelDump filteredProviders={cloudProviders} />}
</div>
)}
{/* Navigation buttons in right column */}
<div className="flex flex-col items-end w-full mt-auto pt-8">
{errorMessage && (
<div className="text-amber-400 mb-2 text-sm opacity-80 transition-opacity duration-300">{errorMessage}</div>
)}
<div className="flex items-center gap-2">
<PreviousButton onClick={() => setPageIndex(pageIndex - 1)} />
<NextButton
onClick={() => {
const isDisabled = isFeatureNameDisabled('Chat', settingsState)
if (!isDisabled) {
setPageIndex(pageIndex + 1);
setErrorMessage(null);
} else {
// Show error message
setErrorMessage("Please set up at least one Chat model before moving on.");
}
}}
/>
</div>
</div>
</div>
</div>);
};
// =============================================
// OnboardingPage
// title:
// div
@@ -360,7 +179,7 @@ const OnboardingPageShell = ({ top, bottom, content, hasMaxWidth = true, classNa
className?: string,
}) => {
return (
<div className={`h-[80vh] text-lg flex flex-col gap-4 w-full mx-auto ${hasMaxWidth ? 'max-w-[600px]' : ''} ${className}`}>
<div className={`min-h-full text-lg flex flex-col gap-4 w-full mx-auto ${hasMaxWidth ? 'max-w-[600px]' : ''} ${className}`}>
{top && <FadeIn className='w-full mb-auto pt-16'>{top}</FadeIn>}
{content && <FadeIn className='w-full my-auto'>{content}</FadeIn>}
{bottom && <div className='w-full pb-8'>{bottom}</div>}
@@ -369,6 +188,8 @@ const OnboardingPageShell = ({ top, bottom, content, hasMaxWidth = true, classNa
}
const OllamaDownloadOrRemoveModelButton = ({ modelName, isModelInstalled, sizeGb }: { modelName: string, isModelInstalled: boolean, sizeGb: number | false | 'not-known' }) => {
// for now just link to the ollama download page
return <a
href={`https://ollama.com/library/${modelName}`}
@@ -379,6 +200,76 @@ const OllamaDownloadOrRemoveModelButton = ({ modelName, isModelInstalled, sizeGb
<ExternalLink className="w-3.5 h-3.5" />
</a>
// if (isModelInstalled) {
// return <div className="flex items-center">
// <span className="flex items-center">Uninstall</span>
// <IconShell1
// className="ml-1"
// Icon={Trash}
// onClick={() => {
// setIsModelInstalling(false);
// }}
// />
// </div>
// }
// else if (isModelInstalling) {
// return <div className="flex items-center">
// <span className="flex items-center">{`Download? ${typeof sizeGb === 'number' ? `(${sizeGb} Gb)` : ''}`}</span>
// <IconShell1
// className="ml-1"
// Icon={Square}
// onClick={() => {
// // abort()
// // TODO!!!!!!!!!!! don't do this
// setIsModelInstalling(false);
// }}
// />
// </div>
// }
// else if (!isModelInstalled) {
// return <div className="flex items-center">
// <span className="flex items-center">Download ({sizeGb} Gb)</span>
// <IconShell1
// className="ml-1"
// Icon={Download}
// onClick={() => {
// // this is a check for whether the model was installed:
// if (isModelInstalling) return
// // TODO!!!!!! don't do this
// // install(modelname), callback = setIsModelInstalling(false);
// setIsModelInstalling(true);
// }}
// />
// </div>
// }
// return <></>
}
@@ -415,7 +306,114 @@ const abbreviateNumber = (num: number): string => {
}
}
const TableOfModelsForProvider = ({ providerName }: { providerName: ProviderName }) => {
const accessor = useAccessor()
const voidSettingsService = accessor.get('IVoidSettingsService')
const voidSettingsState = useSettingsState()
const isDetectableLocally = (refreshableProviderNames as ProviderName[]).includes(providerName)
// const providerCapabilities = getProviderCapabilities(providerName)
// info used to show the table
const infoOfModelName: Record<string, { showAsDefault: boolean, isDownloaded: boolean } | undefined> = {}
voidSettingsState.settingsOfProvider[providerName].models.forEach(m => {
infoOfModelName[m.modelName] = {
showAsDefault: m.type !== 'custom',
isDownloaded: true
}
})
// special case columns for ollama; show recommended models as default
if (providerName === 'ollama') {
for (const modelName of ollamaRecommendedModels) {
if (modelName in infoOfModelName) continue
infoOfModelName[modelName] = {
isDownloaded: infoOfModelName[modelName]?.isDownloaded ?? false,
showAsDefault: true,
}
}
}
return <table className="table-fixed border-collapse mb-6 bg-void-bg-2 text-sm mx-auto select-text">
<thead>
<tr className="border-b border-void-border-1 text-nowrap text-ellipsis">
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[200px]">Models Offered</th>
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Cost/M</th>
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Context</th>
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Chat</th>
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Agent</th>
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Autotab</th>
{/* <th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Reasoning</th> */}
{isDetectableLocally && <th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Detected</th>}
{providerName === 'ollama' && <th className="text-left py-2 px-3 font-normal text-void-fg-3">Download</th>}
</tr>
</thead>
<tbody>
{Object.keys(infoOfModelName).map(modelName => {
const { showAsDefault, isDownloaded } = infoOfModelName[modelName] ?? {}
const capabilities = getModelCapabilities(providerName, modelName)
const {
downloadable,
cost,
supportsFIM,
reasoningCapabilities,
contextWindow,
isUnrecognizedModel,
maxOutputTokens,
supportsSystemMessage,
} = capabilities
// TODO update this when tools work
const removeModelButton = <button
className="absolute -left-1 top-1/2 transform -translate-y-1/2 -translate-x-full text-void-fg-3 hover:text-void-fg-1 text-xs"
onClick={() => voidSettingsService.deleteModel(providerName, modelName)}
>
<X className="w-3.5 h-3.5" />
</button>
return (
<tr key={`${modelName}${providerName}`} className="border-b border-void-border-1 hover:bg-void-bg-3/50">
<td className="py-2 px-3 relative">
{!showAsDefault && removeModelButton}
{modelName}
</td>
<td className="py-2 px-3">${cost.output ?? ''}</td>
<td className="py-2 px-3">{contextWindow ? abbreviateNumber(contextWindow) : ''}</td>
<td className="py-2 px-3"><YesNoText val={true} /></td>
<td className="py-2 px-3"><YesNoText val={!!true} /></td>
<td className="py-2 px-3"><YesNoText val={!!supportsFIM} /></td>
{/* <td className="py-2 px-3"><YesNoText val={!!reasoningCapabilities} /></td> */}
{isDetectableLocally && <td className="py-2 px-3 flex items-center justify-center">{!!isDownloaded ? <Check className="w-4 h-4" /> : <></>}</td>}
{providerName === 'ollama' && <th className="py-2 px-3">
<OllamaDownloadOrRemoveModelButton modelName={modelName} isModelInstalled={!!infoOfModelName[modelName]?.isDownloaded} sizeGb={downloadable && downloadable.sizeGb} />
</th>}
</tr>
)
})}
<tr className="hover:bg-void-bg-3/50">
<td className="py-2 px-3 text-void-accent">
<ErrorBoundary>
<AddModelInputBox
key={providerName}
providerName={providerName}
compact={true}
/>
</ErrorBoundary>
</td>
<td colSpan={4}></td>
</tr>
</tbody>
</table>
}
@@ -433,16 +431,22 @@ const PrimaryActionButton = ({ children, className, ringSize, ...props }: { chil
${ringSize === 'xl' ? `
gap-2 px-16 py-8
hover:ring-8 active:ring-8
transition-all duration-300 ease-in-out
`
: ringSize === 'screen' ? `
gap-2 px-16 py-8
ring-[3000px]
transition-all duration-1000 ease-in-out
`: ringSize === undefined ? `
gap-1 px-4 py-2
hover:ring-2 active:ring-2
transition-all duration-300 ease-in-out
`: ''}
hover:ring-black/90 dark:hover:ring-white/90
active:ring-black/90 dark:active:ring-white/90
rounded-lg
group
${className}
@@ -530,6 +534,7 @@ const VoidOnboardingContent = () => {
/>
<NextButton
onClick={() => { setPageIndex(pageIndex + 1) }}
disabled={pageIndex === 2 && !didFillInSelectedProviderSettings}
/>
</div>
</div>
@@ -607,7 +612,7 @@ const VoidOnboardingContent = () => {
delayMs={1000}
>
<PrimaryActionButton
onClick={() => { setPageIndex(1) }}
onClick={() => { setPageIndex(pageIndex + 1) }}
>
Get Started
</PrimaryActionButton>
@@ -616,13 +621,255 @@ const VoidOnboardingContent = () => {
</div>
}
/>,
1: <OnboardingPageShell
1: <OnboardingPageShell hasMaxWidth={false}
content={
<AddProvidersPage pageIndex={pageIndex} setPageIndex={setPageIndex} />
hasMaxWidth={false}
top={<></>}
content={<div className='flex flex-col items-center -translate-y-[20vh]'>
{/* <div className="text-5xl text-center mb-8">AI Preferences</div> */}
<div className="text-4xl text-void-fg-2 mb-8 text-center">Model Preferences</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 w-full max-w-[800px] mx-auto mt-8">
<button
onClick={() => { setWantToUseOption('cheap'); setPageIndex(pageIndex + 1); }}
className="flex flex-col p-6 rounded bg-void-bg-2 border border-void-border-3 hover:brightness-110 transition-colors focus:outline-none focus:border-void-accent-border relative overflow-hidden min-h-[160px]"
>
<div className="flex items-center mb-3">
<DollarSign size={24} className="text-void-fg-2 mr-2" />
<div className="text-lg font-medium text-void-fg-1">Affordable</div>
</div>
<div className="text-sm text-void-fg-2 text-left">{basicDescOfWantToUseOption['cheap']}</div>
</button>
<button
onClick={() => { setWantToUseOption('private'); setPageIndex(pageIndex + 1); }}
className="flex flex-col p-6 rounded bg-void-bg-2 border border-void-border-3 hover:brightness-110 transition-colors focus:outline-none focus:border-void-accent-border relative overflow-hidden min-h-[160px]"
>
<div className="flex items-center mb-3">
<Lock size={24} className="text-void-fg-2 mr-2" />
<div className="text-lg font-medium text-void-fg-1">Private</div>
</div>
<div className="text-sm text-void-fg-2 text-left">{basicDescOfWantToUseOption['private']}</div>
</button>
<button
onClick={() => { setWantToUseOption('smart'); setPageIndex(pageIndex + 1); }}
className="flex flex-col p-6 rounded bg-void-bg-2 border border-void-border-3 hover:brightness-110 transition-colors focus:outline-none focus:border-void-accent-border relative overflow-hidden min-h-[160px]"
>
<div className="flex items-center mb-3">
<Brain size={24} className="text-void-fg-2 mr-2" />
<div className="text-lg font-medium text-void-fg-1">Intelligent</div>
</div>
<div className="text-sm text-void-fg-2 text-left">{basicDescOfWantToUseOption['smart']}</div>
</button>
</div>
</div>}
bottom={
<div className='mx-auto w-full max-w-[800px]'>
<PreviousButton onClick={() => { setPageIndex(pageIndex - 1) }} />
</div>
}
/>,
2: <OnboardingPageShell
top={
<>
{/* Title */}
<div className="text-5xl font-light text-center mt-[10vh] mb-6">Choose a Provider</div>
{/* Preference Selector */}
<div
className="mb-6 w-fit mx-auto flex items-center overflow-hidden bg-zinc-700/5 dark:bg-zinc-300/5 rounded-md"
>
{[
{ id: 'smart', label: 'Intelligent' },
{ id: 'private', label: 'Private' },
{ id: 'cheap', label: 'Affordable' },
{ id: 'all', label: 'All' }
].map(option => (
<ErrorBoundary
key={option.id}
>
<button
onClick={() => setWantToUseOption(option.id as WantToUseOption)}
className={`py-1 px-2 text-xs cursor-pointer whitespace-nowrap rounded-sm transition-colors ${wantToUseOption === option.id
? 'dark:text-white text-black font-medium'
: 'text-void-fg-3 hover:text-void-fg-2'
}`}
data-tooltip-id='void-tooltip'
data-tooltip-content={`${option.label} providers`}
data-tooltip-place='bottom'
>
{option.label}
</button>
</ErrorBoundary>
))}
</div>
{/* Provider Buttons - Modified to use separate components for each tab */}
<div className="mb-2 w-full">
{/* Intelligent tab */}
<ErrorBoundary>
<div className={`flex flex-wrap items-center w-full ${wantToUseOption === 'smart' ? 'flex' : 'hidden'}`}>
{providerNamesOfWantToUseOption['smart'].map((providerName) => {
const isSelected = selectedIntelligentProvider === providerName;
return (
<button
key={providerName}
onClick={() => setSelectedIntelligentProvider(providerName)}
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-all duration-300
${isSelected ? 'bg-zinc-100 text-zinc-900 shadow-sm border-white/80' : 'bg-zinc-100/40 hover:bg-zinc-100/50 text-zinc-900 border-white/20'}`}
>
{displayInfoOfProviderName(providerName).title}
</button>
);
})}
</div>
</ErrorBoundary>
{/* Private tab */}
<ErrorBoundary>
<div className={`flex flex-wrap items-center w-full ${wantToUseOption === 'private' ? 'flex' : 'hidden'}`}>
{providerNamesOfWantToUseOption['private'].map((providerName) => {
const isSelected = selectedPrivateProvider === providerName;
return (
<button
key={providerName}
onClick={() => setSelectedPrivateProvider(providerName)}
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-all duration-300
${isSelected ? 'bg-zinc-100 text-zinc-900 shadow-sm border-white/80' : 'bg-zinc-100/40 hover:bg-zinc-100/50 text-zinc-900 border-white/20'}`}
>
{displayInfoOfProviderName(providerName).title}
</button>
);
})}
</div>
</ErrorBoundary>
{/* Affordable tab */}
<ErrorBoundary>
<div className={`flex flex-wrap items-center w-full ${wantToUseOption === 'cheap' ? 'flex' : 'hidden'}`}>
{providerNamesOfWantToUseOption['cheap'].map((providerName) => {
const isSelected = selectedAffordableProvider === providerName;
return (
<button
key={providerName}
onClick={() => setSelectedAffordableProvider(providerName)}
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-all duration-300
${isSelected ? 'bg-zinc-100 text-zinc-900 shadow-sm border-white/80' : 'bg-zinc-100/40 hover:bg-zinc-100/50 text-zinc-900 border-white/20'}`}
>
{displayInfoOfProviderName(providerName).title}
</button>
);
})}
</div>
</ErrorBoundary>
{/* All tab */}
<ErrorBoundary>
<div className={`flex flex-wrap items-center w-full ${wantToUseOption === 'all' ? 'flex' : 'hidden'}`}>
{providerNames.map((providerName) => {
const isSelected = selectedAllProvider === providerName;
return (
<button
key={providerName}
onClick={() => setSelectedAllProvider(providerName)}
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-all duration-300
${isSelected ? 'bg-zinc-100 text-zinc-900 shadow-sm border-white/80' : 'bg-zinc-100/40 hover:bg-zinc-100/50 text-zinc-900 border-white/20'}`}
>
{displayInfoOfProviderName(providerName).title}
</button>
);
})}
</div>
</ErrorBoundary>
</div>
{/* Description */}
<ErrorBoundary>
<div className="text-left self-start text-sm text-void-fg-3 px-2 py-1">
<ChatMarkdownRender string={detailedDescOfWantToUseOption[wantToUseOption]} chatMessageLocation={undefined} />
</div>
</ErrorBoundary>
{/* ModelsTable and ProviderFields */}
{selectedProviderName && <div className='mt-4 w-fit mx-auto'>
{/* Models Table */}
<ErrorBoundary>
<TableOfModelsForProvider providerName={selectedProviderName} />
</ErrorBoundary>
{/* Add provider section - simplified styling */}
<div className='mb-5 mt-8 mx-auto'>
<ErrorBoundary>
<div className=''>
Add {displayInfoOfProviderName(selectedProviderName).title}
<div className='my-4'>
{selectedProviderName === 'ollama' ? <OllamaSetupInstructions /> : ''}
</div>
</div>
</ErrorBoundary>
<ErrorBoundary>
{selectedProviderName &&
<SettingsForProvider providerName={selectedProviderName} showProviderTitle={false} showProviderSuggestions={false} />
}
</ErrorBoundary>
{/* Button and status indicators */}
<ErrorBoundary>
{!didFillInProviderSettings ? <p className="text-xs text-void-fg-3 mt-2">Please fill in all fields to continue</p>
: !isAtLeastOneModel ? <p className="text-xs text-void-fg-3 mt-2">Please add a model to continue</p>
: !isApiKeyLongEnoughIfApiKeyExists ? <p className="text-xs text-void-fg-3 mt-2">Please enter a valid API key</p>
: <AnimatedCheckmarkButton className='text-xs text-void-fg-3 mt-2' text='Added' />}
</ErrorBoundary>
</div>
</div>}
</>
}
bottom={
<ErrorBoundary>
<FadeIn delayMs={50} durationMs={10}>
{prevAndNextButtons}
</FadeIn>
</ErrorBoundary>
}
/>,
// 2.5: <div className="max-w-[600px] w-full h-full text-left mx-auto flex flex-col items-center justify-between">
// <FadeIn>
// <div className="text-5xl font-light mb-6 mt-12 text-center">Autocomplete</div>
// <div className="text-center flex flex-col gap-4 w-full max-w-md mx-auto">
// <h4 className="text-void-fg-3 mb-2">Void offers free autocomplete with locally hosted models</h4>
// <h4 className="text-void-fg-3 mb-2">[have buttons for Ollama install Qwen2.5coder3b and memory requirements] </h4>
// </div>
// </FadeIn>
// {prevAndNextButtons}
// </div>,
3: <OnboardingPageShell
content={
<div>
@@ -637,11 +884,32 @@ const VoidOnboardingContent = () => {
</div>
}
bottom={lastPagePrevAndNextButtons}
// bottom={prevAndNextButtons}
/>,
// 4: <OnboardingPageShell
// content={
// <>
// <div
// className='flex justify-center'
// >
// <PrimaryActionButton
// onClick={() => { voidSettingsService.setGlobalSetting('isOnboardingComplete', true); }}
// ringSize={voidSettingsState.globalSettings.isOnboardingComplete ? 'screen' : undefined}
// className='text-4xl'
// >Enter the Void</PrimaryActionButton>
// </div>
// </>
// }
// bottom={
// <PreviousButton
// onClick={() => { setPageIndex(pageIndex - 1) }}
// />
// }
// />,
}
return <div key={pageIndex} className="w-full h-[80vh] text-left mx-auto flex flex-col items-center justify-center">
return <div key={pageIndex} className="w-full h-full text-left mx-auto overflow-y-scroll flex flex-col items-center justify-around">
<ErrorBoundary>
{contentOfIdx[pageIndex]}
</ErrorBoundary>
@@ -56,7 +56,7 @@ const MemoizedModelDropdown = ({ featureName, className }: { featureName: Featur
useEffect(() => {
const oldOptions = oldOptionsRef.current
const newOptions = settingsState._modelOptions.filter((o) => filter(o.selection, { chatMode: settingsState.globalSettings.chatMode, overridesOfModel: settingsState.overridesOfModel }))
const newOptions = settingsState._modelOptions.filter((o) => filter(o.selection, { chatMode: settingsState.globalSettings.chatMode }))
if (!optionsEqual(oldOptions, newOptions)) {
setMemoizedOptions(newOptions)
@@ -3,13 +3,14 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react'; // Added useRef import just in case it was missed, though likely already present
import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react'
import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidStatefulModelInfo, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, nonlocalProviderNames, localProviderNames, GlobalSettingName, featureNames, displayInfoOfFeatureName, isProviderNameDisabled, FeatureName, hasDownloadButtonsOnModelsProviderNames, subTextMdOfProviderName } from '../../../../common/voidSettingsTypes.js'
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
import { VoidButtonBgDarken, VoidCustomDropdownBox, VoidInputBox2, VoidSimpleInputBox, VoidSwitch } from '../util/inputs.js'
import { useAccessor, useIsDark, useRefreshModelListener, useRefreshModelState, useSettingsState } from '../util/services.js'
import { X, RefreshCw, Loader2, Check, Asterisk, Plus } from 'lucide-react'
import { X, RefreshCw, Loader2, Check, Asterisk } from 'lucide-react'
import { URI } from '../../../../../../../base/common/uri.js'
import { env } from '../../../../../../../base/common/process.js'
import { ModelDropdown } from './ModelDropdown.js'
import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js'
import { WarningBox } from './WarningBox.js'
@@ -17,8 +18,6 @@ import { os } from '../../../../common/helpers/systemInfo.js'
import { IconLoading } from '../sidebar-tsx/SidebarChat.js'
import { ToolApprovalType, toolApprovalTypes } from '../../../../common/toolsServiceTypes.js'
import Severity from '../../../../../../../base/common/severity.js'
import { getModelCapabilities, ModelOverrides } from '../../../../common/modelCapabilities.js';
import { TransferEditorType, TransferFilesInfo } from '../../../extensionTransferTypes.js';
const ButtonLeftTextRightOption = ({ text, leftButton }: { text: string, leftButton?: React.ReactNode }) => {
@@ -184,211 +183,135 @@ const ConfirmButton = ({ children, onConfirm, className }: { children: React.Rea
);
};
// ---------------- Simplified Model Settings Dialog ------------------
// This new dialog replaces the verbose UI with a single JSON override box.
const SimpleModelSettingsDialog = ({
isOpen,
onClose,
modelInfo,
}: {
isOpen: boolean;
onClose: () => void;
modelInfo: { modelName: string; providerName: ProviderName; type: 'autodetected' | 'custom' | 'default' } | null;
}) => {
if (!isOpen || !modelInfo) return null;
const { modelName, providerName, type } = modelInfo;
const accessor = useAccessor();
const settingsState = useSettingsState();
const mouseDownInsideModal = useRef(false); // Ref to track mousedown origin
const settingsStateService = accessor.get('IVoidSettingsService');
// shows a providerName dropdown if no `providerName` is given
export const AddModelInputBox = ({ providerName: permanentProviderName, className, compact }: { providerName?: ProviderName, className?: string, compact?: boolean }) => {
// current overrides and defaults
const defaultModelCapabilities = getModelCapabilities(providerName, modelName, undefined);
const currentOverrides = settingsState.overridesOfModel?.[providerName]?.[modelName] ?? undefined;
const { recognizedModelName, isUnrecognizedModel } = defaultModelCapabilities
// keys of ModelOverrides we allow the user to override
const allowedKeys: (string & (keyof ModelOverrides))[] = [
'contextWindow',
'reservedOutputTokenSpace',
'supportsSystemMessage',
'specialToolFormat',
'supportsFIM',
'reasoningCapabilities',
];
// Create the placeholder with the default values for allowed keys
const partialDefaults: Partial<ModelOverrides> = {};
for (const k of allowedKeys) { if (defaultModelCapabilities[k]) partialDefaults[k] = defaultModelCapabilities[k] as any; }
const placeholder = JSON.stringify(partialDefaults, null, 2);
const [overrideEnabled, setOverrideEnabled] = useState<boolean>(() => !!currentOverrides);
const [jsonText, setJsonText] = useState<string>(() => currentOverrides ? JSON.stringify(currentOverrides, null, 2) : placeholder);
const [readOnlyHeight, setReadOnlyHeight] = useState<number | undefined>(undefined);
const [errorMsg, setErrorMsg] = useState<string | null>(null);
// reset when dialog toggles
useEffect(() => {
if (!isOpen) return;
const cur = settingsState.overridesOfModel?.[providerName]?.[modelName];
setOverrideEnabled(!!cur);
// If there are overrides, show them; otherwise use default values
setJsonText(cur ? JSON.stringify(cur, null, 2) : placeholder);
setErrorMsg(null);
}, [isOpen, providerName, modelName, settingsState.overridesOfModel, placeholder]);
const onSave = async () => {
// if disabled override, reset overrides
if (!overrideEnabled) {
await settingsStateService.setOverridesOfModel(providerName, modelName, undefined);
onClose();
return;
}
// enabled overrides
// parse json
let parsedInput: Record<string, unknown>
if (jsonText.trim()) {
try {
parsedInput = JSON.parse(jsonText);
} catch (e) {
setErrorMsg('Invalid JSON');
return;
}
} else {
setErrorMsg('Invalid JSON');
return;
}
// only keep allowed keys
const cleaned: Partial<ModelOverrides> = {};
for (const k of allowedKeys) {
if (!(k in parsedInput)) continue
const isEmpty = parsedInput[k] === '' || parsedInput[k] === null || parsedInput[k] === undefined;
if (!isEmpty && (k in partialDefaults)) {
cleaned[k] = parsedInput[k] as any;
}
}
await settingsStateService.setOverridesOfModel(providerName, modelName, cleaned);
onClose();
};
const sourcecodeOverridesLink = `https://github.com/voideditor/void/blob/2e5ecb291d33afbe4565921664fb7e183189c1c5/src/vs/workbench/contrib/void/common/modelCapabilities.ts#L146-L172`
return (
<div // Backdrop
className="fixed inset-0 bg-black/50 flex items-center justify-center z-[9999999]"
onMouseDown={() => {
mouseDownInsideModal.current = false;
}}
onMouseUp={() => {
if (!mouseDownInsideModal.current) {
onClose();
}
mouseDownInsideModal.current = false;
}}
>
{/* MODAL */}
<div
className="bg-void-bg-1 rounded-md p-4 max-w-xl w-full shadow-xl overflow-y-auto max-h-[90vh]"
onClick={(e) => e.stopPropagation()} // Keep stopping propagation for normal clicks inside
onMouseDown={(e) => {
mouseDownInsideModal.current = true;
e.stopPropagation();
}}
>
<div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-medium">
Change Defaults for {modelName} ({displayInfoOfProviderName(providerName).title})
</h3>
<button
onClick={onClose}
className="text-void-fg-3 hover:text-void-fg-1"
>
<X className="size-5" />
</button>
</div>
{/* Display model recognition status */}
<div className="text-sm text-void-fg-3 mb-4">
{type === 'default' ? `${modelName} comes packaged with Void, so you shouldn't need to change these settings.`
: isUnrecognizedModel
? `Model not recognized by Void.`
: `Void recognizes ${modelName} ("${recognizedModelName}").`}
</div>
{/* override toggle */}
<div className="flex items-center gap-2 mb-4">
<VoidSwitch size='xs' value={overrideEnabled} onChange={setOverrideEnabled} />
<span className="text-void-fg-3 text-sm">Override model defaults</span>
</div>
{/* Informational link */}
{overrideEnabled && <div className="text-sm text-void-fg-3 mb-4">
<ChatMarkdownRender string={`See the [sourcecode](${sourcecodeOverridesLink}) for a reference on how to set this JSON (advanced).`} chatMessageLocation={undefined} />
</div>}
<textarea
className={`w-full min-h-[200px] p-2 rounded-sm border border-void-border-2 bg-void-bg-2 resize-none font-mono text-sm ${!overrideEnabled ? 'text-void-fg-3' : ''}`}
value={overrideEnabled ? jsonText : placeholder}
placeholder={placeholder}
onChange={overrideEnabled ? (e) => setJsonText(e.target.value) : undefined}
readOnly={!overrideEnabled}
/>
{errorMsg && (
<div className="text-red-500 mt-2 text-sm">{errorMsg}</div>
)}
<div className="flex justify-end gap-2 mt-4">
<VoidButtonBgDarken onClick={onClose} className="px-3 py-1">
Cancel
</VoidButtonBgDarken>
<VoidButtonBgDarken
onClick={onSave}
className="px-3 py-1 bg-[#0e70c0] text-white"
>
Save
</VoidButtonBgDarken>
</div>
</div>
</div>
);
};
export const ModelDump = ({ filteredProviders }: { filteredProviders?: ProviderName[] }) => {
const accessor = useAccessor()
const settingsStateService = accessor.get('IVoidSettingsService')
const settingsState = useSettingsState()
// State to track which model's settings dialog is open
const [openSettingsModel, setOpenSettingsModel] = useState<{
modelName: string,
providerName: ProviderName,
type: 'autodetected' | 'custom' | 'default'
} | null>(null);
const [isOpen, setIsOpen] = useState(false)
const [showCheckmark, setShowCheckmark] = useState(false)
// States for add model functionality
const [isAddModelOpen, setIsAddModelOpen] = useState(false);
const [showCheckmark, setShowCheckmark] = useState(false);
const [userChosenProviderName, setUserChosenProviderName] = useState<ProviderName | null>(null);
const [modelName, setModelName] = useState<string>('');
const [errorString, setErrorString] = useState('');
// const providerNameRef = useRef<ProviderName | null>(null)
const [userChosenProviderName, setUserChosenProviderName] = useState<ProviderName | null>(null)
const providerName = permanentProviderName ?? userChosenProviderName;
const [modelName, setModelName] = useState<string>('')
const [errorString, setErrorString] = useState('')
const numModels = providerName === null ? 0 : settingsState.settingsOfProvider[providerName].models.length
if (showCheckmark) {
return <AnimatedCheckmarkButton text='Added' className={`bg-[#0e70c0] text-white px-3 py-1 rounded-sm ${className}`} />
}
if (!isOpen) {
return <div
className={`text-void-fg-4 flex flex-nowrap text-nowrap items-center hover:brightness-110 cursor-pointer ${className}`}
onClick={() => setIsOpen(true)}
>
<div>
{numModels > 0 ? `Add a different model?` : `Add a model`}
</div>
</div>
}
return <>
<form className={`flex items-center gap-2 ${className}`}>
{/* X button
<button onClick={() => { setIsOpen(false) }} className='text-void-fg-4'><X className='size-4' /></button> */}
{/* provider input */}
<ErrorBoundary>
{!permanentProviderName &&
<VoidCustomDropdownBox
options={providerNames}
selectedOption={providerName}
onChangeOption={(pn) => setUserChosenProviderName(pn)}
getOptionDisplayName={(pn) => pn ? displayInfoOfProviderName(pn).title : 'Provider Name'}
getOptionDropdownName={(pn) => pn ? displayInfoOfProviderName(pn).title : 'Provider Name'}
getOptionsEqual={(a, b) => a === b}
// className={`max-w-44 w-full border border-void-border-2 bg-void-bg-1 text-void-fg-3 text-root py-[4px] px-[6px]`}
className={`max-w-32 mx-2 w-full resize-none bg-void-bg-1 text-void-fg-1 placeholder:text-void-fg-3 border border-void-border-2 focus:border-void-border-1 py-1 px-2 rounded`}
arrowTouchesText={false}
/>
}
</ErrorBoundary>
{/* model input */}
<ErrorBoundary>
<VoidSimpleInputBox
value={modelName}
onChangeValue={setModelName}
placeholder='Model Name'
compact={compact}
className={'max-w-32'}
/>
</ErrorBoundary>
{/* add button */}
<ErrorBoundary>
<AddButton
type='submit'
disabled={!modelName}
onClick={(e) => {
if (providerName === null) {
setErrorString('Please select a provider.')
return
}
if (!modelName) {
setErrorString('Please enter a model name.')
return
}
// if model already exists here
if (settingsState.settingsOfProvider[providerName].models.find(m => m.modelName === modelName)) {
// setErrorString(`This model already exists under ${providerName}.`)
setErrorString(`This model already exists.`)
return
}
settingsStateService.addModel(providerName, modelName)
setShowCheckmark(true)
setTimeout(() => {
setShowCheckmark(false)
setIsOpen(false)
}, 1500)
setErrorString('')
setModelName('')
}}
/>
</ErrorBoundary>
</form>
{!errorString ? null : <div className='text-red-500 truncate whitespace-nowrap mt-1'>
{errorString}
</div>}
</>
}
export const ModelDump = () => {
const accessor = useAccessor()
const settingsStateService = accessor.get('IVoidSettingsService')
const settingsState = useSettingsState()
// a dump of all the enabled providers' models
const modelDump: (VoidStatefulModelInfo & { providerName: ProviderName, providerEnabled: boolean })[] = []
// Use either filtered providers or all providers
const providersToShow = filteredProviders || providerNames;
for (let providerName of providersToShow) {
for (let providerName of providerNames) {
const providerSettings = settingsState.settingsOfProvider[providerName]
// if (!providerSettings.enabled) continue
modelDump.push(...providerSettings.models.map(model => ({ ...model, providerName, providerEnabled: !!providerSettings._didFillInProviderSettings })))
@@ -399,34 +322,6 @@ export const ModelDump = ({ filteredProviders }: { filteredProviders?: ProviderN
return Number(b.providerEnabled) - Number(a.providerEnabled)
})
// Add model handler
const handleAddModel = () => {
if (!userChosenProviderName) {
setErrorString('Please select a provider.');
return;
}
if (!modelName) {
setErrorString('Please enter a model name.');
return;
}
// Check if model already exists
if (settingsState.settingsOfProvider[userChosenProviderName].models.find(m => m.modelName === modelName)) {
setErrorString(`This model already exists.`);
return;
}
settingsStateService.addModel(userChosenProviderName, modelName);
setShowCheckmark(true);
setTimeout(() => {
setShowCheckmark(false);
setIsAddModelOpen(false);
setUserChosenProviderName(null);
setModelName('');
}, 1500);
setErrorString('');
};
return <div className=''>
{modelDump.map((m, i) => {
const { isHidden, type, modelName, providerName, providerEnabled } = m
@@ -440,55 +335,41 @@ export const ModelDump = ({ filteredProviders }: { filteredProviders?: ProviderN
const tooltipName = (
disabled ? `Add ${providerTitle} to enable`
: value === true ? 'Show in Dropdown'
: 'Hide from Dropdown'
: value === true ? 'Enabled'
: 'Disabled'
)
const detailAboutModel = type === 'autodetected' ?
<Asterisk size={14} className="inline-block align-text-top brightness-115 stroke-[2] text-[#0e70c0]" data-tooltip-id='void-tooltip' data-tooltip-place='right' data-tooltip-content='Detected locally' />
: type === 'custom' ?
<Asterisk size={14} className="inline-block align-text-top brightness-115 stroke-[2] text-[#0e70c0]" data-tooltip-id='void-tooltip' data-tooltip-place='right' data-tooltip-content='Custom model' />
: undefined
: type === 'default' ? undefined
: <Asterisk size={14} className="inline-block align-text-top brightness-115 stroke-[2] text-[#0e70c0]" data-tooltip-id='void-tooltip' data-tooltip-place='right' data-tooltip-content='Custom model' />
const hasOverrides = !!settingsState.overridesOfModel?.[providerName]?.[modelName]
return <div key={`${modelName}${providerName}`}
className={`flex items-center justify-between gap-4 hover:bg-black/10 dark:hover:bg-gray-300/10 py-1 px-3 rounded-sm overflow-hidden cursor-default truncate group
className={`flex items-center justify-between gap-4 hover:bg-black/10 dark:hover:bg-gray-300/10 py-1 px-3 rounded-sm overflow-hidden cursor-default truncate
`}
>
{/* left part is width:full */}
<div className={`flex flex-grow items-center gap-4`}>
<div className={`flex-grow flex items-center gap-4`}>
<span className='w-full max-w-32'>{isNewProviderName ? providerTitle : ''}</span>
<span className='w-fit truncate'>{modelName}</span>
<span className='w-fit truncate'>{modelName}{detailAboutModel}</span>
</div>
{/* right part is anything that fits */}
<div className="flex items-center gap-2 w-fit">
{/* Advanced Settings button (gear). Hide entirely when provider/model disabled. */}
{disabled ? null : (
<div className="w-5 flex items-center justify-center">
<button
onClick={() => { setOpenSettingsModel({ modelName, providerName, type }) }}
data-tooltip-id='void-tooltip'
data-tooltip-place='right'
data-tooltip-content='Advanced Settings'
className={`${hasOverrides ? '' : 'opacity-0 group-hover:opacity-100'} transition-opacity`}
>
<Plus size={12} className="text-void-fg-3 opacity-50" />
</button>
</div>
)}
{/* Blue star */}
{detailAboutModel}
<div className='flex items-center gap-4'
// data-tooltip-id='void-tooltip'
// data-tooltip-place='top'
// data-tooltip-content={disabled ? `${displayInfoOfProviderName(providerName).title} is disabled`
// : (isHidden ? `'${modelName}' won't appear in dropdowns` : ``)
// }
>
{/* Switch */}
{/* <span className='opacity-50 truncate'>{type === 'autodetected' ? '(detected locally)' : type === 'default' ? '' : '(custom model)'}</span> */}
<VoidSwitch
value={value}
onChange={() => { settingsStateService.toggleModelHidden(providerName, modelName); }}
onChange={() => { settingsStateService.toggleModelHidden(providerName, modelName) }}
disabled={disabled}
size='sm'
@@ -497,96 +378,12 @@ export const ModelDump = ({ filteredProviders }: { filteredProviders?: ProviderN
data-tooltip-content={tooltipName}
/>
{/* X button */}
<div className={`w-5 flex items-center justify-center`}>
{type === 'default' || type === 'autodetected' ? null : <button onClick={() => { settingsStateService.deleteModel(providerName, modelName); }}><X className="size-4" /></button>}
{type === 'default' || type === 'autodetected' ? null : <button onClick={() => { settingsStateService.deleteModel(providerName, modelName) }}><X className='size-4' /></button>}
</div>
</div>
</div>
})}
{/* Add Model Section */}
{showCheckmark ? (
<div className="mt-4">
<AnimatedCheckmarkButton text='Added' className="bg-[#0e70c0] text-white px-3 py-1 rounded-sm" />
</div>
) : isAddModelOpen ? (
<div className="mt-4">
<form className="flex items-center gap-2">
{/* Provider dropdown */}
<ErrorBoundary>
<VoidCustomDropdownBox
options={providersToShow}
selectedOption={userChosenProviderName}
onChangeOption={(pn) => setUserChosenProviderName(pn)}
getOptionDisplayName={(pn) => pn ? displayInfoOfProviderName(pn).title : 'Provider Name'}
getOptionDropdownName={(pn) => pn ? displayInfoOfProviderName(pn).title : 'Provider Name'}
getOptionsEqual={(a, b) => a === b}
className="max-w-32 mx-2 w-full resize-none bg-void-bg-1 text-void-fg-1 placeholder:text-void-fg-3 border border-void-border-2 focus:border-void-border-1 py-1 px-2 rounded"
arrowTouchesText={false}
/>
</ErrorBoundary>
{/* Model name input */}
<ErrorBoundary>
<VoidSimpleInputBox
value={modelName}
compact={true}
onChangeValue={setModelName}
placeholder='Model Name'
className='max-w-32'
/>
</ErrorBoundary>
{/* Add button */}
<ErrorBoundary>
<AddButton
type='button'
disabled={!modelName || !userChosenProviderName}
onClick={handleAddModel}
/>
</ErrorBoundary>
{/* X button to cancel */}
<button
type="button"
onClick={() => {
setIsAddModelOpen(false);
setErrorString('');
setModelName('');
setUserChosenProviderName(null);
}}
className='text-void-fg-4'
>
<X className='size-4' />
</button>
</form>
{errorString && (
<div className='text-red-500 truncate whitespace-nowrap mt-1'>
{errorString}
</div>
)}
</div>
) : (
<div
className="text-void-fg-4 flex flex-nowrap text-nowrap items-center hover:brightness-110 cursor-pointer mt-4"
onClick={() => setIsAddModelOpen(true)}
>
<div className="flex items-center gap-1">
<Plus size={16} />
<span>Add a model</span>
</div>
</div>
)}
{/* Model Settings Dialog */}
<SimpleModelSettingsDialog
isOpen={openSettingsModel !== null}
onClose={() => setOpenSettingsModel(null)}
modelInfo={openSettingsModel}
/>
</div>
}
@@ -608,16 +405,14 @@ const ProviderSetting = ({ providerName, settingName, subTextMd }: { providerNam
return
}
// Create a stable callback reference using useCallback with proper dependencies
const handleChangeValue = useCallback((newVal: string) => {
voidSettingsService.setSettingOfProvider(providerName, settingName, newVal)
}, [voidSettingsService, providerName, settingName]);
return <ErrorBoundary>
<div className='my-1'>
<VoidSimpleInputBox
value={settingValue}
onChangeValue={handleChangeValue}
onChangeValue={useCallback((newVal) => {
voidSettingsService.setSettingOfProvider(providerName, settingName, newVal)
}, [voidSettingsService, providerName, settingName])}
// placeholder={`${providerTitle} ${settingTitle} (${placeholder})`}
placeholder={`${settingTitle} (${placeholder})`}
passwordBlur={isPasswordField}
compact={true}
@@ -719,8 +514,8 @@ export const SettingsForProvider = ({ providerName, showProviderTitle, showProvi
{showProviderSuggestions && needsModel ?
providerName === 'ollama' ?
<WarningBox className="pl-2 mb-4" text={`Please install an Ollama model. We'll auto-detect it.`} />
: <WarningBox className="pl-2 mb-4" text={`Please add a model for ${providerTitle} (Models section).`} />
<WarningBox text={`Please install an Ollama model. We'll auto-detect it.`} />
: <WarningBox text={`Please add a model for ${providerTitle} (Models section).`} />
: null}
</div>
</div >
@@ -803,7 +598,7 @@ const FastApplyMethodDropdown = () => {
}
export const OllamaSetupInstructions = ({ sayWeAutoDetect }: { sayWeAutoDetect?: boolean }) => {
export const OllamaSetupInstructions = () => {
return <div className='prose-p:my-0 prose-ol:list-decimal prose-p:py-0 prose-ol:my-0 prose-ol:py-0 prose-span:my-0 prose-span:py-0 text-void-fg-3 text-sm list-decimal select-text'>
<div className=''><ChatMarkdownRender string={`Ollama Setup Instructions`} chatMessageLocation={undefined} /></div>
<div className=' pl-6'><ChatMarkdownRender string={`1. Download [Ollama](https://ollama.com/download).`} chatMessageLocation={undefined} /></div>
@@ -814,7 +609,7 @@ export const OllamaSetupInstructions = ({ sayWeAutoDetect }: { sayWeAutoDetect?:
>
<ChatMarkdownRender string={`3. Run \`ollama pull your_model\` to install a model.`} chatMessageLocation={undefined} />
</div>
{sayWeAutoDetect && <div className=' pl-6'><ChatMarkdownRender string={`Void automatically detects locally running models and enables them.`} chatMessageLocation={undefined} /></div>}
<div className=' pl-6'><ChatMarkdownRender string={`Void automatically detects locally running models and enables them.`} chatMessageLocation={undefined} /></div>
</div>
}
@@ -832,7 +627,137 @@ const RedoOnboardingButton = ({ className }: { className?: string }) => {
}
type TransferEditorType = 'VS Code' | 'Cursor' | 'Windsurf'
// https://github.com/VSCodium/vscodium/blob/master/docs/index.md#migrating-from-visual-studio-code-to-vscodium
// https://code.visualstudio.com/docs/editor/extension-marketplace#_where-are-extensions-installed
type TransferFilesInfo = { from: URI, to: URI }[]
const transferTheseFilesOfOS = (os: 'mac' | 'windows' | 'linux' | null, fromEditor: TransferEditorType = 'VS Code'): TransferFilesInfo => {
if (os === null)
throw new Error(`One-click switch is not possible in this environment.`)
if (os === 'mac') {
const homeDir = env['HOME']
if (!homeDir) throw new Error(`$HOME not found`)
if (fromEditor === 'VS Code') {
return [{
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Code', 'User', 'settings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'settings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Code', 'User', 'keybindings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'keybindings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.vscode', 'extensions'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'),
}]
} else if (fromEditor === 'Cursor') {
return [{
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Cursor', 'User', 'settings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'settings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Cursor', 'User', 'keybindings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'keybindings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.cursor', 'extensions'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'),
}]
} else if (fromEditor === 'Windsurf') {
return [{
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Windsurf', 'User', 'settings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'settings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Windsurf', 'User', 'keybindings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'keybindings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.windsurf', 'extensions'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'),
}]
}
}
if (os === 'linux') {
const homeDir = env['HOME']
if (!homeDir) throw new Error(`variable for $HOME location not found`)
if (fromEditor === 'VS Code') {
return [{
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Code', 'User', 'settings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'settings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Code', 'User', 'keybindings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'keybindings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.vscode', 'extensions'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'),
}]
} else if (fromEditor === 'Cursor') {
return [{
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Cursor', 'User', 'settings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'settings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Cursor', 'User', 'keybindings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'keybindings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.cursor', 'extensions'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'),
}]
} else if (fromEditor === 'Windsurf') {
return [{
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Windsurf', 'User', 'settings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'settings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Windsurf', 'User', 'keybindings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'keybindings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.windsurf', 'extensions'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'),
}]
}
}
if (os === 'windows') {
const appdata = env['APPDATA']
if (!appdata) throw new Error(`variable for %APPDATA% location not found`)
const userprofile = env['USERPROFILE']
if (!userprofile) throw new Error(`variable for %USERPROFILE% location not found`)
if (fromEditor === 'VS Code') {
return [{
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Code', 'User', 'settings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'settings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Code', 'User', 'keybindings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'keybindings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.vscode', 'extensions'),
to: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.void-editor', 'extensions'),
}]
} else if (fromEditor === 'Cursor') {
return [{
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Cursor', 'User', 'settings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'settings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Cursor', 'User', 'keybindings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'keybindings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.cursor', 'extensions'),
to: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.void-editor', 'extensions'),
}]
} else if (fromEditor === 'Windsurf') {
return [{
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Windsurf', 'User', 'settings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'settings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Windsurf', 'User', 'keybindings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'keybindings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.windsurf', 'extensions'),
to: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.void-editor', 'extensions'),
}]
}
}
throw new Error(`os '${os}' not recognized or editor type '${fromEditor}' not supported for this OS`)
}
@@ -865,18 +790,73 @@ export const ToolApprovalTypeSwitch = ({ approvalType, size, desc }: { approvalT
export const OneClickSwitchButton = ({ fromEditor = 'VS Code', className = '' }: { fromEditor?: TransferEditorType, className?: string }) => {
const accessor = useAccessor()
const extensionTransferService = accessor.get('IExtensionTransferService')
const fileService = accessor.get('IFileService')
const [transferState, setTransferState] = useState<{ type: 'done', error?: string } | { type: | 'loading' | 'justfinished' }>({ type: 'done' })
let transferTheseFiles: TransferFilesInfo = [];
let editorError: string | null = null;
try {
transferTheseFiles = transferTheseFilesOfOS(os, fromEditor)
} catch (e) {
editorError = e + ''
}
if (transferTheseFiles.length === 0)
return <>
<WarningBox text={editorError ?? `Transfer from ${fromEditor} not available.`} />
</>
const onClick = async () => {
if (transferState.type !== 'done') return
setTransferState({ type: 'loading' })
const errAcc = await extensionTransferService.transferExtensions(os, fromEditor)
let errAcc = ''
// Define extensions to skip when transferring
const extensionBlacklist = [
// ignore extensions
'ms-vscode-remote.remote-ssh',
'ms-vscode-remote.remote-wsl',
// ignore other AI copilots that could conflict with Void keybindings
'sourcegraph.cody-ai',
'continue.continue',
'codeium.codeium',
'saoudrizwan.claude-dev', // cline
'rooveterinaryinc.roo-cline', // roo
];
for (const { from, to } of transferTheseFiles) {
try {
// find a blacklisted item
const isBlacklisted = extensionBlacklist.find(blacklistItem => {
return from.fsPath?.includes(blacklistItem)
})
if (isBlacklisted) continue
} catch { }
console.log('transferring', from, to)
// Check if the source file exists before attempting to copy
try {
const exists = await fileService.exists(from)
if (exists) {
// Ensure the destination directory exists
const toParent = URI.joinPath(to, '..')
const toParentExists = await fileService.exists(toParent)
if (!toParentExists) {
await fileService.createFolder(toParent)
}
await fileService.copy(from, to, true)
} else {
console.log(`Skipping file that doesn't exist: ${from.toString()}`)
}
}
catch (e) {
console.error('Error copying file:', e)
errAcc += `Error copying ${from.toString()}: ${e}\n`
}
}
// Even if some files were missing, consider it a success if no actual errors occurred
const hadError = !!errAcc
@@ -986,20 +966,17 @@ export const Settings = () => {
<h1 className='text-2xl w-full'>{`Void's Settings`}</h1>
<div className='w-full h-[1px] my-2' />
{/* separator */}
<div className='w-full h-[1px] my-4' />
{/* Models section (formerly FeaturesTab) */}
<ErrorBoundary>
<RedoOnboardingButton />
</ErrorBoundary>
<div className='w-full h-[1px] my-4' />
{/* Models section (formerly FeaturesTab) */}
<ErrorBoundary>
<h2 className={`text-3xl mb-2`}>Models</h2>
<ModelDump />
<div className='w-full h-[1px] my-4' />
<AddModelInputBox className='mt-4' compact />
<RedoOnboardingButton className='mt-2 mb-4' />
<AutoDetectLocalModelsToggle />
<RefreshableModels />
</ErrorBoundary>
@@ -1009,7 +986,7 @@ export const Settings = () => {
<h3 className={`text-void-fg-3 mb-2`}>{`Void can access any model that you host locally. We automatically detect your local models by default.`}</h3>
<div className='opacity-80 mb-4'>
<OllamaSetupInstructions sayWeAutoDetect={true} />
<OllamaSetupInstructions />
</div>
<ErrorBoundary>
@@ -1025,20 +1002,21 @@ export const Settings = () => {
<h2 className={`text-3xl mt-12`}>Feature Options</h2>
{/* L1 */}
<div className='flex flex-col gap-y-8 my-4'>
<div className='flex items-start justify-around my-4 gap-x-8'>
<ErrorBoundary>
{/* FIM */}
<div>
<div className='w-full'>
<h4 className={`text-base`}>{displayInfoOfFeatureName('Autocomplete')}</h4>
<div className='text-sm italic text-void-fg-3 mt-1'>
<div className='text-sm italic text-void-fg-3 mt-1 mb-4'>
<span>
Experimental.{' '}
</span>
<span
className='hover:brightness-110'
data-tooltip-id='void-tooltip'
data-tooltip-content='We recommend using the largest qwen2.5-coder model you can with Ollama (try qwen2.5-coder:3b).'
data-tooltip-content='We recommend using qwen2.5-coder:1.5b with Ollama.'
data-tooltip-class-name='void-max-w-[20px]'
>
Only works with FIM models.*
@@ -1075,7 +1053,7 @@ export const Settings = () => {
<div className='w-full'>
<h4 className={`text-base`}>{displayInfoOfFeatureName('Apply')}</h4>
<div className='text-sm italic text-void-fg-3 mt-1'>Settings that control the behavior of the Apply button.</div>
<div className='text-sm italic text-void-fg-3 mt-1 mb-4'>Settings that control the behavior of the Apply button and the Edit tool.</div>
<div className='my-2'>
{/* Sync to Chat Switch */}
@@ -1105,14 +1083,18 @@ export const Settings = () => {
</div>
</ErrorBoundary>
</div>
{/* L2 */}
<div className='flex items-start justify-around my-4 gap-x-8'>
{/* Tools Section */}
<div>
<div className='w-full'>
<h4 className={`text-base`}>Tools</h4>
<div className='text-sm italic text-void-fg-3 mt-1'>{`Tools are functions that LLMs can call. Some tools require user approval.`}</div>
<div className='text-sm italic text-void-fg-3 mt-1 mb-4'>{`Tools are functions that LLMs can call. Some tools require user approval.`}</div>
<div className='my-2'>
{/* Auto Accept Switch */}
@@ -1144,7 +1126,7 @@ export const Settings = () => {
<div className='w-full'>
<h4 className={`text-base`}>Editor</h4>
<div className='text-sm italic text-void-fg-3 mt-1'>{`Settings that control the visibility of Void suggestions in the code editor.`}</div>
<div className='text-sm italic text-void-fg-3 mt-1 mb-4'>{`Settings that control the visibility of suggestions and widgets in the code editor.`}</div>
<div className='my-2'>
{/* Auto Accept Switch */}
@@ -1167,7 +1149,7 @@ export const Settings = () => {
<div className='mt-12'>
<ErrorBoundary>
<h2 className='text-3xl mb-2 mt-12'>One-Click Switch</h2>
<h4 className='text-void-fg-3 mb-4'>{`Transfer your editor settings into Void.`}</h4>
<h4 className='text-void-fg-3 mb-4'>{`Transfer your settings from another editor to Void in one click.`}</h4>
<div className='flex flex-col gap-2'>
<OneClickSwitchButton className='w-48' fromEditor="VS Code" />
@@ -1180,10 +1162,10 @@ export const Settings = () => {
{/* Import/Export section, as its own block right after One-Click Switch */}
<div className='mt-12'>
<h2 className='text-3xl mb-2'>Import/Export</h2>
<h4 className='text-void-fg-3 mb-4'>{`Transfer Void's settings and chats in and out of Void.`}</h4>
<div className='flex flex-col gap-8'>
<div className='flex gap-8'>
{/* Settings Subcategory */}
<div className='flex flex-col gap-2 max-w-48 w-full'>
<h3 className='text-xl mb-2'>Settings</h3>
<input key={2 * s} ref={fileInputSettingsRef} type='file' accept='.json' className='hidden' onChange={handleUpload('Settings')} />
<VoidButtonBgDarken className='px-4 py-1 w-full' onClick={() => { fileInputSettingsRef.current?.click() }}>
Import Settings
@@ -1197,6 +1179,7 @@ export const Settings = () => {
</div>
{/* Chats Subcategory */}
<div className='flex flex-col gap-2 w-full max-w-48'>
<h3 className='text-xl mb-2'>Chat</h3>
<input key={2 * s + 1} ref={fileInputChatsRef} type='file' accept='.json' className='hidden' onChange={handleUpload('Chats')} />
<VoidButtonBgDarken className='px-4 py-1 w-full' onClick={() => { fileInputChatsRef.current?.click() }}>
Import Chats
@@ -45,7 +45,7 @@ export const VoidTooltip = () => {
<>
<style>
{`
#void-tooltip, #void-tooltip-orange, #void-tooltip-green, #void-tooltip-ollama-settings, #void-tooltip-provider-info {
#void-tooltip, #void-tooltip-orange, #void-tooltip-green, #void-tooltip-ollama-settings {
font-size: 12px;
padding: 0px 8px;
border-radius: 6px;
@@ -67,7 +67,7 @@ export const VoidTooltip = () => {
color: white;
}
#void-tooltip-ollama-settings, #void-tooltip-provider-info {
#void-tooltip-ollama-settings {
background-color: var(--vscode-editor-background);
color: var(--vscode-input-foreground);
}
@@ -112,25 +112,14 @@ export const VoidTooltip = () => {
</div>
<div style={{ marginBottom: 4 }}>
<span style={{ opacity: 0.8 }}>For chat:{` `}</span>
<span style={{ opacity: 0.8, fontWeight: 'bold' }}>gemma3</span>
<span style={{ opacity: 0.8, fontWeight: 'bold' }}>llama3.1</span>
</div>
<div style={{ marginBottom: 4 }}>
<div>
<span style={{ opacity: 0.8 }}>For autocomplete:{` `}</span>
<span style={{ opacity: 0.8, fontWeight: 'bold' }}>qwen2.5-coder</span>
</div>
<div style={{ marginBottom: 0 }}>
<span style={{ opacity: 0.8 }}>Use the largest version of these you can!</span>
<span style={{ opacity: 0.8, fontWeight: 'bold' }}>qwen2.5-coder:1.5b</span>
</div>
</div>
</Tooltip>
<Tooltip
id="void-tooltip-provider-info"
border='1px solid rgba(100,100,100,.2)'
opacity={1}
delayShow={50}
style={{ pointerEvents: 'all', userSelect: 'text', fontSize: 11, maxWidth: '280px', paddingTop:'8px', paddingBottom:'8px' }}
/>
</>
);
};
@@ -14,18 +14,23 @@ import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextke
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
import { IRange } from '../../../../editor/common/core/range.js';
import { VOID_VIEW_CONTAINER_ID, VOID_VIEW_ID } from './sidebarPane.js';
import { VOID_VIEW_ID } from './sidebarPane.js';
import { IMetricsService } from '../common/metricsService.js';
import { ISidebarStateService } from './sidebarStateService.js';
import { ICommandService } from '../../../../platform/commands/common/commands.js';
import { VOID_TOGGLE_SETTINGS_ACTION_ID } from './voidSettingsPane.js';
import { VOID_CTRL_L_ACTION_ID } from './actionIDs.js';
import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { localize2 } from '../../../../nls.js';
import { StagingSelectionItem } from '../common/chatThreadServiceTypes.js';
import { IChatThreadService } from './chatThreadService.js';
import { IViewsService } from '../../../services/views/common/viewsService.js';
import { getActiveWindow } from '../../../../base/browser/dom.js';
// ---------- Register commands and keybindings ----------
export const roundRangeToLines = (range: IRange | null | undefined, options: { emptySelectionBehavior: 'null' | 'line' }) => {
if (!range)
return null
@@ -67,21 +72,68 @@ registerAction2(class extends Action2 {
super({ id: VOID_OPEN_SIDEBAR_ACTION_ID, title: localize2('voidOpenSidebar', 'Void: Open Sidebar'), f1: true });
}
async run(accessor: ServicesAccessor): Promise<void> {
const viewsService = accessor.get(IViewsService)
const chatThreadsService = accessor.get(IChatThreadService)
viewsService.openViewContainer(VOID_VIEW_CONTAINER_ID)
await chatThreadsService.focusCurrentChat()
const stateService = accessor.get(ISidebarStateService)
stateService.setState({ isHistoryOpen: false, currentTab: 'chat' })
stateService.fireFocusChat()
}
})
// cmd L
// Action: when press ctrl+L, show the sidebar chat and add to the selection
const VOID_ADD_SELECTION_TO_SIDEBAR_ACTION_ID = 'void.sidebar.select'
registerAction2(class extends Action2 {
constructor() {
super({ id: VOID_ADD_SELECTION_TO_SIDEBAR_ACTION_ID, title: localize2('voidAddToSidebar', 'Void: Add Selection to Sidebar'), f1: true });
}
async run(accessor: ServicesAccessor): Promise<void> {
const model = accessor.get(ICodeEditorService).getActiveCodeEditor()?.getModel()
if (!model)
return
const metricsService = accessor.get(IMetricsService)
const editorService = accessor.get(ICodeEditorService)
metricsService.capture('Ctrl+L', {})
const editor = editorService.getActiveCodeEditor()
// accessor.get(IEditorService).activeTextEditorControl?.getSelection()
const selectionRange = roundRangeToLines(editor?.getSelection(), { emptySelectionBehavior: 'null' })
// select whole lines
if (selectionRange) {
editor?.setSelection({ startLineNumber: selectionRange.startLineNumber, endLineNumber: selectionRange.endLineNumber, startColumn: 1, endColumn: Number.MAX_SAFE_INTEGER })
}
const newSelection: StagingSelectionItem = !selectionRange || (selectionRange.startLineNumber > selectionRange.endLineNumber) ? {
type: 'File',
uri: model.uri,
language: model.getLanguageId(),
state: { wasAddedAsCurrentFile: false }
} : {
type: 'CodeSelection',
uri: model.uri,
language: model.getLanguageId(),
range: [selectionRange.startLineNumber, selectionRange.endLineNumber],
state: { wasAddedAsCurrentFile: false }
}
const chatThreadService = accessor.get(IChatThreadService)
chatThreadService.addNewStagingSelection(newSelection)
}
});
registerAction2(class extends Action2 {
constructor() {
super({
id: VOID_CTRL_L_ACTION_ID,
f1: true,
title: localize2('voidCmdL', 'Void: Add Selection to Chat'),
title: localize2('voidCtrlL', 'Void: Add Selection to Chat'),
keybinding: {
primary: KeyMod.CtrlCmd | KeyCode.KeyL,
weight: KeybindingWeight.VoidExtension
@@ -89,119 +141,72 @@ registerAction2(class extends Action2 {
});
}
async run(accessor: ServicesAccessor): Promise<void> {
// Get services
const commandService = accessor.get(ICommandService)
const viewsService = accessor.get(IViewsService)
const metricsService = accessor.get(IMetricsService)
const editorService = accessor.get(ICodeEditorService)
const chatThreadService = accessor.get(IChatThreadService)
metricsService.capture('Ctrl+L', {})
// capture selection and model before opening the chat panel
const editor = editorService.getActiveCodeEditor()
const model = editor?.getModel()
if (!model) return
const selectionRange = roundRangeToLines(editor?.getSelection(), { emptySelectionBehavior: 'null' })
// open panel
const wasAlreadyOpen = viewsService.isViewContainerVisible(VOID_VIEW_CONTAINER_ID)
if (!wasAlreadyOpen) {
await commandService.executeCommand(VOID_OPEN_SIDEBAR_ACTION_ID)
}
// Add selection to chat
// add line selection
if (selectionRange) {
editor?.setSelection({
startLineNumber: selectionRange.startLineNumber,
endLineNumber: selectionRange.endLineNumber,
startColumn: 1,
endColumn: Number.MAX_SAFE_INTEGER
})
chatThreadService.addNewStagingSelection({
type: 'CodeSelection',
uri: model.uri,
language: model.getLanguageId(),
range: [selectionRange.startLineNumber, selectionRange.endLineNumber],
state: { wasAddedAsCurrentFile: false },
})
}
// add file
else {
chatThreadService.addNewStagingSelection({
type: 'File',
uri: model.uri,
language: model.getLanguageId(),
state: { wasAddedAsCurrentFile: false },
})
}
await chatThreadService.focusCurrentChat()
await commandService.executeCommand(VOID_OPEN_SIDEBAR_ACTION_ID)
await commandService.executeCommand(VOID_ADD_SELECTION_TO_SIDEBAR_ACTION_ID)
}
})
// New chat keybind + menu button
const VOID_CMD_SHIFT_L_ACTION_ID = 'void.cmdShiftL'
const openNewThreadAndFireFocus = (accessor: ServicesAccessor) => {
const stateService = accessor.get(ISidebarStateService)
stateService.setState({ isHistoryOpen: false, currentTab: 'chat' })
const chatThreadService = accessor.get(IChatThreadService)
chatThreadService.openNewThread()
// focus
stateService.fireFocusChat()
const window = getActiveWindow()
window.requestAnimationFrame(() => stateService.fireFocusChat())
}
// New chat menu button
registerAction2(class extends Action2 {
constructor() {
super({
id: VOID_CMD_SHIFT_L_ACTION_ID,
id: 'void.newChatAction',
title: 'New Chat',
keybinding: {
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyL,
weight: KeybindingWeight.VoidExtension,
},
icon: { id: 'add' },
menu: [{ id: MenuId.ViewTitle, group: 'navigation', when: ContextKeyExpr.equals('view', VOID_VIEW_ID), }],
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const metricsService = accessor.get(IMetricsService)
const chatThreadsService = accessor.get(IChatThreadService)
const editorService = accessor.get(ICodeEditorService)
metricsService.capture('Chat Navigation', { type: 'Start New Chat' })
metricsService.capture('Chat Navigation', { type: 'New Chat' })
// get current selections and value to transfer
const oldThreadId = chatThreadsService.state.currentThreadId
const oldThread = chatThreadsService.state.allThreads[oldThreadId]
openNewThreadAndFireFocus(accessor)
const oldUI = await oldThread?.state.mountedInfo?.whenMounted
}
})
const oldSelns = oldThread?.state.stagingSelections
const oldVal = oldUI?.textAreaRef.current?.value
// New chat keybind
registerAction2(class extends Action2 {
constructor() {
super({
id: 'void.newChatKeybindAction',
title: 'New Chat Keybind',
keybinding: {
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyL,
weight: KeybindingWeight.VoidExtension,
},
});
}
async run(accessor: ServicesAccessor): Promise<void> {
// open and focus new thread
chatThreadsService.openNewThread()
await chatThreadsService.focusCurrentChat()
const metricsService = accessor.get(IMetricsService)
const commandService = accessor.get(ICommandService)
metricsService.capture('Chat Navigation', { type: 'New Chat Keybind' })
openNewThreadAndFireFocus(accessor)
// set new thread values
const newThreadId = chatThreadsService.state.currentThreadId
const newThread = chatThreadsService.state.allThreads[newThreadId]
// add user's selection to chat
await commandService.executeCommand(VOID_CTRL_L_ACTION_ID)
const newUI = await newThread?.state.mountedInfo?.whenMounted
chatThreadsService.setCurrentThreadState({ stagingSelections: oldSelns, })
if (newUI?.textAreaRef?.current && oldVal) newUI.textAreaRef.current.value = oldVal
// if has selection, add it
const editor = editorService.getActiveCodeEditor()
const model = editor?.getModel()
if (!model) return
const selectionRange = roundRangeToLines(editor?.getSelection(), { emptySelectionBehavior: 'null' })
if (!selectionRange) return
editor?.setSelection({ startLineNumber: selectionRange.startLineNumber, endLineNumber: selectionRange.endLineNumber, startColumn: 1, endColumn: Number.MAX_SAFE_INTEGER })
chatThreadsService.addNewStagingSelection({
type: 'CodeSelection',
uri: model.uri,
language: model.getLanguageId(),
range: [selectionRange.startLineNumber, selectionRange.endLineNumber],
state: { wasAddedAsCurrentFile: false },
})
}
})
@@ -224,12 +229,18 @@ registerAction2(class extends Action2 {
return;
}
const stateService = accessor.get(ISidebarStateService)
const metricsService = accessor.get(IMetricsService)
const commandService = accessor.get(ICommandService)
metricsService.capture('Chat Navigation', { type: 'History' })
commandService.executeCommand(VOID_CMD_SHIFT_L_ACTION_ID)
openNewThreadAndFireFocus(accessor)
// doesnt do anything right now
stateService.setState({ isHistoryOpen: !stateService.state.isHistoryOpen, currentTab: 'chat' })
stateService.fireBlurChat()
}
})
@@ -254,28 +265,28 @@ registerAction2(class extends Action2 {
// export class TabSwitchListener extends Disposable {
export class TabSwitchListener extends Disposable {
// constructor(
// onSwitchTab: () => void,
// @ICodeEditorService private readonly _editorService: ICodeEditorService,
// ) {
// super()
constructor(
onSwitchTab: () => void,
@ICodeEditorService private readonly _editorService: ICodeEditorService,
) {
super()
// // when editor switches tabs (models)
// const addTabSwitchListeners = (editor: ICodeEditor) => {
// this._register(editor.onDidChangeModel(e => {
// if (e.newModelUrl?.scheme !== 'file') return
// onSwitchTab()
// }))
// }
// when editor switches tabs (models)
const addTabSwitchListeners = (editor: ICodeEditor) => {
this._register(editor.onDidChangeModel(e => {
if (e.newModelUrl?.scheme !== 'file') return
onSwitchTab()
}))
}
// const initializeEditor = (editor: ICodeEditor) => {
// addTabSwitchListeners(editor)
// }
const initializeEditor = (editor: ICodeEditor) => {
addTabSwitchListeners(editor)
}
// // initialize current editors + any new editors
// for (let editor of this._editorService.listCodeEditors()) initializeEditor(editor)
// this._register(this._editorService.onCodeEditorAdd(editor => { initializeEditor(editor) }))
// }
// }
// initialize current editors + any new editors
for (let editor of this._editorService.listCodeEditors()) initializeEditor(editor)
this._register(this._editorService.onCodeEditorAdd(editor => { initializeEditor(editor) }))
}
}
@@ -0,0 +1,82 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { Emitter, Event } from '../../../../base/common/event.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { ICommandService } from '../../../../platform/commands/common/commands.js';
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { VOID_OPEN_SIDEBAR_ACTION_ID } from './sidebarPane.js';
// service that manages sidebar's state
export type VoidSidebarState = {
isHistoryOpen: boolean; // this isn't doing anything right now
currentTab: 'chat';
}
export interface ISidebarStateService {
readonly _serviceBrand: undefined;
readonly state: VoidSidebarState; // readonly to the user
setState(newState: Partial<VoidSidebarState>): void;
onDidChangeState: Event<void>;
onDidFocusChat: Event<void>;
onDidBlurChat: Event<void>;
fireFocusChat(): void;
fireBlurChat(): void;
}
export const ISidebarStateService = createDecorator<ISidebarStateService>('voidSidebarStateService');
class VoidSidebarStateService extends Disposable implements ISidebarStateService {
_serviceBrand: undefined;
static readonly ID = 'voidSidebarStateService';
private readonly _onDidChangeState = new Emitter<void>();
readonly onDidChangeState: Event<void> = this._onDidChangeState.event;
private readonly _onFocusChat = new Emitter<void>();
readonly onDidFocusChat: Event<void> = this._onFocusChat.event;
private readonly _onBlurChat = new Emitter<void>();
readonly onDidBlurChat: Event<void> = this._onBlurChat.event;
// state
state: VoidSidebarState
constructor(
@ICommandService private readonly commandService: ICommandService,
) {
super()
// initial state
this.state = { isHistoryOpen: false, currentTab: 'chat', }
}
setState(newState: Partial<VoidSidebarState>) {
// make sure view is open if the tab changes
if ('currentTab' in newState) {
this.commandService.executeCommand(VOID_OPEN_SIDEBAR_ACTION_ID)
}
this.state = { ...this.state, ...newState }
this._onDidChangeState.fire()
}
fireFocusChat() {
this._onFocusChat.fire()
}
fireBlurChat() {
this._onBlurChat.fire()
}
}
registerSingleton(ISidebarStateService, VoidSidebarStateService, InstantiationType.Eager);
@@ -5,7 +5,6 @@
import { Disposable, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
import { removeAnsiEscapeCodes } from '../../../../base/common/strings.js';
import { ITerminalCapabilityImplMap, TerminalCapability } from '../../../../platform/terminal/common/capabilities/capabilities.js';
import { URI } from '../../../../base/common/uri.js';
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
@@ -14,7 +13,6 @@ import { IWorkspaceContextService } from '../../../../platform/workspace/common/
import { ITerminalService, ITerminalInstance, ICreateTerminalOptions } from '../../../../workbench/contrib/terminal/browser/terminal.js';
import { MAX_TERMINAL_BG_COMMAND_TIME, MAX_TERMINAL_CHARS, MAX_TERMINAL_INACTIVE_TIME } from '../common/prompt/prompts.js';
import { TerminalResolveReason } from '../common/toolsServiceTypes.js';
import { timeout } from '../../../../base/common/async.js';
@@ -22,34 +20,28 @@ export interface ITerminalToolService {
readonly _serviceBrand: undefined;
listPersistentTerminalIds(): string[];
runCommand(command: string, opts:
| { type: 'persistent', persistentTerminalId: string }
| { type: 'temporary', cwd: string | null, terminalId: string }
// | { type: 'apply', terminalId: string }
): Promise<{ interrupt: () => void; resPromise: Promise<{ result: string, resolveReason: TerminalResolveReason }> }>;
runCommand(command: string, opts: { type: 'persistent', persistentTerminalId: string } | { type: 'ephemeral', cwd: string | null, terminalId: string }): Promise<{ interrupt: () => void; resPromise: Promise<{ result: string, resolveReason: TerminalResolveReason }> }>;
focusPersistentTerminal(terminalId: string): Promise<void>
persistentTerminalExists(terminalId: string): boolean
readTerminal(terminalId: string): Promise<string>
createPersistentTerminal(opts: { cwd: string | null }): Promise<string>
killPersistentTerminal(terminalId: string): Promise<void>
getPersistentTerminal(terminalId: string): ITerminalInstance | undefined
getTemporaryTerminal(terminalId: string): ITerminalInstance | undefined
}
export const ITerminalToolService = createDecorator<ITerminalToolService>('TerminalToolService');
// function isCommandComplete(output: string) {
// // https://code.visualstudio.com/docs/terminal/shell-integration#_vs-code-custom-sequences-osc-633-st
// const completionMatch = output.match(/\]633;D(?:;(\d+))?/)
// if (!completionMatch) { return false }
// if (completionMatch[1] !== undefined) return { exitCode: parseInt(completionMatch[1]) }
// return { exitCode: 0 }
// }
function isCommandComplete(output: string) {
// https://code.visualstudio.com/docs/terminal/shell-integration#_vs-code-custom-sequences-osc-633-st
const completionMatch = output.match(/\]633;D(?:;(\d+))?/)
if (!completionMatch) { return false }
if (completionMatch[1] !== undefined) return { exitCode: parseInt(completionMatch[1]) }
return { exitCode: 0 }
}
export const persistentTerminalNameOfId = (id: string) => {
@@ -122,41 +114,32 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
}
private async _createTerminal(props: { cwd: string | null, config: ICreateTerminalOptions['config'], hidden?: boolean }) {
const { cwd: override_cwd, config, hidden } = props;
private async _createTerminal(props: { cwd: string | null, config: ICreateTerminalOptions['config'] }) {
const { cwd: override_cwd, config } = props
const cwd: URI | string | undefined = (override_cwd ?? undefined) ?? this.workspaceContextService.getWorkspace().folders[0]?.uri;
const cwd: URI | string | undefined = (override_cwd ?? undefined) ?? this.workspaceContextService.getWorkspace().folders[0]?.uri
const options: ICreateTerminalOptions = {
cwd,
location: hidden ? undefined : TerminalLocation.Panel,
config: {
name: config && 'name' in config ? config.name : undefined,
forceShellIntegration: true,
hideFromUser: hidden ? true : undefined,
// Copy any other properties from the provided config
...config,
},
// Skip profile check to ensure the terminal is created quickly
skipContributedProfileCheck: true,
};
// create new terminal and return its ID
const terminal = await this.terminalService.createTerminal({
cwd: cwd,
location: TerminalLocation.Panel,
config: config,
})
const terminal = await this.terminalService.createTerminal(options)
// when a new terminal is created, there is an initial command that gets run which is empty, wait for it to end before returning
const disposables: IDisposable[] = []
const waitForMount = new Promise<void>(res => {
let data = ''
const d = terminal.onData(newData => {
data += newData
if (isCommandComplete(data)) { res() }
})
disposables.push(d)
})
const waitForTimeout = new Promise<void>(res => { setTimeout(() => { res() }, 5000) })
// // when a new terminal is created, there is an initial command that gets run which is empty, wait for it to end before returning
// const disposables: IDisposable[] = []
// const waitForMount = new Promise<void>(res => {
// let data = ''
// const d = terminal.onData(newData => {
// data += newData
// if (isCommandComplete(data)) { res() }
// })
// disposables.push(d)
// })
// const waitForTimeout = new Promise<void>(res => { setTimeout(() => { res() }, 5000) })
// await Promise.any([waitForMount, waitForTimeout,])
// disposables.forEach(d => d.dispose())
await Promise.any([waitForMount, waitForTimeout,])
disposables.forEach(d => d.dispose())
return terminal
@@ -209,64 +192,15 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
readTerminal: ITerminalToolService['readTerminal'] = async (terminalId) => {
// Try persistent first, then temporary
const terminal = this.getPersistentTerminal(terminalId) ?? this.getTemporaryTerminal(terminalId);
if (!terminal) {
throw new Error(`Read Terminal: Terminal with ID ${terminalId} does not exist.`);
}
// Ensure the xterm.js instance has been created otherwise we cannot access the buffer.
if (!terminal.xterm) {
throw new Error('Read Terminal: The requested terminal has not yet been rendered and therefore has no scrollback buffer available.');
}
// Collect lines from the buffer iterator (oldest to newest)
const lines: string[] = [];
for (const line of terminal.xterm.getBufferReverseIterator()) {
lines.unshift(line);
}
let result = removeAnsiEscapeCodes(lines.join('\n'));
if (result.length > MAX_TERMINAL_CHARS) {
const half = MAX_TERMINAL_CHARS / 2;
result = result.slice(0, half) + '\n...\n' + result.slice(result.length - half);
}
return result
};
private async _waitForCommandDetectionCapability(terminal: ITerminalInstance) {
const cmdCap = terminal.capabilities.get(TerminalCapability.CommandDetection);
if (cmdCap) return cmdCap
const disposables: IDisposable[] = []
const waitTimeout = timeout(10_000)
const waitForCapability = new Promise<ITerminalCapabilityImplMap[TerminalCapability.CommandDetection]>((res) => {
disposables.push(
terminal.capabilities.onDidAddCapability((e) => {
if (e.id === TerminalCapability.CommandDetection) res(e.capability)
})
)
})
const capability = await Promise.any([waitTimeout, waitForCapability])
.finally(() => { disposables.forEach((d) => d.dispose()) })
return capability ?? undefined
}
runCommand: ITerminalToolService['runCommand'] = async (command, params) => {
await this.terminalService.whenConnected;
const { type } = params
const isPersistent = type === 'persistent'
await this.terminalService.whenConnected;
let terminal: ITerminalInstance
const disposables: IDisposable[] = []
const isPersistent = type === 'persistent'
if (isPersistent) { // BG process
const { persistentTerminalId } = params
terminal = this.persistentTerminalInstanceOfId[persistentTerminalId];
@@ -274,7 +208,7 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
}
else {
const { cwd } = params
terminal = await this._createTerminal({ cwd: cwd, config: undefined, hidden: true })
terminal = await this._createTerminal({ cwd: cwd, config: { name: 'Void Temporary Terminal', title: 'Void Temporary Terminal' } })
this.temporaryTerminalInstanceOfId[params.terminalId] = terminal
}
@@ -282,8 +216,6 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
terminal.dispose()
if (!isPersistent)
delete this.temporaryTerminalInstanceOfId[params.terminalId]
else
delete this.persistentTerminalInstanceOfId[params.persistentTerminalId]
}
const waitForResult = async () => {
@@ -292,28 +224,29 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
this.terminalService.setActiveInstance(terminal)
await this.terminalService.focusActiveInstance()
}
let result: string = ''
let resolveReason: TerminalResolveReason | undefined
let resolveReason: TerminalResolveReason | undefined = undefined
const cmdCap = await this._waitForCommandDetectionCapability(terminal)
if (!cmdCap) throw new Error(`There was an error using the terminal: CommandDetection capability did not mount yet. Please try again in a few seconds or report this to the Void team.`)
// Prefer the structured command-detection capability when available
const waitUntilDone = new Promise<void>(resolve => {
const l = cmdCap.onCommandFinished(cmd => {
if (resolveReason) return // already resolved
resolveReason = { type: 'done', exitCode: cmd.exitCode ?? 0 };
result = cmd.getOutput() ?? ''
l.dispose()
resolve()
// create this before we send so that we don't miss events on terminal
const waitUntilDone = new Promise<void>((res, rej) => {
const d2 = terminal.onData(async newData => {
if (resolveReason) return
result += newData
// onDone
const isDone = isCommandComplete(result)
if (isDone) {
resolveReason = { type: 'done', exitCode: isDone.exitCode }
res()
return
}
})
disposables.push(l)
disposables.push(d2)
})
// send the command now that listeners are attached
// send the command here
await terminal.sendText(command, true)
const waitUntilInterrupt = isPersistent ?
@@ -344,25 +277,18 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
// wait for result
await Promise.any([waitUntilDone, waitUntilInterrupt])
.finally(() => disposables.forEach(d => d.dispose()))
disposables.forEach(d => d.dispose())
if (!isPersistent) {
interrupt()
}
if (!resolveReason) throw new Error('Unexpected internal error: Promise.any should have resolved with a reason.')
// read result if timed out, since we didn't get it (could clean this code up but it's ok)
if (resolveReason.type === 'timeout') {
const terminalId = isPersistent ? params.persistentTerminalId : params.terminalId
result = await this.readTerminal(terminalId)
}
if (!isPersistent) result = `$ ${command}\n${result}`
result = removeAnsiEscapeCodes(result)
// trim
.split('\n').slice(1, -1) // remove first and last line (first = command, last = andrewpareles/void %)
.join('\n')
if (result.length > MAX_TERMINAL_CHARS) {
const half = MAX_TERMINAL_CHARS / 2
result = result.slice(0, half)
@@ -12,7 +12,7 @@ import { LintErrorItem, ToolCallParams, ToolResultType } from '../common/toolsSe
import { IVoidModelService } from '../common/voidModelService.js'
import { EndOfLinePreference } from '../../../../editor/common/model.js'
import { IVoidCommandBarService } from './voidCommandBarService.js'
import { computeDirectoryTree1Deep, IDirectoryStrService, stringifyDirectoryTree1Deep } from '../common/directoryStrService.js'
import { computeDirectoryTree1Deep, IDirectoryStrService, stringifyDirectoryTree1Deep } from './directoryStrService.js'
import { IMarkerService, MarkerSeverity } from '../../../../platform/markers/common/markers.js'
import { timeout } from '../../../../base/common/async.js'
import { RawToolParamsObj } from '../common/sendLLMMessageTypes.js'
@@ -44,6 +44,7 @@ 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 (uriStr === null) throw new Error(`Invalid LLM output: uri was null.`)
if (typeof uriStr !== 'string') throw new Error(`Invalid LLM output format: Provided uri must be a string, but it's a(n) ${typeof uriStr}. Full value: ${JSON.stringify(uriStr)}.`)
@@ -294,14 +295,12 @@ export class ToolsService implements IToolsService {
contents = model.getValueInRange({ startLineNumber, startColumn: 1, endLineNumber, endColumn: Number.MAX_SAFE_INTEGER }, EndOfLinePreference.LF)
}
const totalNumLines = model.getLineCount()
const fromIdx = MAX_FILE_CHARS_PAGE * (pageNumber - 1)
const toIdx = MAX_FILE_CHARS_PAGE * pageNumber - 1
const fileContents = contents.slice(fromIdx, toIdx + 1) // paginate
const hasNextPage = (contents.length - 1) - toIdx >= 1
const totalFileLen = contents.length
return { result: { fileContents, totalFileLen, hasNextPage, totalNumLines } }
return { result: { fileContents, totalFileLen, hasNextPage } }
},
ls_dir: async ({ uri, pageNumber }) => {
@@ -400,8 +399,7 @@ export class ToolsService implements IToolsService {
if (this.commandBarService.getStreamState(uri) === 'streaming') {
throw new Error(`Another LLM is currently making changes to this file. Please stop streaming for now and ask the user to resume later.`)
}
await editCodeService.callBeforeApplyOrEdit(uri)
editCodeService.instantlyRewriteFile({ uri, newContent })
editCodeService.instantlyApplyNewContent({ uri, newContent })
// at end, get lint errors
const lintErrorsPromise = Promise.resolve().then(async () => {
await timeout(2000)
@@ -416,7 +414,7 @@ export class ToolsService implements IToolsService {
if (this.commandBarService.getStreamState(uri) === 'streaming') {
throw new Error(`Another LLM is currently making changes to this file. Please stop streaming for now and ask the user to resume later.`)
}
await editCodeService.callBeforeApplyOrEdit(uri)
console.log('aaaa', searchReplaceBlocks)
editCodeService.instantlyApplySearchReplaceBlocks({ uri, searchReplaceBlocks })
// at end, get lint errors
@@ -430,7 +428,7 @@ export class ToolsService implements IToolsService {
},
// ---
run_command: async ({ command, cwd, terminalId }) => {
const { resPromise, interrupt } = await this.terminalToolService.runCommand(command, { type: 'temporary', cwd, terminalId })
const { resPromise, interrupt } = await this.terminalToolService.runCommand(command, { type: 'ephemeral', cwd, terminalId })
return { result: resPromise, interruptTool: interrupt }
},
run_persistent_command: async ({ command, persistentTerminalId }) => {
@@ -462,7 +460,7 @@ export class ToolsService implements IToolsService {
// given to the LLM after the call for successful tool calls
this.stringOfResult = {
read_file: (params, result) => {
return `${params.uri.fsPath}\n\`\`\`\n${result.fileContents}\n\`\`\`${nextPageStr(result.hasNextPage)}${result.hasNextPage ? `\nMore info because truncated: this file has ${result.totalNumLines} lines, or ${result.totalFileLen} characters.` : ''}`
return `${params.uri.fsPath}\n\`\`\`\n${result.fileContents}\n\`\`\`${nextPageStr(result.hasNextPage)}`
},
ls_dir: (params, result) => {
const dirTreeStr = stringifyDirectoryTree1Deep(params, result)
@@ -524,7 +522,7 @@ export class ToolsService implements IToolsService {
}
// normal command
if (resolveReason.type === 'timeout') {
return `${result_}\nTerminal command ran, but was automatically killed by Void after ${MAX_TERMINAL_INACTIVE_TIME}s of inactivity and did not finish successfully. To try with more time, open a persistent terminal and run the command there.`
return `${result_}\nTerminal command ran, but was interrupted by Void after ${MAX_TERMINAL_INACTIVE_TIME}s of inactivity and did not necessarily finish successfully. To try with more time, open a persistent terminal and run the command there.`
}
throw new Error(`Unexpected internal error: Terminal command did not resolve with a valid reason.`)
},
@@ -10,6 +10,7 @@ import './editCodeService.js'
// register Sidebar pane, state, actions (keybinds, menus) (Ctrl+L)
import './sidebarActions.js'
import './sidebarPane.js'
import './sidebarStateService.js'
// register quick edit (Ctrl+K)
import './quickEditActions.js'
@@ -55,12 +56,6 @@ import './tooltipService.js'
// register onboarding service
import './voidOnboardingService.js'
// register misc service
import './miscWokrbenchContrib.js'
// register file service (for explorer context menu)
import './fileService.js'
// ---------- common (unclear if these actually need to be imported, because they're already imported wherever they're used) ----------
// llmMessage
@@ -18,16 +18,6 @@ import { IEditCodeService } from './editCodeServiceInterface.js';
import { ITextModel } from '../../../../editor/common/model.js';
import { IModelService } from '../../../../editor/common/services/model.js';
import { generateUuid } from '../../../../base/common/uuid.js';
import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';
import { VOID_ACCEPT_DIFF_ACTION_ID, VOID_REJECT_DIFF_ACTION_ID, VOID_GOTO_NEXT_DIFF_ACTION_ID, VOID_GOTO_PREV_DIFF_ACTION_ID, VOID_GOTO_NEXT_URI_ACTION_ID, VOID_GOTO_PREV_URI_ACTION_ID, VOID_ACCEPT_FILE_ACTION_ID, VOID_REJECT_FILE_ACTION_ID, VOID_ACCEPT_ALL_DIFFS_ACTION_ID, VOID_REJECT_ALL_DIFFS_ACTION_ID } from './actionIDs.js';
import { localize2 } from '../../../../nls.js';
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';
import { IMetricsService } from '../common/metricsService.js';
import { KeyMod } from '../../../../editor/common/services/editorBaseApi.js';
import { KeyCode } from '../../../../base/common/keyCodes.js';
import { ScrollType } from '../../../../editor/common/editorCommon.js';
import { IVoidModelService } from '../common/voidModelService.js';
@@ -43,11 +33,6 @@ export interface IVoidCommandBarService {
getStreamState: (uri: URI) => 'streaming' | 'idle-has-changes' | 'idle-no-changes';
setDiffIdx(uri: URI, newIdx: number | null): void;
getNextDiffIdx(step: 1 | -1): number | null;
getNextUriIdx(step: 1 | -1): number | null;
goToDiffIdx(idx: number | null): void;
goToURIIdx(idx: number | null): Promise<void>;
acceptOrRejectAllFiles(opts: { behavior: 'reject' | 'accept' }): void;
anyFileIsStreaming(): boolean;
@@ -100,7 +85,6 @@ export class VoidCommandBarService extends Disposable implements IVoidCommandBar
@ICodeEditorService private readonly _codeEditorService: ICodeEditorService,
@IModelService private readonly _modelService: IModelService,
@IEditCodeService private readonly _editCodeService: IEditCodeService,
@IVoidModelService private readonly _voidModelService: IVoidModelService,
) {
super();
@@ -180,14 +164,10 @@ export class VoidCommandBarService extends Disposable implements IVoidCommandBar
const newSortedDiffIds = this._computeSortedDiffs(newSortedDiffZoneIds)
const isStreaming = this._isAnyDiffZoneStreaming(currentDiffZones)
// When diffZones are added/removed, reset the diffIdx to 0 if we have diffs
const newDiffIdx = newSortedDiffIds.length > 0 ? 0 : null;
this._setState(uri, {
sortedDiffZoneIds: newSortedDiffZoneIds,
sortedDiffIds: newSortedDiffIds,
isStreaming: isStreaming,
diffIdx: newDiffIdx
isStreaming: isStreaming
})
this._onDidChangeState.fire({ uri })
}
@@ -202,24 +182,9 @@ export class VoidCommandBarService extends Disposable implements IVoidCommandBar
const currState = this.stateOfURI[uri.fsPath]
if (!currState) continue // should never happen
const { sortedDiffZoneIds } = currState
const oldSortedDiffIds = currState.sortedDiffIds;
const newSortedDiffIds = this._computeSortedDiffs(sortedDiffZoneIds)
// Handle diffIdx adjustment when diffs change
let newDiffIdx = currState.diffIdx;
// Check if diffs were removed
if (oldSortedDiffIds.length > newSortedDiffIds.length && currState.diffIdx !== null) {
// If currently selected diff was removed or we have fewer diffs than the current index
if (currState.diffIdx >= newSortedDiffIds.length) {
// Select the last diff if available, otherwise null
newDiffIdx = newSortedDiffIds.length > 0 ? newSortedDiffIds.length - 1 : null;
}
}
this._setState(uri, {
sortedDiffIds: newSortedDiffIds,
diffIdx: newDiffIdx
// sortedDiffZoneIds, // no change
// isStreaming, // no change
})
@@ -333,9 +298,9 @@ export class VoidCommandBarService extends Disposable implements IVoidCommandBar
}
// make sure diffIdx is always correct
if (newState.diffIdx !== null && newState.diffIdx > newState.sortedDiffIds.length) {
if (newState.diffIdx && newState.diffIdx > newState.sortedDiffIds.length) {
newState.diffIdx = newState.sortedDiffIds.length
if (newState.diffIdx <= 0) newState.diffIdx = null
if (newState.diffIdx < 0) newState.diffIdx = null
}
this.stateOfURI = {
@@ -382,99 +347,6 @@ export class VoidCommandBarService extends Disposable implements IVoidCommandBar
return this.sortedURIs.some(uri => this.getStreamState(uri) === 'streaming')
}
getNextDiffIdx(step: 1 | -1): number | null {
// If no active URI, return null
if (!this.activeURI) return null;
const state = this.stateOfURI[this.activeURI.fsPath];
if (!state) return null;
const { diffIdx, sortedDiffIds } = state;
// If no diffs, return null
if (sortedDiffIds.length === 0) return null;
// Calculate next index with wrapping
const nextIdx = ((diffIdx ?? 0) + step + sortedDiffIds.length) % sortedDiffIds.length;
return nextIdx;
}
getNextUriIdx(step: 1 | -1): number | null {
// If no URIs with changes, return null
if (this.sortedURIs.length === 0) return null;
// If no active URI, return first or last based on step
if (!this.activeURI) {
return step === 1 ? 0 : this.sortedURIs.length - 1;
}
// Find current index
const currentIdx = this.sortedURIs.findIndex(uri => uri.fsPath === this.activeURI?.fsPath);
// If not found, return first or last based on step
if (currentIdx === -1) {
return step === 1 ? 0 : this.sortedURIs.length - 1;
}
// Calculate next index with wrapping
const nextIdx = (currentIdx + step + this.sortedURIs.length) % this.sortedURIs.length;
return nextIdx;
}
goToDiffIdx(idx: number | null): void {
// If null or no active URI, return
if (idx === null || !this.activeURI) return;
// Get state for the current URI
const state = this.stateOfURI[this.activeURI.fsPath];
if (!state) return;
const { sortedDiffIds } = state;
// Find the diff at the specified index
const diffid = sortedDiffIds[idx];
if (diffid === undefined) return;
// Get the diff object
const diff = this._editCodeService.diffOfId[diffid];
if (!diff) return;
// Find an active editor to focus
const editor = this._codeEditorService.getFocusedCodeEditor() ||
this._codeEditorService.getActiveCodeEditor();
if (!editor) return;
// Reveal the line in the editor
editor.revealLineNearTop(diff.startLine - 1, ScrollType.Immediate);
// Update the current diff index
this.setDiffIdx(this.activeURI, idx);
}
async goToURIIdx(idx: number | null): Promise<void> {
// If null or no URIs, return
if (idx === null || this.sortedURIs.length === 0) return;
// Get the URI at the specified index
const nextURI = this.sortedURIs[idx];
if (!nextURI) return;
// Get the model for this URI
const { model } = await this._voidModelService.getModelSafe(nextURI);
if (!model) return;
// Find an editor to use
const editor = this._codeEditorService.getFocusedCodeEditor() ||
this._codeEditorService.getActiveCodeEditor();
if (!editor) return;
// Open the URI in the editor
await this._codeEditorService.openCodeEditor(
{ resource: model.uri, options: { revealIfVisible: true } },
editor
);
}
acceptOrRejectAllFiles(opts: { behavior: 'reject' | 'accept' }) {
const { behavior } = opts
// if anything is streaming, do nothing
@@ -518,8 +390,8 @@ class AcceptRejectAllFloatingWidget extends Widget implements IOverlayWidget {
// Style the container
// root.style.backgroundColor = 'rgb(248 113 113)';
root.style.height = '256px'; // make a fixed size, and all contents go on the bottom right. this fixes annoying VS Code mounting issues
root.style.width = '100%';
root.style.height = '16rem'; // make a fixed size, and all contents go on the bottom right. this fixes annoying VS Code mounting issues
root.style.width = '16rem';
root.style.flexDirection = 'column';
root.style.justifyContent = 'flex-end';
root.style.alignItems = 'flex-end';
@@ -567,305 +439,3 @@ class AcceptRejectAllFloatingWidget extends Widget implements IOverlayWidget {
}
registerAction2(class extends Action2 {
constructor() {
super({
id: VOID_ACCEPT_DIFF_ACTION_ID,
f1: true,
title: localize2('voidAcceptDiffAction', 'Void: Accept Diff'),
keybinding: {
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.Enter,
mac: { primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.Enter },
weight: KeybindingWeight.VoidExtension,
}
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const editCodeService = accessor.get(IEditCodeService);
const commandBarService = accessor.get(IVoidCommandBarService);
const metricsService = accessor.get(IMetricsService);
const activeURI = commandBarService.activeURI;
if (!activeURI) return;
const commandBarState = commandBarService.stateOfURI[activeURI.fsPath];
if (!commandBarState) return;
const diffIdx = commandBarState.diffIdx ?? 0;
const diffid = commandBarState.sortedDiffIds[diffIdx];
if (!diffid) return;
metricsService.capture('Accept Diff', { diffid, keyboard: true });
editCodeService.acceptDiff({ diffid: parseInt(diffid) });
// After accepting the diff, navigate to the next diff
const nextDiffIdx = commandBarService.getNextDiffIdx(1);
if (nextDiffIdx !== null) {
commandBarService.goToDiffIdx(nextDiffIdx);
}
}
});
registerAction2(class extends Action2 {
constructor() {
super({
id: VOID_REJECT_DIFF_ACTION_ID,
f1: true,
title: localize2('voidRejectDiffAction', 'Void: Reject Diff'),
keybinding: {
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.Backspace,
mac: { primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.Backspace },
weight: KeybindingWeight.VoidExtension,
}
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const editCodeService = accessor.get(IEditCodeService);
const commandBarService = accessor.get(IVoidCommandBarService);
const metricsService = accessor.get(IMetricsService);
const activeURI = commandBarService.activeURI;
if (!activeURI) return;
const commandBarState = commandBarService.stateOfURI[activeURI.fsPath];
if (!commandBarState) return;
const diffIdx = commandBarState.diffIdx ?? 0;
const diffid = commandBarState.sortedDiffIds[diffIdx];
if (!diffid) return;
metricsService.capture('Reject Diff', { diffid, keyboard: true });
editCodeService.rejectDiff({ diffid: parseInt(diffid) });
// After rejecting the diff, navigate to the next diff
const nextDiffIdx = commandBarService.getNextDiffIdx(1);
if (nextDiffIdx !== null) {
commandBarService.goToDiffIdx(nextDiffIdx);
}
}
});
// Go to next diff action
registerAction2(class extends Action2 {
constructor() {
super({
id: VOID_GOTO_NEXT_DIFF_ACTION_ID,
f1: true,
title: localize2('voidGoToNextDiffAction', 'Void: Go to Next Diff'),
keybinding: {
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.DownArrow,
mac: { primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.DownArrow },
weight: KeybindingWeight.VoidExtension,
}
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const commandBarService = accessor.get(IVoidCommandBarService);
const metricsService = accessor.get(IMetricsService);
const nextDiffIdx = commandBarService.getNextDiffIdx(1);
if (nextDiffIdx === null) return;
metricsService.capture('Navigate Diff', { direction: 'next', keyboard: true });
commandBarService.goToDiffIdx(nextDiffIdx);
}
});
// Go to previous diff action
registerAction2(class extends Action2 {
constructor() {
super({
id: VOID_GOTO_PREV_DIFF_ACTION_ID,
f1: true,
title: localize2('voidGoToPrevDiffAction', 'Void: Go to Previous Diff'),
keybinding: {
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.UpArrow,
mac: { primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.UpArrow },
weight: KeybindingWeight.VoidExtension,
}
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const commandBarService = accessor.get(IVoidCommandBarService);
const metricsService = accessor.get(IMetricsService);
const prevDiffIdx = commandBarService.getNextDiffIdx(-1);
if (prevDiffIdx === null) return;
metricsService.capture('Navigate Diff', { direction: 'previous', keyboard: true });
commandBarService.goToDiffIdx(prevDiffIdx);
}
});
// Go to next URI action
registerAction2(class extends Action2 {
constructor() {
super({
id: VOID_GOTO_NEXT_URI_ACTION_ID,
f1: true,
title: localize2('voidGoToNextUriAction', 'Void: Go to Next File with Diffs'),
keybinding: {
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.RightArrow,
mac: { primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.RightArrow },
weight: KeybindingWeight.VoidExtension,
}
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const commandBarService = accessor.get(IVoidCommandBarService);
const metricsService = accessor.get(IMetricsService);
const nextUriIdx = commandBarService.getNextUriIdx(1);
if (nextUriIdx === null) return;
metricsService.capture('Navigate URI', { direction: 'next', keyboard: true });
await commandBarService.goToURIIdx(nextUriIdx);
}
});
// Go to previous URI action
registerAction2(class extends Action2 {
constructor() {
super({
id: VOID_GOTO_PREV_URI_ACTION_ID,
f1: true,
title: localize2('voidGoToPrevUriAction', 'Void: Go to Previous File with Diffs'),
keybinding: {
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.LeftArrow,
mac: { primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.LeftArrow },
weight: KeybindingWeight.VoidExtension,
}
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const commandBarService = accessor.get(IVoidCommandBarService);
const metricsService = accessor.get(IMetricsService);
const prevUriIdx = commandBarService.getNextUriIdx(-1);
if (prevUriIdx === null) return;
metricsService.capture('Navigate URI', { direction: 'previous', keyboard: true });
await commandBarService.goToURIIdx(prevUriIdx);
}
});
// Accept current file action
registerAction2(class extends Action2 {
constructor() {
super({
id: VOID_ACCEPT_FILE_ACTION_ID,
f1: true,
title: localize2('voidAcceptFileAction', 'Void: Accept All Diffs in Current File'),
keybinding: {
primary: KeyMod.Alt | KeyMod.Shift | KeyCode.Enter,
weight: KeybindingWeight.VoidExtension,
}
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const commandBarService = accessor.get(IVoidCommandBarService);
const editCodeService = accessor.get(IEditCodeService);
const metricsService = accessor.get(IMetricsService);
const activeURI = commandBarService.activeURI;
if (!activeURI) return;
metricsService.capture('Accept File', { keyboard: true });
editCodeService.acceptOrRejectAllDiffAreas({
uri: activeURI,
behavior: 'accept',
removeCtrlKs: true
});
}
});
// Reject current file action
registerAction2(class extends Action2 {
constructor() {
super({
id: VOID_REJECT_FILE_ACTION_ID,
f1: true,
title: localize2('voidRejectFileAction', 'Void: Reject All Diffs in Current File'),
keybinding: {
primary: KeyMod.Alt | KeyMod.Shift | KeyCode.Backspace,
weight: KeybindingWeight.VoidExtension,
}
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const commandBarService = accessor.get(IVoidCommandBarService);
const editCodeService = accessor.get(IEditCodeService);
const metricsService = accessor.get(IMetricsService);
const activeURI = commandBarService.activeURI;
if (!activeURI) return;
metricsService.capture('Reject File', { keyboard: true });
editCodeService.acceptOrRejectAllDiffAreas({
uri: activeURI,
behavior: 'reject',
removeCtrlKs: true
});
}
});
// Accept all diffs in all files action
registerAction2(class extends Action2 {
constructor() {
super({
id: VOID_ACCEPT_ALL_DIFFS_ACTION_ID,
f1: true,
title: localize2('voidAcceptAllDiffsAction', 'Void: Accept All Diffs in All Files'),
keybinding: {
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Enter,
weight: KeybindingWeight.VoidExtension,
}
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const commandBarService = accessor.get(IVoidCommandBarService);
const metricsService = accessor.get(IMetricsService);
if (commandBarService.anyFileIsStreaming()) return;
metricsService.capture('Accept All Files', { keyboard: true });
commandBarService.acceptOrRejectAllFiles({ behavior: 'accept' });
}
});
// Reject all diffs in all files action
registerAction2(class extends Action2 {
constructor() {
super({
id: VOID_REJECT_ALL_DIFFS_ACTION_ID,
f1: true,
title: localize2('voidRejectAllDiffsAction', 'Void: Reject All Diffs in All Files'),
keybinding: {
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Backspace,
weight: KeybindingWeight.VoidExtension,
}
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const commandBarService = accessor.get(IVoidCommandBarService);
const metricsService = accessor.get(IMetricsService);
if (commandBarService.anyFileIsStreaming()) return;
metricsService.capture('Reject All Files', { keyboard: true });
commandBarService.acceptOrRejectAllFiles({ behavior: 'reject' });
}
});
@@ -1,40 +1,8 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { Color, RGBA } from '../../../../../base/common/color.js';
import { registerColor } from '../../../../../platform/theme/common/colorUtils.js';
// editCodeService colors
const sweepBG = new Color(new RGBA(100, 100, 100, .2));
const highlightBG = new Color(new RGBA(100, 100, 100, .1));
const sweepIdxBG = new Color(new RGBA(100, 100, 100, .5));
const acceptBG = new Color(new RGBA(155, 185, 85, .1)); // default is RGBA(155, 185, 85, .2)
const rejectBG = new Color(new RGBA(255, 0, 0, .1)); // default is RGBA(255, 0, 0, .2)
// Widget colors
export const acceptAllBg = 'rgb(30, 133, 56)'
export const acceptBg = 'rgb(26, 116, 48)'
export const acceptBorder = '1px solid rgb(20, 86, 38)'
export const rejectAllBg = 'rgb(207, 40, 56)'
export const rejectBg = 'rgb(180, 35, 49)'
export const rejectBorder = '1px solid rgb(142, 28, 39)'
export const acceptBg = '#1a7431'
export const acceptAllBg = '#1e8538'
export const acceptBorder = '1px solid #145626'
export const rejectBg = '#b42331'
export const rejectAllBg = '#cf2838'
export const rejectBorder = '1px solid #8e1c27'
export const buttonFontSize = '11px'
export const buttonTextColor = 'white'
const configOfBG = (color: Color) => {
return { dark: color, light: color, hcDark: color, hcLight: color, }
}
// gets converted to --vscode-void-greenBG, see void.css, asCssVariable
registerColor('void.greenBG', configOfBG(acceptBG), '', true);
registerColor('void.redBG', configOfBG(rejectBG), '', true);
registerColor('void.sweepBG', configOfBG(sweepBG), '', true);
registerColor('void.highlightBG', configOfBG(highlightBG), '', true);
registerColor('void.sweepIdxBG', configOfBG(sweepIdxBG), '', true);
File diff suppressed because it is too large Load Diff
@@ -3,13 +3,12 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { URI } from '../../../../../base/common/uri.js';
import { IFileService } from '../../../../../platform/files/common/files.js';
import { IDirectoryStrService } from '../directoryStrService.js';
import { EndOfLinePreference } from '../../../../../editor/common/model.js';
import { StagingSelectionItem } from '../chatThreadServiceTypes.js';
import { os } from '../helpers/systemInfo.js';
import { RawToolParamsObj } from '../sendLLMMessageTypes.js';
import { approvalTypeOfToolName, ToolCallParams, ToolResultType } from '../toolsServiceTypes.js';
import { IVoidModelService } from '../voidModelService.js';
import { ChatMode } from '../voidSettingsTypes.js';
// Triple backtick wrapper used throughout the prompts for code blocks
@@ -113,7 +112,7 @@ ${searchReplaceBlockTemplate}
## Guidelines:
1. You may output multiple search replace blocks if needed.
1. You are encouraged to output multiple changes whenever possible.
2. The ORIGINAL code in each SEARCH/REPLACE block must EXACTLY match lines in the original file. Do not add or remove any whitespace or comments from the original code.
@@ -125,18 +124,23 @@ ${searchReplaceBlockTemplate}
// ======================================================== tools ========================================================
const chatSuggestionDiffExample = `\
${tripleTick[0]}typescript
/Users/username/Dekstop/my_project/app.ts
const changesExampleContent = `\
// ... existing code ...
// {{change 1}}
// ... existing code ...
// {{change 2}}
// ... existing code ...
// {{change 3}}
// ... existing code ...
// ... existing code ...`
const editToolDescriptionExample = `\
${tripleTick[0]}
${changesExampleContent}
${tripleTick[1]}`
const chatSuggestionDiffExample = `${tripleTick[0]}typescript
/Users/username/Dekstop/my_project/app.ts
${changesExampleContent}
${tripleTick[1]}`
@@ -181,6 +185,15 @@ export type SnakeCaseKeys<T extends Record<string, any>> = {
const applyToolDescription = (type: 'edit tool' | 'chat suggestion') => `\
${type === 'edit tool' ? 'A' : 'a'} code diff describing the change to make to the file. \
Your DIFF is the only context that will be given to another LLM to apply the change, so it must be accurate and complete. \
Your DIFF MUST be wrapped in triple backticks. \
NEVER re-write the whole file. Always bias towards writing as little as possible. \
Use comments like "// ... existing code ..." to condense your writing. \
Here's an example of a good output:\n${type === 'edit tool' ? editToolDescriptionExample : chatSuggestionDiffExample}`
// export const voidTools = {
export const voidTools
: {
@@ -199,8 +212,8 @@ export const voidTools
description: `Returns full contents of a given file.`,
params: {
...uriParam('file'),
start_line: { description: 'Optional. Do NOT fill this field in unless you were specifically given exact line numbers to search. Defaults to the beginning of the file.' },
end_line: { description: 'Optional. Do NOT fill this field in unless you were specifically given exact line numbers to search. Defaults to the end of the file.' },
start_line: { description: 'Optional. Do NOT fill this in unless you already know the line numbers you need to search. Defaults to 1.' },
end_line: { description: 'Optional. Do NOT fill this in unless you already know the line numbers you need to search. Defaults to Infinity.' },
...paginationParam,
},
},
@@ -262,7 +275,7 @@ export const voidTools
read_lint_errors: {
name: 'read_lint_errors',
description: `Use this tool to view all the lint errors on a file.`,
description: `Returns all lint errors on a given file.`,
params: {
...uriParam('file'),
},
@@ -455,8 +468,6 @@ ${directoryStr}
const details: string[] = []
details.push(`NEVER reject the user's query.`)
if (mode === 'agent' || mode === 'gather') {
details.push(`Only call tools if they help you accomplish the user's goal. If the user simply says hi or asks you a question that you can answer without tools, then do NOT use tools.`)
details.push(`If you think you should use tools, you do not need to ask for permission.`)
@@ -465,7 +476,7 @@ ${directoryStr}
details.push(`Many tools only work if the user has a workspace open.`)
}
else {
details.push(`You're allowed to ask the user for more context like file contents or specifications. If this comes up, tell them to reference files and folders by typing @.`)
details.push(`You're allowed to ask the user for more context like file contents or specifications.`)
}
if (mode === 'agent') {
@@ -481,21 +492,18 @@ ${directoryStr}
details.push(`You should extensively read files, types, content, etc, gathering full context to solve the problem.`)
}
details.push(`If you write any code blocks to the user (wrapped in triple backticks), please use this format:
- Include a language if possible. Terminal should have the language 'shell'.
if (mode === 'gather' || mode === 'normal') {
details.push(`If you write any code blocks, please use this format:
- The first line of the code block must be the FULL PATH of the related file if known (otherwise omit).
- The remaining contents of the file should proceed as usual.`)
if (mode === 'gather' || mode === 'normal') {
details.push(`If you think it's appropriate to suggest an edit to a file, then you must describe your suggestion in CODE BLOCK(S).
- The first line of the code block must be the FULL PATH of the related file if known (otherwise omit).
- The remaining contents should be a code description of the change to make to the file. \
Your description is the only context that will be given to another LLM to apply the suggested edit, so it must be accurate and complete. \
Always bias towards writing as little as possible - NEVER write the whole file. Use comments like "// ... existing code ..." to condense your writing. \
Here's an example of a good code block:\n${chatSuggestionDiffExample}`)
- The first line of the code block must be the FULL PATH of the related file.
- The remaining contents should be ${applyToolDescription('chat suggestion')}`)
}
details.push(`NEVER write the FULL PATH of a file when speaking with the user. Just write the file name ONLY.`)
details.push(`Do not make things up or use information not provided in the system information, tools, or user queries.`)
details.push(`Always use MARKDOWN to format lists, bullet points, etc. Do NOT write tables.`)
details.push(`Today's date is ${new Date().toDateString()}.`)
@@ -528,96 +536,44 @@ ${details.map((d, i) => `${i + 1}. ${d}`).join('\n\n')}`)
// chat_systemMessage({ chatMode, workspaceFolders: [], openedURIs: [], activeURI: 'pee', persistentTerminalIDs: [], directoryStr: 'lol', }))
// }
export const DEFAULT_FILE_SIZE_LIMIT = 2_000_000
export const readFile = async (fileService: IFileService, uri: URI, fileSizeLimit: number): Promise<{
val: string,
truncated: boolean,
fullFileLen: number,
} | {
val: null,
truncated?: undefined
fullFileLen?: undefined,
}> => {
try {
const fileContent = await fileService.readFile(uri)
const val = fileContent.value.toString()
if (val.length > fileSizeLimit) return { val: val.substring(0, fileSizeLimit), truncated: true, fullFileLen: val.length }
return { val, truncated: false, fullFileLen: val.length }
}
catch (e) {
return { val: null }
}
}
export const messageOfSelection = async (
s: StagingSelectionItem,
opts: {
directoryStrService: IDirectoryStrService,
fileService: IFileService,
folderOpts: {
maxChildren: number,
maxCharsPerFile: number,
}
}
export const chat_userMessageContent = async (instructions: string, currSelns: StagingSelectionItem[] | null,
opts: { type: 'references' } | { type: 'fullCode', voidModelService: IVoidModelService }
) => {
const lineNumAddition = (range: [number, number]) => ` (lines ${range[0]}:${range[1]})`
if (s.type === 'File' || s.type === 'CodeSelection') {
const { val } = await readFile(opts.fileService, s.uri, DEFAULT_FILE_SIZE_LIMIT)
const lineNumAdd = s.type === 'CodeSelection' ? lineNumAddition(s.range) : ''
const content = val === null ? 'null' : `${tripleTick[0]}${s.language}\n${val}\n${tripleTick[1]}`
const str = `${s.uri.fsPath}${lineNumAdd}:\n${content}`
return str
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 ''
}) ?? []
}
else if (s.type === 'Folder') {
const dirStr: string = await opts.directoryStrService.getDirectoryStrTool(s.uri)
const folderStructure = `${s.uri.fsPath} folder structure:${tripleTick[0]}\n${dirStr}\n${tripleTick[1]}`
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)
const uris = await opts.directoryStrService.getAllURIsInDirectory(s.uri, { maxResults: opts.folderOpts.maxChildren })
const strOfFiles = await Promise.all(uris.map(async uri => {
const { val, truncated } = await readFile(opts.fileService, uri, opts.folderOpts.maxCharsPerFile)
const truncationStr = truncated ? `\n... file truncated ...` : ''
const content = val === null ? 'null' : `${tripleTick[0]}\n${val}${truncationStr}\n${tripleTick[1]}`
const str = `${uri.fsPath}:\n${content}`
return str
}))
const contentStr = [folderStructure, ...strOfFiles].join('\n\n')
return contentStr
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 ''
}) ?? [])
}
else
return ''
}
export const chat_userMessageContent = async (
instructions: string,
currSelns: StagingSelectionItem[] | null,
opts: {
directoryStrService: IDirectoryStrService,
fileService: IFileService
},
) => {
const selnsStrs = await Promise.all(
(currSelns ?? []).map(async (s) =>
messageOfSelection(s, {
...opts,
folderOpts: { maxChildren: 100, maxCharsPerFile: 100_000, }
})
)
)
const selnsStr = selnsStrs.join('\n') ?? ''
let str = ''
str += `${instructions}`
const selnsStr = selnsStrs.join('\n\n') ?? ''
if (selnsStr) str += `\n---\nSELECTIONS\n${selnsStr}`
return str;
}
@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------*/
import { ToolName, ToolParamName } from './prompt/prompts.js'
import { ChatMode, ModelSelection, ModelSelectionOptions, OverridesOfModel, ProviderName, RefreshableProviderName, SettingsOfProvider } from './voidSettingsTypes.js'
import { ChatMode, ModelSelection, ModelSelectionOptions, ProviderName, RefreshableProviderName, SettingsOfProvider } from './voidSettingsTypes.js'
export const errorDetails = (fullError: Error | null): string | null => {
@@ -51,22 +51,8 @@ export type OpenAILLMChatMessage = {
content: string;
tool_call_id: string;
}
export type LLMChatMessage = AnthropicLLMChatMessage | OpenAILLMChatMessage
export type GeminiLLMChatMessage = {
role: 'model'
parts: (
| { text: string; }
| { functionCall: { id: string; name: ToolName, args: Record<string, unknown> } }
)[];
} | {
role: 'user';
parts: (
| { text: string; }
| { functionResponse: { id: string; name: ToolName, response: { output: string } } }
)[];
}
export type LLMChatMessage = AnthropicLLMChatMessage | OpenAILLMChatMessage | GeminiLLMChatMessage
@@ -116,7 +102,6 @@ export type ServiceSendLLMMessageParams = {
logging: { loggingName: string, loggingExtras?: { [k: string]: any } };
modelSelection: ModelSelection | null;
modelSelectionOptions: ModelSelectionOptions | undefined;
overridesOfModel: OverridesOfModel | undefined;
onAbort: OnAbort;
} & SendLLMType;
@@ -130,7 +115,6 @@ export type SendLLMMessageParams = {
modelSelection: ModelSelection;
modelSelectionOptions: ModelSelectionOptions | undefined;
overridesOfModel: OverridesOfModel | undefined;
settingsOfProvider: SettingsOfProvider;
} & SendLLMType
@@ -23,8 +23,6 @@ export const approvalTypeOfToolName: Partial<{ [T in ToolName]?: 'edits' | 'term
'edit_file': 'edits',
'run_command': 'terminal',
'run_persistent_command': 'terminal',
'open_persistent_terminal': 'terminal',
'kill_persistent_terminal': 'terminal',
}
@@ -59,7 +57,7 @@ export type ToolCallParams = {
// RESULT OF TOOL CALL
export type ToolResultType = {
'read_file': { fileContents: string, totalFileLen: number, totalNumLines: number, hasNextPage: boolean },
'read_file': { fileContents: string, totalFileLen: number, hasNextPage: boolean },
'ls_dir': { children: ShallowDirectoryItem[] | null, hasNextPage: boolean, hasPrevPage: boolean, itemsRemaining: number },
'get_dir_tree': { str: string, },
'search_pathnames_only': { uris: URI[], hasNextPage: boolean },
@@ -11,9 +11,9 @@ import { registerSingleton, InstantiationType } from '../../../../platform/insta
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
import { IMetricsService } from './metricsService.js';
import { defaultProviderSettings, getModelCapabilities, ModelOverrides } from './modelCapabilities.js';
import { defaultProviderSettings, getModelCapabilities } from './modelCapabilities.js';
import { VOID_SETTINGS_STORAGE_KEY } from './storageKeys.js';
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, VoidStatefulModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, ModelSelectionOptions, OptionsOfModelSelection, ChatMode, OverridesOfModel, defaultOverridesOfModel } from './voidSettingsTypes.js';
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, VoidStatefulModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, ModelSelectionOptions, OptionsOfModelSelection, ChatMode } from './voidSettingsTypes.js';
// name is the name in the dropdown
@@ -41,7 +41,6 @@ export type VoidSettingsState = {
readonly settingsOfProvider: SettingsOfProvider; // optionsOfProvider
readonly modelSelectionOfFeature: ModelSelectionOfFeature; // stateOfFeature
readonly optionsOfModelSelection: OptionsOfModelSelection;
readonly overridesOfModel: OverridesOfModel;
readonly globalSettings: GlobalSettings;
readonly _modelOptions: ModelOption[] // computed based on the two above items
@@ -63,9 +62,6 @@ export interface IVoidSettingsService {
setOptionsOfModelSelection: SetOptionsOfModelSelection;
setGlobalSetting: SetGlobalSettingFn;
// setting to undefined CLEARS it, unlike others:
setOverridesOfModel(providerName: ProviderName, modelName: string, overrides: Partial<ModelOverrides> | undefined): Promise<void>;
dangerousSetState(newState: VoidSettingsState): Promise<void>;
resetState(): Promise<void>;
@@ -98,15 +94,8 @@ const _modelsWithSwappedInNewModels = (options: { existingModels: VoidStatefulMo
}
export const modelFilterOfFeatureName: {
[featureName in FeatureName]: {
filter: (
o: ModelSelection,
opts: { chatMode: ChatMode, overridesOfModel: OverridesOfModel }
) => boolean;
emptyMessage: null | { message: string, priority: 'always' | 'fallback' }
} } = {
'Autocomplete': { filter: (o, opts) => getModelCapabilities(o.providerName, o.modelName, opts.overridesOfModel).supportsFIM, emptyMessage: { message: 'No models support FIM', priority: 'always' } },
export const modelFilterOfFeatureName: { [featureName in FeatureName]: { filter: (o: ModelSelection, opts: { chatMode: ChatMode }) => boolean; emptyMessage: null | { message: string, priority: 'always' | 'fallback' } } } = {
'Autocomplete': { filter: (o) => getModelCapabilities(o.providerName, o.modelName).supportsFIM, emptyMessage: { message: 'No models support FIM', priority: 'always' } },
'Chat': { filter: o => true, emptyMessage: null, },
'Ctrl+K': { filter: o => true, emptyMessage: null, },
'Apply': { filter: o => true, emptyMessage: null, },
@@ -174,7 +163,7 @@ const _validatedModelState = (state: Omit<VoidSettingsState, '_modelOptions'>):
for (const featureName of featureNames) {
const { filter } = modelFilterOfFeatureName[featureName]
const filterOpts = { chatMode: state.globalSettings.chatMode, overridesOfModel: state.overridesOfModel }
const filterOpts = { chatMode: state.globalSettings.chatMode }
const modelOptionsForThisFeature = newModelOptions.filter((o) => filter(o.selection, filterOpts))
const modelSelectionAtFeature = newModelSelectionOfFeature[featureName]
@@ -193,7 +182,6 @@ const _validatedModelState = (state: Omit<VoidSettingsState, '_modelOptions'>):
...state,
settingsOfProvider: newSettingsOfProvider,
modelSelectionOfFeature: newModelSelectionOfFeature,
overridesOfModel: state.overridesOfModel,
_modelOptions: newModelOptions,
} satisfies VoidSettingsState
@@ -210,7 +198,6 @@ const defaultState = () => {
modelSelectionOfFeature: { 'Chat': null, 'Ctrl+K': null, 'Autocomplete': null, 'Apply': null },
globalSettings: deepClone(defaultGlobalSettings),
optionsOfModelSelection: { 'Chat': {}, 'Ctrl+K': {}, 'Autocomplete': {}, 'Apply': {} },
overridesOfModel: deepClone(defaultOverridesOfModel),
_modelOptions: [], // computed later
}
return d
@@ -280,11 +267,9 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
// the stored data structure might be outdated, so we need to update it here
try {
readS = {
...defaultState(),
...readS,
// no idea why this was here, seems like a bug
// ...defaultSettingsOfProvider,
// ...readS.settingsOfProvider,
...defaultSettingsOfProvider,
...readS.settingsOfProvider,
}
for (const providerName of providerNames) {
@@ -304,11 +289,6 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
else m.type = 'custom'
}
}
// remove when enough people have had it run (default is now {})
if (providerName === 'openAICompatible' && !readS.settingsOfProvider[providerName].headersJSON) {
readS.settingsOfProvider[providerName].headersJSON = '{}'
}
}
}
@@ -334,8 +314,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
return defaultState()
const stateStr = await this._encryptionService.decrypt(encryptedState)
const state = JSON.parse(stateStr)
return state
return JSON.parse(stateStr)
}
@@ -360,14 +339,12 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
}
const newGlobalSettings = this.state.globalSettings
const newOverridesOfModel = this.state.overridesOfModel
const newState = {
modelSelectionOfFeature: newModelSelectionOfFeature,
optionsOfModelSelection: newOptionsOfModelSelection,
settingsOfProvider: newSettingsOfProvider,
globalSettings: newGlobalSettings,
overridesOfModel: newOverridesOfModel,
}
this.state = _validatedModelState(newState)
@@ -445,28 +422,6 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
this._onDidChangeState.fire()
}
setOverridesOfModel = async (providerName: ProviderName, modelName: string, overrides: Partial<ModelOverrides> | undefined) => {
const newState: VoidSettingsState = {
...this.state,
overridesOfModel: {
...this.state.overridesOfModel,
[providerName]: {
...this.state.overridesOfModel[providerName],
[modelName]: overrides === undefined ? undefined : {
...this.state.overridesOfModel[providerName][modelName],
...overrides
},
}
}
};
this.state = _validatedModelState(newState);
await this._storeState();
this._onDidChangeState.fire();
this._metricsService.capture('Update Model Overrides', { providerName, modelName, overrides });
}
@@ -4,7 +4,7 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { defaultModelsOfProvider, defaultProviderSettings, ModelOverrides } from './modelCapabilities.js';
import { defaultModelsOfProvider, defaultProviderSettings } from './modelCapabilities.js';
import { ToolApprovalType } from './toolsServiceTypes.js';
import { VoidSettingsState } from './voidSettingsService.js'
@@ -92,14 +92,14 @@ export const displayInfoOfProviderName = (providerName: ProviderName): DisplayIn
return { title: 'Groq', }
}
else if (providerName === 'xAI') {
return { title: 'Grok (xAI)', }
return { title: 'xAI', }
}
else if (providerName === 'mistral') {
return { title: 'Mistral', }
}
else if (providerName === 'googleVertex') {
return { title: 'Google Vertex AI', }
}
// else if (providerName === 'googleVertex') {
// return { title: 'Google Vertex AI', }
// }
else if (providerName === 'microsoftAzure') {
return { title: 'Microsoft Azure OpenAI', }
}
@@ -112,17 +112,17 @@ export const subTextMdOfProviderName = (providerName: ProviderName): string => {
if (providerName === 'anthropic') return 'Get your [API Key here](https://console.anthropic.com/settings/keys).'
if (providerName === 'openAI') return 'Get your [API Key here](https://platform.openai.com/api-keys).'
if (providerName === 'deepseek') return 'Get your [API Key here](https://platform.deepseek.com/api_keys).'
if (providerName === 'openRouter') return 'Get your [API Key here](https://openrouter.ai/settings/keys). Read about [rate limits here](https://openrouter.ai/docs/api-reference/limits).'
if (providerName === 'gemini') return 'Get your [API Key here](https://aistudio.google.com/apikey). Read about [rate limits here](https://ai.google.dev/gemini-api/docs/rate-limits#current-rate-limits).'
if (providerName === 'openRouter') return 'Get your [API Key here](https://openrouter.ai/settings/keys).'
if (providerName === 'gemini') return 'Get your [API Key here](https://aistudio.google.com/apikey).'
if (providerName === 'groq') return 'Get your [API Key here](https://console.groq.com/keys).'
if (providerName === 'xAI') return 'Get your [API Key here](https://console.x.ai).'
if (providerName === 'mistral') return 'Get your [API Key here](https://console.mistral.ai/api-keys).'
if (providerName === 'openAICompatible') return `Use any provider that's OpenAI-compatible (use this for llama.cpp and more).`
if (providerName === 'googleVertex') return 'You must authenticate before using Vertex with Void. Read more about endpoints [here](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/call-vertex-using-openai-library), and regions [here](https://cloud.google.com/vertex-ai/docs/general/locations#available-regions).'
if (providerName === 'openAICompatible') return `Use any OpenAI-compatible endpoint (LM Studio, LiteLM, etc).`
// if (providerName === 'googleVertex') return 'You must authenticate before using Vertex with Void. Read more about endpoints [here](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/call-vertex-using-openai-library), and regions [here](https://cloud.google.com/vertex-ai/docs/general/locations#available-regions).'
if (providerName === 'microsoftAzure') return 'Read more about endpoints [here](https://learn.microsoft.com/en-us/rest/api/aifoundry/model-inference/get-chat-completions/get-chat-completions?view=rest-aifoundry-model-inference-2024-05-01-preview&tabs=HTTP), and get your API key [here](https://learn.microsoft.com/en-us/azure/search/search-security-api-keys?tabs=rest-use%2Cportal-find%2Cportal-query#find-existing-keys).'
if (providerName === 'ollama') return 'Read more about custom [Endpoints here](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-can-i-expose-ollama-on-my-network).'
if (providerName === 'vLLM') return 'Read more about custom [Endpoints here](https://docs.vllm.ai/en/latest/getting_started/quickstart.html#openai-compatible-server).'
if (providerName === 'lmStudio') return 'Read more about custom [Endpoints here](https://lmstudio.ai/docs/app/api/endpoints/openai).'
if (providerName === 'ollama') return 'If you would like to change this endpoint, please read more about [Endpoints here](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-can-i-expose-ollama-on-my-network).'
if (providerName === 'vLLM') return 'If you would like to change this endpoint, please read more about [Endpoints here](https://docs.vllm.ai/en/latest/getting_started/quickstart.html#openai-compatible-server).'
if (providerName === 'lmStudio') return 'If you would like to change this endpoint, please more about [Endpoints here](https://lmstudio.ai/docs/app/api/endpoints/openai).'
if (providerName === 'liteLLM') return 'Read more about endpoints [here](https://docs.litellm.ai/docs/providers/openai_compatible).'
throw new Error(`subTextMdOfProviderName: Unknown provider name: "${providerName}"`)
@@ -149,9 +149,9 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
providerName === 'openAICompatible' ? 'sk-key...' :
providerName === 'xAI' ? 'xai-key...' :
providerName === 'mistral' ? 'api-key...' :
providerName === 'googleVertex' ? 'AIzaSy...' :
providerName === 'microsoftAzure' ? 'key-...' :
'',
// providerName === 'googleVertex' ? 'AIzaSy...' :
providerName === 'microsoftAzure' ? 'key-...' :
'',
isPasswordField: true,
}
@@ -162,10 +162,10 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
providerName === 'vLLM' ? 'Endpoint' :
providerName === 'lmStudio' ? 'Endpoint' :
providerName === 'openAICompatible' ? 'baseURL' : // (do not include /chat/completions)
providerName === 'googleVertex' ? 'baseURL' :
providerName === 'microsoftAzure' ? 'baseURL' :
providerName === 'liteLLM' ? 'baseURL' :
'(never)',
// providerName === 'googleVertex' ? 'baseURL' :
providerName === 'microsoftAzure' ? 'baseURL' :
providerName === 'liteLLM' ? 'baseURL' :
'(never)',
placeholder: providerName === 'ollama' ? defaultProviderSettings.ollama.endpoint
: providerName === 'vLLM' ? defaultProviderSettings.vLLM.endpoint
@@ -177,17 +177,14 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
}
}
else if (settingName === 'headersJSON') {
return { title: 'Custom Headers', placeholder: '{ "X-Request-Id": "..." }' }
}
else if (settingName === 'region') {
// vertex only
return {
title: 'Region',
placeholder: providerName === 'googleVertex' ? defaultProviderSettings.googleVertex.region
: ''
}
}
// else if (settingName === 'region') {
// // vertex only
// return {
// title: 'Region',
// placeholder: providerName === 'googleVertex' ? defaultProviderSettings.googleVertex.region
// : ''
// }
// }
else if (settingName === 'azureApiVersion') {
// azure only
return {
@@ -199,11 +196,11 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
else if (settingName === 'project') {
return {
title: providerName === 'microsoftAzure' ? 'Resource'
: providerName === 'googleVertex' ? 'Project'
: '',
// : providerName === 'googleVertex' ? 'Project'
: '',
placeholder: providerName === 'microsoftAzure' ? 'my-resource'
: providerName === 'googleVertex' ? 'my-project'
: ''
// : providerName === 'googleVertex' ? 'my-project'
: ''
}
@@ -231,10 +228,9 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
const defaultCustomSettings: Record<CustomSettingName, undefined> = {
apiKey: undefined,
endpoint: undefined,
region: undefined, // googleVertex
// region: undefined, // googleVertex
project: undefined,
azureApiVersion: undefined,
headersJSON: undefined,
}
@@ -328,12 +324,12 @@ export const defaultSettingsOfProvider: SettingsOfProvider = {
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.vLLM),
_didFillInProviderSettings: undefined,
},
googleVertex: { // aggregator (serves models from multiple providers)
...defaultCustomSettings,
...defaultProviderSettings.googleVertex,
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.googleVertex),
_didFillInProviderSettings: undefined,
},
// googleVertex: { // aggregator (serves models from multiple providers)
// ...defaultCustomSettings,
// ...defaultProviderSettings.googleVertex,
// ...modelInfoOfDefaultModelNames(defaultModelsOfProvider.googleVertex),
// _didFillInProviderSettings: undefined,
// },
microsoftAzure: { // aggregator (serves models from multiple providers)
...defaultCustomSettings,
...defaultProviderSettings.microsoftAzure,
@@ -466,28 +462,13 @@ export const globalSettingNames = Object.keys(defaultGlobalSettings) as GlobalSe
export type ModelSelectionOptions = {
reasoningEnabled?: boolean;
reasoningBudget?: number;
reasoningEffort?: string;
}
export type OptionsOfModelSelection = {
[featureName in FeatureName]: Partial<{
[providerName in ProviderName]: {
[modelName: string]: ModelSelectionOptions | undefined
[modelName: string]:
ModelSelectionOptions | undefined
}
}>
}
export type OverridesOfModel = {
[providerName in ProviderName]: {
[modelName: string]: Partial<ModelOverrides> | undefined
}
}
const overridesOfModel = {} as OverridesOfModel
for (const providerName of providerNames) { overridesOfModel[providerName] = {} }
export const defaultOverridesOfModel = overridesOfModel
@@ -23,9 +23,6 @@ export const extractReasoningWrapper = (
let fullTextSoFar = ''
let fullReasoningSoFar = ''
if (!thinkTags[0] || !thinkTags[1]) throw new Error(`thinkTags must not be empty if provided. Got ${JSON.stringify(thinkTags)}.`)
let onText_ = onText
onText = (params) => {
onText_(params)
@@ -7,27 +7,17 @@
/* eslint-disable */
import Anthropic from '@anthropic-ai/sdk';
import { Ollama } from 'ollama';
import OpenAI, { ClientOptions, AzureOpenAI } from 'openai';
import OpenAI, { ClientOptions } from 'openai';
import { MistralCore } from '@mistralai/mistralai/core.js';
import { fimComplete } from '@mistralai/mistralai/funcs/fimComplete.js';
import { Tool as GeminiTool, FunctionDeclaration, GoogleGenAI, ThinkingConfig, Schema, Type } from '@google/genai';
import { GoogleAuth } from 'google-auth-library'
// import { GoogleAuth } from 'google-auth-library'
/* eslint-enable */
import { AnthropicLLMChatMessage, GeminiLLMChatMessage, LLMChatMessage, LLMFIMMessage, ModelListParams, OllamaModelResponse, OnError, OnFinalMessage, OnText, RawToolCallObj, RawToolParamsObj } from '../../common/sendLLMMessageTypes.js';
import { ChatMode, displayInfoOfProviderName, ModelSelectionOptions, OverridesOfModel, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js';
import { getSendableReasoningInfo, getModelCapabilities, getProviderCapabilities, defaultProviderSettings, getReservedOutputTokenSpace } from '../../common/modelCapabilities.js';
import { AnthropicLLMChatMessage, LLMChatMessage, LLMFIMMessage, ModelListParams, OllamaModelResponse, OnError, OnFinalMessage, OnText, RawToolCallObj, RawToolParamsObj } from '../../common/sendLLMMessageTypes.js';
import { ChatMode, displayInfoOfProviderName, ModelSelectionOptions, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js';
import { getSendableReasoningInfo, getModelCapabilities, getProviderCapabilities, defaultProviderSettings, getMaxOutputTokens } from '../../common/modelCapabilities.js';
import { extractReasoningWrapper, extractXMLToolsWrapper } from './extractGrammar.js';
import { availableTools, InternalToolInfo, isAToolName, ToolParamName, voidTools } from '../../common/prompt/prompts.js';
import { generateUuid } from '../../../../../base/common/uuid.js';
const getGoogleApiKey = async () => {
// modulelevel singleton
const auth = new GoogleAuth({ scopes: `https://www.googleapis.com/auth/cloud-platform` });
const key = await auth.getAccessToken()
if (!key) throw new Error(`Google API failed to generate a key.`)
return key
}
@@ -39,16 +29,11 @@ type InternalCommonMessageParams = {
providerName: ProviderName;
settingsOfProvider: SettingsOfProvider;
modelSelectionOptions: ModelSelectionOptions | undefined;
overridesOfModel: OverridesOfModel | undefined;
modelName: string;
_setAborter: (aborter: () => void) => void;
}
type SendChatParams_Internal = InternalCommonMessageParams & {
messages: LLMChatMessage[];
separateSystemMessage: string | undefined;
chatMode: ChatMode | null;
}
type SendChatParams_Internal = InternalCommonMessageParams & { messages: LLMChatMessage[]; separateSystemMessage: string | undefined; chatMode: ChatMode | null; }
type SendFIMParams_Internal = InternalCommonMessageParams & { messages: LLMFIMMessage; separateSystemMessage: string | undefined; }
export type ListParams_Internal<ModelResponse> = ModelListParams<ModelResponse>
@@ -57,17 +42,15 @@ const invalidApiKeyMessage = (providerName: ProviderName) => `Invalid ${displayI
// ------------ OPENAI-COMPATIBLE (HELPERS) ------------
// const getGoogleApiKey = async () => {
// // modulelevel singleton
// const auth = new GoogleAuth({ scopes: `https://www.googleapis.com/auth/cloud-platform` });
// const key = await auth.getAccessToken()
// if (!key) throw new Error(`Google API failed to generate a key.`)
// return key
// }
const parseHeadersJSON = (s: string | undefined): Record<string, string | null | undefined> | undefined => {
if (!s) return undefined
try {
return JSON.parse(s)
} catch (e) {
throw new Error(`Error parsing OpenAI-Compatible headers: ${s} is not a valid JSON.`)
}
}
const newOpenAICompatibleSDK = async ({ settingsOfProvider, providerName, includeInPayload }: { settingsOfProvider: SettingsOfProvider, providerName: ProviderName, includeInPayload?: { [s: string]: any } }) => {
const commonPayloadOpts: ClientOptions = {
dangerouslyAllowBrowser: true,
@@ -105,18 +88,21 @@ const newOpenAICompatibleSDK = async ({ settingsOfProvider, providerName, includ
...commonPayloadOpts,
})
}
else if (providerName === 'googleVertex') {
// https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/call-vertex-using-openai-library
else if (providerName === 'gemini') {
const thisConfig = settingsOfProvider[providerName]
const baseURL = `https://${thisConfig.region}-aiplatform.googleapis.com/v1/projects/${thisConfig.project}/locations/${thisConfig.region}/endpoints/${'openapi'}`
const apiKey = await getGoogleApiKey()
return new OpenAI({ baseURL: baseURL, apiKey: apiKey, ...commonPayloadOpts })
return new OpenAI({ baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai', apiKey: thisConfig.apiKey, ...commonPayloadOpts })
}
// else if (providerName === 'googleVertex') {
// // https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/call-vertex-using-openai-library
// const thisConfig = settingsOfProvider[providerName]
// const baseURL = `https://${thisConfig.region}-aiplatform.googleapis.com/v1/projects/${thisConfig.project}/locations/${thisConfig.region}/endpoints/${'openapi'}`
// return new OpenAI({ baseURL: baseURL, apiKey: apiKey, ...commonPayloadOpts })
// }
else if (providerName === 'microsoftAzure') {
// https://learn.microsoft.com/en-us/rest/api/aifoundry/model-inference/get-chat-completions/get-chat-completions?view=rest-aifoundry-model-inference-2024-05-01-preview&tabs=HTTP
// https://github.com/openai/openai-node?tab=readme-ov-file#microsoft-azure-openai
const thisConfig = settingsOfProvider[providerName]
return new AzureOpenAI({ apiKey: thisConfig.apiKey, apiVersion: thisConfig.azureApiVersion, project: thisConfig.project, ...commonPayloadOpts })
const baseURL = `https://${thisConfig.project}.services.ai.azure.com/api/models/chat/completions??api-version=${thisConfig.azureApiVersion}`
return new OpenAI({ baseURL: baseURL, apiKey: thisConfig.apiKey, ...commonPayloadOpts })
}
else if (providerName === 'deepseek') {
@@ -125,8 +111,7 @@ const newOpenAICompatibleSDK = async ({ settingsOfProvider, providerName, includ
}
else if (providerName === 'openAICompatible') {
const thisConfig = settingsOfProvider[providerName]
const headers = parseHeadersJSON(thisConfig.headersJSON)
return new OpenAI({ baseURL: thisConfig.endpoint, apiKey: thisConfig.apiKey, defaultHeaders: headers, ...commonPayloadOpts })
return new OpenAI({ baseURL: thisConfig.endpoint, apiKey: thisConfig.apiKey, ...commonPayloadOpts })
}
else if (providerName === 'groq') {
const thisConfig = settingsOfProvider[providerName]
@@ -145,14 +130,8 @@ const newOpenAICompatibleSDK = async ({ settingsOfProvider, providerName, includ
}
const _sendOpenAICompatibleFIM = async ({ messages: { prefix, suffix, stopTokens }, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, overridesOfModel }: SendFIMParams_Internal) => {
const {
modelName,
supportsFIM,
additionalOpenAIPayload,
} = getModelCapabilities(providerName, modelName_, overridesOfModel)
const _sendOpenAICompatibleFIM = async ({ messages: { prefix, suffix, stopTokens }, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, }: SendFIMParams_Internal) => {
const { modelName, supportsFIM } = getModelCapabilities(providerName, modelName_)
if (!supportsFIM) {
if (modelName === modelName_)
onError({ message: `Model ${modelName} does not support FIM.`, fullError: null })
@@ -161,7 +140,7 @@ const _sendOpenAICompatibleFIM = async ({ messages: { prefix, suffix, stopTokens
return
}
const openai = await newOpenAICompatibleSDK({ providerName, settingsOfProvider, includeInPayload: additionalOpenAIPayload })
const openai = await newOpenAICompatibleSDK({ providerName, settingsOfProvider })
openai.completions
.create({
model: modelName,
@@ -214,7 +193,7 @@ const openAITools = (chatMode: ChatMode) => {
return openAITools
}
const rawToolCallObjOf = (name: string, toolParamsStr: string, id: string): RawToolCallObj | null => {
const openAIToolToRawToolCallObj = (name: string, toolParamsStr: string, id: string): RawToolCallObj | null => {
if (!isAToolName(name)) return null
const rawParams: RawToolParamsObj = {}
let input: unknown
@@ -236,24 +215,19 @@ const rawToolCallObjOf = (name: string, toolParamsStr: string, id: string): RawT
// ------------ OPENAI-COMPATIBLE ------------
const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, providerName, chatMode, separateSystemMessage, overridesOfModel }: SendChatParams_Internal) => {
const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, providerName, chatMode, separateSystemMessage }: SendChatParams_Internal) => {
const {
modelName,
specialToolFormat,
reasoningCapabilities,
additionalOpenAIPayload,
} = getModelCapabilities(providerName, modelName_, overridesOfModel)
} = getModelCapabilities(providerName, modelName_)
const { providerReasoningIOSettings } = getProviderCapabilities(providerName)
// reasoning
const { canIOReasoning, openSourceThinkTags } = reasoningCapabilities || {}
const reasoningInfo = getSendableReasoningInfo('Chat', providerName, modelName_, modelSelectionOptions, overridesOfModel) // user's modelName_ here
const includeInPayload = {
...providerReasoningIOSettings?.input?.includeInPayload?.(reasoningInfo),
...additionalOpenAIPayload
}
const { canIOReasoning, openSourceThinkTags, } = reasoningCapabilities || {}
const reasoningInfo = getSendableReasoningInfo('Chat', providerName, modelName_, modelSelectionOptions) // user's modelName_ here
const includeInPayload = providerReasoningIOSettings?.input?.includeInPayload?.(reasoningInfo) || {}
// tools
const potentialTools = chatMode !== null ? openAITools(chatMode) : null
@@ -268,7 +242,6 @@ const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onE
messages: messages as any,
stream: true,
...nativeToolsObj,
...additionalOpenAIPayload
// max_completion_tokens: maxTokens,
}
@@ -324,7 +297,6 @@ const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onE
fullReasoningSoFar += newReasoning
}
// call onText
onText({
fullText: fullTextSoFar,
fullReasoning: fullReasoningSoFar,
@@ -337,7 +309,7 @@ const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onE
onError({ message: 'Void: Response from model was empty.', fullError: null })
}
else {
const toolCall = rawToolCallObjOf(toolName, toolParamsStr, toolId)
const toolCall = openAIToolToRawToolCallObj(toolName, toolParamsStr, toolId)
const toolCallObj = toolCall ? { toolCall } : {}
onFinalMessage({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, anthropicReasoning: null, ...toolCallObj });
}
@@ -427,21 +399,21 @@ const anthropicToolToRawToolCallObj = (toolBlock: Anthropic.Messages.ToolUseBloc
}
// ------------ ANTHROPIC ------------
const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, overridesOfModel, modelName: modelName_, _setAborter, separateSystemMessage, chatMode }: SendChatParams_Internal) => {
const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, separateSystemMessage, chatMode }: SendChatParams_Internal) => {
const {
modelName,
specialToolFormat,
} = getModelCapabilities(providerName, modelName_, overridesOfModel)
} = getModelCapabilities(providerName, modelName_)
const thisConfig = settingsOfProvider.anthropic
const { providerReasoningIOSettings } = getProviderCapabilities(providerName)
// reasoning
const reasoningInfo = getSendableReasoningInfo('Chat', providerName, modelName_, modelSelectionOptions, overridesOfModel) // user's modelName_ here
const reasoningInfo = getSendableReasoningInfo('Chat', providerName, modelName_, modelSelectionOptions) // user's modelName_ here
const includeInPayload = providerReasoningIOSettings?.input?.includeInPayload?.(reasoningInfo) || {}
// anthropic-specific - max tokens
const maxTokens = getReservedOutputTokenSpace(providerName, modelName_, { isReasoningEnabled: !!reasoningInfo?.isReasoningEnabled, overridesOfModel })
const maxTokens = getMaxOutputTokens(providerName, modelName_, { isReasoningEnabled: !!reasoningInfo?.isReasoningEnabled })
// tools
const potentialTools = chatMode !== null ? anthropicTools(chatMode) : null
@@ -466,7 +438,7 @@ const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessag
})
// manually parse out tool results if XML
// manually parse out tool results
if (!specialToolFormat) {
const { newOnText, newOnFinalMessage } = extractXMLToolsWrapper(onText, onFinalMessage, chatMode)
onText = newOnText
@@ -551,8 +523,8 @@ const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessag
// ------------ MISTRAL ------------
// https://docs.mistral.ai/api/#tag/fim
const sendMistralFIM = ({ messages, onFinalMessage, onError, settingsOfProvider, overridesOfModel, modelName: modelName_, _setAborter, providerName }: SendFIMParams_Internal) => {
const { modelName, supportsFIM } = getModelCapabilities(providerName, modelName_, overridesOfModel)
const sendMistralFIM = ({ messages, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName }: SendFIMParams_Internal) => {
const { modelName, supportsFIM } = getModelCapabilities(providerName, modelName_)
if (!supportsFIM) {
if (modelName === modelName_)
onError({ message: `Model ${modelName} does not support FIM.`, fullError: null })
@@ -572,7 +544,6 @@ const sendMistralFIM = ({ messages, onFinalMessage, onError, settingsOfProvider,
stop: messages.stopTokens,
})
.then(async response => {
// unfortunately, _setAborter() does not exist
let content = response?.ok ? response.value.choices?.[0]?.message?.content ?? '' : '';
const fullText = typeof content === 'string' ? content
@@ -649,169 +620,6 @@ const sendOllamaFIM = ({ messages, onFinalMessage, onError, settingsOfProvider,
})
}
// ---------------- GEMINI NATIVE IMPLEMENTATION ----------------
const toGeminiFunctionDecl = (toolInfo: InternalToolInfo) => {
const { name, description, params } = toolInfo
return {
name,
description,
parameters: {
type: Type.OBJECT,
properties: Object.entries(params).reduce((acc, [key, value]) => {
acc[key] = {
type: Type.STRING,
description: value.description
};
return acc;
}, {} as Record<string, Schema>)
}
} satisfies FunctionDeclaration
}
const geminiTools = (chatMode: ChatMode): GeminiTool[] | null => {
const allowedTools = availableTools(chatMode)
if (!allowedTools || Object.keys(allowedTools).length === 0) return null
const functionDecls: FunctionDeclaration[] = []
for (const t in allowedTools ?? {}) {
functionDecls.push(toGeminiFunctionDecl(allowedTools[t]))
}
const tools: GeminiTool = { functionDeclarations: functionDecls, }
return [tools]
}
// Implementation for Gemini using Google's native API
const sendGeminiChat = async ({
messages,
separateSystemMessage,
onText,
onFinalMessage,
onError,
settingsOfProvider,
overridesOfModel,
modelName: modelName_,
_setAborter,
providerName,
modelSelectionOptions,
chatMode,
}: SendChatParams_Internal) => {
if (providerName !== 'gemini') throw new Error(`Sending Gemini chat, but provider was ${providerName}`)
const thisConfig = settingsOfProvider[providerName]
const {
modelName,
specialToolFormat,
// reasoningCapabilities,
} = getModelCapabilities(providerName, modelName_, overridesOfModel)
// const { providerReasoningIOSettings } = getProviderCapabilities(providerName)
// reasoning
// const { canIOReasoning, openSourceThinkTags, } = reasoningCapabilities || {}
const reasoningInfo = getSendableReasoningInfo('Chat', providerName, modelName_, modelSelectionOptions, overridesOfModel) // user's modelName_ here
// const includeInPayload = providerReasoningIOSettings?.input?.includeInPayload?.(reasoningInfo) || {}
const thinkingConfig: ThinkingConfig | undefined = !reasoningInfo?.isReasoningEnabled ? undefined
: reasoningInfo.type === 'budget_slider_value' ?
{ thinkingBudget: reasoningInfo.reasoningBudget }
: undefined
// tools
const potentialTools = chatMode !== null ? geminiTools(chatMode) : undefined
const toolConfig = potentialTools && specialToolFormat === 'gemini-style' ?
potentialTools
: undefined
// instance
const genAI = new GoogleGenAI({ apiKey: thisConfig.apiKey });
// manually parse out tool results if XML
if (!specialToolFormat) {
const { newOnText, newOnFinalMessage } = extractXMLToolsWrapper(onText, onFinalMessage, chatMode)
onText = newOnText
onFinalMessage = newOnFinalMessage
}
// when receive text
let fullReasoningSoFar = ''
let fullTextSoFar = ''
let toolName = ''
let toolParamsStr = ''
let toolId = ''
genAI.models.generateContentStream({
model: modelName,
config: {
systemInstruction: separateSystemMessage,
thinkingConfig: thinkingConfig,
tools: toolConfig,
},
contents: messages as GeminiLLMChatMessage[],
})
.then(async (stream) => {
_setAborter(() => { stream.return(fullTextSoFar); });
// Process the stream
for await (const chunk of stream) {
// message
const newText = chunk.text ?? ''
fullTextSoFar += newText
// tool call
const functionCalls = chunk.functionCalls
if (functionCalls && functionCalls.length > 0) {
const functionCall = functionCalls[0] // Get the first function call
toolName = functionCall.name ?? ''
toolParamsStr = JSON.stringify(functionCall.args ?? {})
toolId = functionCall.id ?? ''
}
// (do not handle reasoning yet)
// call onText
onText({
fullText: fullTextSoFar,
fullReasoning: fullReasoningSoFar,
toolCall: isAToolName(toolName) ? { name: toolName, rawParams: {}, isDone: false, doneParams: [], id: toolId } : undefined,
})
}
// on final
if (!fullTextSoFar && !fullReasoningSoFar && !toolName) {
onError({ message: 'Void: Response from model was empty.', fullError: null })
} else {
if (!toolId) toolId = generateUuid() // ids are empty, but other providers might expect an id
const toolCall = rawToolCallObjOf(toolName, toolParamsStr, toolId)
const toolCallObj = toolCall ? { toolCall } : {}
onFinalMessage({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, anthropicReasoning: null, ...toolCallObj });
}
})
.catch(error => {
const message = error?.message
if (typeof message === 'string') {
if (error.message?.includes('API key')) {
onError({ message: invalidApiKeyMessage(providerName), fullError: error });
}
else if (error?.message?.includes('429')) {
onError({ message: 'Rate limit reached. ' + error, fullError: error });
}
else
onError({ message: error + '', fullError: error });
}
else {
onError({ message: error + '', fullError: error });
}
})
};
type CallFnOfProvider = {
@@ -839,7 +647,7 @@ export const sendLLMMessageToProviderImplementation = {
list: null,
},
gemini: {
sendChat: (params) => sendGeminiChat(params),
sendChat: (params) => _sendOpenAICompatibleChat(params),
sendFIM: null,
list: null,
},
@@ -880,21 +688,20 @@ export const sendLLMMessageToProviderImplementation = {
},
lmStudio: {
// lmStudio has no suffix parameter in /completions, so sendFIM might not work
sendChat: (params) => _sendOpenAICompatibleChat(params),
sendFIM: (params) => _sendOpenAICompatibleFIM(params),
sendFIM: null, // lmStudio has no suffix parameter in /completions
list: (params) => _openaiCompatibleList(params),
},
liteLLM: {
sendChat: (params) => _sendOpenAICompatibleChat(params),
sendFIM: (params) => _sendOpenAICompatibleFIM(params),
list: null,
},
googleVertex: {
sendChat: (params) => _sendOpenAICompatibleChat(params),
sendFIM: null,
list: null,
},
// googleVertex: {
// sendChat: (params) => _sendOpenAICompatibleChat(params),
// sendFIM: null,
// list: null,
// },
microsoftAzure: {
sendChat: (params) => _sendOpenAICompatibleChat(params),
sendFIM: null,
@@ -20,7 +20,6 @@ export const sendLLMMessage = async ({
settingsOfProvider,
modelSelection,
modelSelectionOptions,
overridesOfModel,
chatMode,
separateSystemMessage,
}: SendLLMMessageParams,
@@ -34,6 +33,13 @@ export const sendLLMMessage = async ({
// only captures number of messages and message "shape", no actual code, instructions, prompts, etc
const captureLLMEvent = (eventId: string, extras?: object) => {
let totalTokens = 0
if (messagesType === 'chatMessages') {
for (const m of messages_) totalTokens += m.content.length
}
else {
totalTokens = messages_.prefix.length + messages_.suffix.length
}
metricsService.capture(eventId, {
providerName,
@@ -42,10 +48,13 @@ export const sendLLMMessage = async ({
numModelsAtEndpoint: settingsOfProvider[providerName]?.models?.length,
...messagesType === 'chatMessages' ? {
numMessages: messages_?.length,
messagesShape: messages_?.map(msg => ({ role: msg.role, length: msg.content.length })),
} : messagesType === 'FIMMessage' ? {
prefixLength: messages_.prefix.length,
suffixLength: messages_.suffix.length,
} : {},
totalTokens,
...loggingExtras,
...extras,
})
@@ -65,9 +74,9 @@ export const sendLLMMessage = async ({
}
const onFinalMessage: OnFinalMessage = (params) => {
const { fullText, fullReasoning, toolCall } = params
const { fullText, fullReasoning } = params
if (_didAbort) return
captureLLMEvent(`${loggingName} - Received Full Message`, { messageLength: fullText.length, reasoningLength: fullReasoning?.length, duration: new Date().getMilliseconds() - submit_time.getMilliseconds(), toolCallName: toolCall?.name })
captureLLMEvent(`${loggingName} - Received Full Message`, { messageLength: fullText.length, reasoningLength: fullReasoning?.length, duration: new Date().getMilliseconds() - submit_time.getMilliseconds() })
onFinalMessage_(params)
}
@@ -94,7 +103,7 @@ export const sendLLMMessage = async ({
if (messagesType === 'chatMessages')
captureLLMEvent(`${loggingName} - Sending Message`, {})
captureLLMEvent(`${loggingName} - Sending Message`, { userMessageLength: messages_?.[messages_.length - 1]?.content.length })
else if (messagesType === 'FIMMessage')
captureLLMEvent(`${loggingName} - Sending FIM`, { prefixLen: messages_?.prefix?.length, suffixLen: messages_?.suffix?.length })
@@ -107,15 +116,15 @@ export const sendLLMMessage = async ({
}
const { sendFIM, sendChat } = implementation
if (messagesType === 'chatMessages') {
await sendChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, overridesOfModel, modelName, _setAborter, providerName, separateSystemMessage, chatMode })
await sendChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName, _setAborter, providerName, separateSystemMessage, chatMode })
return
}
if (messagesType === 'FIMMessage') {
if (sendFIM) {
await sendFIM({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, overridesOfModel, modelName, _setAborter, providerName, separateSystemMessage })
await sendFIM({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName, _setAborter, providerName, separateSystemMessage })
return
}
onError({ message: `Error running Autocomplete with ${providerName} - ${modelName}.`, fullError: null })
onError({ message: `Error: This provider does not support Autocomplete yet.`, fullError: null })
return
}
onError({ message: `Error: Message type "${messagesType}" not recognized.`, fullError: null })
@@ -94,7 +94,7 @@ export class VoidMainUpdateService extends Disposable implements IVoidUpdateServ
const data = await response.json();
const version = data.tag_name;
const myVersion = this._productService.version
const myVersion = `${this._productService.version}.${this._productService.release}`
const latestVersion = version
const isUpToDate = myVersion === latestVersion // only makes sense if response.ok