Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 524413db5a | |||
| d077eb9ca4 | |||
| 54da9fa0f9 | |||
| 19dda35ff7 | |||
| d15e559a73 | |||
| f949c05b14 | |||
| 121983ae6d |
@@ -1 +0,0 @@
|
||||
blank_issues_enabled: false
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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())"
|
||||
|
||||
@@ -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');
|
||||
|
||||
Generated
+134
-161
@@ -13,7 +13,7 @@
|
||||
"@anthropic-ai/sdk": "^0.40.0",
|
||||
"@c4312/eventsource-umd": "^3.0.5",
|
||||
"@floating-ui/react": "^0.27.8",
|
||||
"@google/generative-ai": "^0.24.1",
|
||||
"@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",
|
||||
@@ -33,6 +33,7 @@
|
||||
"@vscode/windows-mutex": "^0.5.0",
|
||||
"@vscode/windows-process-tree": "^0.6.0",
|
||||
"@vscode/windows-registry": "^1.1.0",
|
||||
<<<<<<< HEAD
|
||||
"@xterm/addon-clipboard": "^0.2.0-beta.82",
|
||||
"@xterm/addon-image": "^0.9.0-beta.99",
|
||||
"@xterm/addon-ligatures": "^0.10.0-beta.99",
|
||||
@@ -43,11 +44,22 @@
|
||||
"@xterm/addon-webgl": "^0.19.0-beta.99",
|
||||
"@xterm/headless": "^5.6.0-beta.99",
|
||||
"@xterm/xterm": "^5.6.0-beta.99",
|
||||
=======
|
||||
"@xterm/addon-clipboard": "^0.2.0-beta.81",
|
||||
"@xterm/addon-image": "^0.9.0-beta.98",
|
||||
"@xterm/addon-ligatures": "^0.10.0-beta.98",
|
||||
"@xterm/addon-progress": "^0.2.0-beta.4",
|
||||
"@xterm/addon-search": "^0.16.0-beta.98",
|
||||
"@xterm/addon-serialize": "^0.14.0-beta.98",
|
||||
"@xterm/addon-unicode11": "^0.9.0-beta.98",
|
||||
"@xterm/addon-webgl": "^0.19.0-beta.98",
|
||||
"@xterm/headless": "^5.6.0-beta.98",
|
||||
"@xterm/xterm": "^5.6.0-beta.98",
|
||||
>>>>>>> model-selection
|
||||
"ajv": "^8.17.1",
|
||||
"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",
|
||||
@@ -1714,6 +1726,33 @@
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
"json-schema-traverse": "^0.4.1",
|
||||
"uri-js": "^4.2.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@eslint/js": {
|
||||
"version": "9.25.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.25.0.tgz",
|
||||
"integrity": "sha512-iWhsUS8Wgxz9AXNfvfOPFSW4VfMXdVhp1hjkZVhXCrpgh/aLcc45rX6MPu+tIVUWDw0HfNwth7O28M1xDxNf9w==",
|
||||
>>>>>>> model-selection
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1818,9 +1857,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@google/generative-ai": {
|
||||
"version": "0.24.1",
|
||||
"resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.24.1.tgz",
|
||||
"integrity": "sha512-MqO+MLfM6kjxcKoy0p1wRzG3b4ZZXtPI+z2IE26UogS2Cm/XHO+7gGRBh6gcJsOiIVoH93UwKvW4HdgiOZCy9Q==",
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.24.0.tgz",
|
||||
"integrity": "sha512-fnEITCGEB7NdX0BhoYZ/cq/7WPZ1QS5IzJJfC3Tg/OwkvBetMiVJciyaan297OvE4B9Jg1xvo0zIazX/9sGu1Q==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
@@ -5220,6 +5259,28 @@
|
||||
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
|
||||
"integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
|
||||
"dev": true,
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ajv": "^8.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ajv": "^8.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"ajv": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/ajv-keywords": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
|
||||
"integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
>>>>>>> model-selection
|
||||
"dependencies": {
|
||||
"ajv": "^8.0.0"
|
||||
},
|
||||
@@ -6024,15 +6085,6 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/bignumber.js": {
|
||||
"version": "9.3.0",
|
||||
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.0.tgz",
|
||||
"integrity": "sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
|
||||
@@ -6263,12 +6315,6 @@
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-equal-constant-time": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/buffer-from": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
@@ -8116,15 +8162,6 @@
|
||||
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/ecdsa-sig-formatter": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
||||
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/editorconfig": {
|
||||
"version": "0.15.2",
|
||||
"resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.2.tgz",
|
||||
@@ -8841,6 +8878,23 @@
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
"json-schema-traverse": "^0.4.1",
|
||||
"uri-js": "^4.2.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint/node_modules/glob-parent": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
||||
@@ -9365,7 +9419,8 @@
|
||||
"node_modules/extend": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
||||
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
|
||||
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/extend-shallow": {
|
||||
"version": "3.0.2",
|
||||
@@ -9480,7 +9535,12 @@
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
<<<<<<< HEAD
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
|
||||
=======
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"license": "MIT"
|
||||
>>>>>>> model-selection
|
||||
},
|
||||
"node_modules/fast-fifo": {
|
||||
"version": "1.3.2",
|
||||
@@ -9607,6 +9667,43 @@
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
"json-schema-traverse": "^0.4.1",
|
||||
"uri-js": "^4.2.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/file-loader/node_modules/ajv-keywords": {
|
||||
"version": "3.5.2",
|
||||
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
|
||||
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"ajv": "^6.9.1"
|
||||
}
|
||||
},
|
||||
"node_modules/file-loader/node_modules/json-schema-traverse": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
||||
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/file-loader/node_modules/schema-utils": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
|
||||
"integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==",
|
||||
>>>>>>> model-selection
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -10332,68 +10429,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/gaxios": {
|
||||
"version": "6.7.1",
|
||||
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz",
|
||||
"integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"extend": "^3.0.2",
|
||||
"https-proxy-agent": "^7.0.1",
|
||||
"is-stream": "^2.0.0",
|
||||
"node-fetch": "^2.6.9",
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/gaxios/node_modules/is-stream": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
|
||||
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/gaxios/node_modules/node-fetch": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "4.x || >=6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"encoding": "^0.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"encoding": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/gcp-metadata": {
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz",
|
||||
"integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"gaxios": "^6.1.1",
|
||||
"google-logging-utils": "^0.0.2",
|
||||
"json-bigint": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/gensync": {
|
||||
"version": "1.0.0-beta.2",
|
||||
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
||||
@@ -11030,32 +11065,6 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/google-auth-library": {
|
||||
"version": "9.15.1",
|
||||
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz",
|
||||
"integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.0",
|
||||
"ecdsa-sig-formatter": "^1.0.11",
|
||||
"gaxios": "^6.1.1",
|
||||
"gcp-metadata": "^6.1.0",
|
||||
"gtoken": "^7.0.0",
|
||||
"jws": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/google-logging-utils": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz",
|
||||
"integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
@@ -11128,19 +11137,6 @@
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"node_modules/gtoken": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz",
|
||||
"integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"gaxios": "^6.0.0",
|
||||
"jws": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/gulp": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/gulp/-/gulp-4.0.2.tgz",
|
||||
@@ -14100,15 +14096,6 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/json-bigint": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
|
||||
"integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bignumber.js": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/json-buffer": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
|
||||
@@ -14229,27 +14216,6 @@
|
||||
"integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/jwa": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz",
|
||||
"integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-equal-constant-time": "1.0.1",
|
||||
"ecdsa-sig-formatter": "1.0.11",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/jws": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
|
||||
"integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jwa": "^2.0.0",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/kerberos": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/kerberos/-/kerberos-2.1.1.tgz",
|
||||
@@ -19110,6 +19076,10 @@
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
"license": "MIT",
|
||||
>>>>>>> model-selection
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -19584,6 +19554,7 @@
|
||||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
<<<<<<< HEAD
|
||||
"node_modules/schema-utils/node_modules/ajv-keywords": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
|
||||
@@ -19596,6 +19567,8 @@
|
||||
"ajv": "^8.8.2"
|
||||
}
|
||||
},
|
||||
=======
|
||||
>>>>>>> model-selection
|
||||
"node_modules/scope-tailwind": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/scope-tailwind/-/scope-tailwind-1.0.9.tgz",
|
||||
|
||||
+11
-12
@@ -75,7 +75,7 @@
|
||||
"@anthropic-ai/sdk": "^0.40.0",
|
||||
"@c4312/eventsource-umd": "^3.0.5",
|
||||
"@floating-ui/react": "^0.27.8",
|
||||
"@google/generative-ai": "^0.24.1",
|
||||
"@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",
|
||||
@@ -95,21 +95,20 @@
|
||||
"@vscode/windows-mutex": "^0.5.0",
|
||||
"@vscode/windows-process-tree": "^0.6.0",
|
||||
"@vscode/windows-registry": "^1.1.0",
|
||||
"@xterm/addon-clipboard": "^0.2.0-beta.82",
|
||||
"@xterm/addon-image": "^0.9.0-beta.99",
|
||||
"@xterm/addon-ligatures": "^0.10.0-beta.99",
|
||||
"@xterm/addon-progress": "^0.2.0-beta.5",
|
||||
"@xterm/addon-search": "^0.16.0-beta.99",
|
||||
"@xterm/addon-serialize": "^0.14.0-beta.99",
|
||||
"@xterm/addon-unicode11": "^0.9.0-beta.99",
|
||||
"@xterm/addon-webgl": "^0.19.0-beta.99",
|
||||
"@xterm/headless": "^5.6.0-beta.99",
|
||||
"@xterm/xterm": "^5.6.0-beta.99",
|
||||
"@xterm/addon-clipboard": "^0.2.0-beta.81",
|
||||
"@xterm/addon-image": "^0.9.0-beta.98",
|
||||
"@xterm/addon-ligatures": "^0.10.0-beta.98",
|
||||
"@xterm/addon-progress": "^0.2.0-beta.4",
|
||||
"@xterm/addon-search": "^0.16.0-beta.98",
|
||||
"@xterm/addon-serialize": "^0.14.0-beta.98",
|
||||
"@xterm/addon-unicode11": "^0.9.0-beta.98",
|
||||
"@xterm/addon-webgl": "^0.19.0-beta.98",
|
||||
"@xterm/headless": "^5.6.0-beta.98",
|
||||
"@xterm/xterm": "^5.6.0-beta.98",
|
||||
"ajv": "^8.17.1",
|
||||
"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
-4
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"nameShort": "Void",
|
||||
"nameLong": "Void",
|
||||
"voidVersion": "1.3.2",
|
||||
"voidVersion": "1.2.8",
|
||||
"applicationName": "void",
|
||||
"dataFolderName": ".void-editor",
|
||||
"win32MutexName": "voideditor",
|
||||
@@ -38,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 }) => {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -549,11 +485,16 @@ 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') {
|
||||
@@ -564,17 +505,6 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
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)
|
||||
}
|
||||
@@ -666,6 +596,9 @@ 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 {
|
||||
@@ -698,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
|
||||
@@ -713,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) {
|
||||
@@ -729,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,
|
||||
@@ -738,27 +662,26 @@ 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
|
||||
|
||||
let resMessageIsDonePromise: (res: { type: 'llmDone', toolCall?: RawToolCallObj } | { type: 'llmError', error?: { message: string; fullError: Error | null; } } | { type: 'llmAborted' }) => void // resolves when user approves this tool use (or if tool doesn't require approval)
|
||||
const messageIsDonePromise = new Promise<{ type: 'llmDone', toolCall?: RawToolCallObj } | { type: 'llmError', error?: { message: string; fullError: Error | null; } } | { type: 'llmAborted' }>((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 }) => {
|
||||
@@ -766,15 +689,36 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||
},
|
||||
onFinalMessage: async ({ fullText, fullReasoning, toolCall, anthropicReasoning, }) => {
|
||||
this._addMessageToThread(threadId, { role: 'assistant', displayContent: fullText, reasoning: fullReasoning, anthropicReasoning })
|
||||
resMessageIsDonePromise({ type: 'llmDone', toolCall }) // resolve with tool calls
|
||||
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 })
|
||||
},
|
||||
})
|
||||
@@ -786,63 +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 } = llmRes
|
||||
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)
|
||||
@@ -852,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 })
|
||||
@@ -888,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)
|
||||
}
|
||||
|
||||
|
||||
@@ -1156,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
|
||||
}
|
||||
}]
|
||||
},
|
||||
@@ -1185,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)
|
||||
}
|
||||
|
||||
@@ -1193,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)
|
||||
|
||||
@@ -1208,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()
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1235,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
|
||||
@@ -1265,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 })
|
||||
@@ -1538,7 +1446,7 @@ We only need to do it for files that were edited since `from`, ie files between
|
||||
|
||||
}
|
||||
}
|
||||
})
|
||||
}, true)
|
||||
}
|
||||
|
||||
|
||||
@@ -1569,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)
|
||||
}
|
||||
|
||||
|
||||
@@ -1592,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)
|
||||
}
|
||||
|
||||
|
||||
@@ -1605,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) {
|
||||
@@ -1621,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)
|
||||
}
|
||||
|
||||
|
||||
@@ -1642,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)
|
||||
@@ -1663,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)
|
||||
@@ -1703,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 {
|
||||
|
||||
@@ -1751,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
|
||||
|
||||
@@ -1771,7 +1654,7 @@ We only need to do it for files that were edited since `from`, ie files between
|
||||
}
|
||||
}
|
||||
}
|
||||
}, doNotRefreshMountInfo)
|
||||
}, true)
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -6,12 +6,12 @@ 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';
|
||||
@@ -36,10 +36,12 @@ type SimpleLLMMessage = {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
const EMPTY_MESSAGE = '(empty message)'
|
||||
|
||||
const CHARS_PER_TOKEN = 4 // assume abysmal chars per token
|
||||
const TRIM_TO_LEN = 120
|
||||
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)}`
|
||||
}
|
||||
@@ -238,28 +239,32 @@ const prepareMessages_XML_tools = (messages: SimpleLLMMessage[], supportsAnthrop
|
||||
|
||||
|
||||
|
||||
|
||||
export type GeminiMessage = {
|
||||
role: 'user' | 'model'; // Gemini uses 'user' and 'model' roles
|
||||
parts: (
|
||||
| { text: string; }
|
||||
| { functionCall: { tool_call: any } }
|
||||
| { functionResponse: { name: ToolName, response: { result: string } } }
|
||||
)[];
|
||||
};
|
||||
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,
|
||||
@@ -268,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
|
||||
@@ -304,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
|
||||
@@ -338,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 | | |
|
||||
@@ -362,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
|
||||
@@ -441,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: { 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: { name: latestToolName, response: { result: 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 })
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -523,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');
|
||||
@@ -574,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) || [];
|
||||
@@ -585,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()
|
||||
@@ -632,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,
|
||||
@@ -658,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]
|
||||
@@ -681,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({
|
||||
@@ -693,8 +581,7 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess
|
||||
specialToolFormat,
|
||||
supportsAnthropicReasoning: providerName === 'anthropic',
|
||||
contextWindow,
|
||||
reservedOutputTokenSpace,
|
||||
providerName,
|
||||
maxOutputTokens,
|
||||
})
|
||||
return { messages, separateSystemMessage };
|
||||
}
|
||||
|
||||
+64
-132
@@ -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,289 +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 { 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
|
||||
// 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',
|
||||
];
|
||||
|
||||
|
||||
|
||||
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 ?? []) {
|
||||
if (extensionBlacklist.find(bItem => extensionFolder.resource.path.includes(bItem))) {
|
||||
console.log('Skipping...', extensionFolder.resource.path)
|
||||
continue
|
||||
}
|
||||
const from = extensionFolder.resource
|
||||
const to = URI.joinPath(toParent, extensionFolder.name)
|
||||
await fileService.copy(from, to, true)
|
||||
}
|
||||
// Ensure the destination directory exists
|
||||
} 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 extensionsURI = getExtensionsFolder(os)
|
||||
if (!extensionsURI) return
|
||||
const eURI = await this._fileService.resolve(extensionsURI)
|
||||
for (const child of eURI.children ?? []) {
|
||||
|
||||
// if is not blacklisted, continue
|
||||
if (!extensionBlacklist.find(bItem => child.resource.path.includes(bItem))) {
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('Deleting extension', child.resource.fsPath)
|
||||
await this._fileService.del(child.resource, { recursive: true, useTrash: true })
|
||||
}
|
||||
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,37 +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';
|
||||
|
||||
// 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'
|
||||
// const alreadyDeleted = this.storageService.get(deleteExtensionsStorageId, StorageScope.APPLICATION)
|
||||
// if (!alreadyDeleted) {
|
||||
// this.storageService.store(deleteExtensionsStorageId, 'true', StorageScope.APPLICATION, StorageTarget.MACHINE)
|
||||
// this.extensionTransferService.deleteBlacklistExtensions(os)
|
||||
// }
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
registerWorkbenchContribution2(MiscWorkbenchContribs.ID, MiscWorkbenchContribs, WorkbenchPhase.Eventually);
|
||||
+73
-159
@@ -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,88 +231,74 @@ export const StatusIndicatorForApplyButton = ({ applyBoxId, uri }: { applyBoxId:
|
||||
}
|
||||
|
||||
|
||||
export const ApplyButtonsHTML = ({
|
||||
codeStr,
|
||||
applyBoxId,
|
||||
uri,
|
||||
}: {
|
||||
codeStr: string,
|
||||
applyBoxId: 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 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',
|
||||
}) ?? []
|
||||
console.log('setting!!!', newApplyingUri)
|
||||
setApplying(newApplyingUri)
|
||||
} as const
|
||||
|
||||
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 })
|
||||
})
|
||||
|
||||
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])
|
||||
}, [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])
|
||||
|
||||
|
||||
const currStreamState = currStreamStateRef.current
|
||||
console.log('currStreamState...', currStreamState)
|
||||
if (uri) editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false })
|
||||
}, [applyBoxId, editCodeService])
|
||||
|
||||
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}
|
||||
@@ -329,69 +307,6 @@ export const ApplyButtonsHTML = ({
|
||||
}
|
||||
|
||||
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 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}
|
||||
@@ -410,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;
|
||||
@@ -425,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' ?
|
||||
@@ -434,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>
|
||||
|
||||
@@ -450,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} />
|
||||
{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 { voidOpenFileFn } from '../sidebar-tsx/SidebarChat.js'
|
||||
|
||||
|
||||
export type ChatMessageLocation = {
|
||||
@@ -134,13 +134,31 @@ const CodespanWithLink = ({ text, rawText, chatMessageLocation }: { text: string
|
||||
|
||||
|
||||
const onClick = () => {
|
||||
if (!link || !link.selection) return;
|
||||
|
||||
// Use the updated voidOpenFileFn to open the file and handle selection
|
||||
voidOpenFileFn(link.uri, accessor, [link.selection.startLineNumber, link.selection.endLineNumber]);
|
||||
if (!link) return;
|
||||
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={link?.displayText || text}
|
||||
text={link?.displayText || text}
|
||||
onClick={onClick}
|
||||
className={link ? 'underline hover:brightness-90 transition-all duration-200 cursor-pointer' : ''}
|
||||
@@ -301,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
+139
-10
@@ -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, }
|
||||
)
|
||||
|
||||
|
||||
@@ -160,7 +159,7 @@ export function getRelativeWorkspacePath(accessor: ReturnType<typeof useAccessor
|
||||
if (relativePath.startsWith('/')) {
|
||||
relativePath = relativePath.slice(1);
|
||||
}
|
||||
// console.log({ folderPath, relativePath, uriPath });
|
||||
console.log({ folderPath, relativePath, uriPath });
|
||||
|
||||
return relativePath;
|
||||
}
|
||||
@@ -174,19 +173,10 @@ export function getRelativeWorkspacePath(accessor: ReturnType<typeof useAccessor
|
||||
|
||||
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'
|
||||
@@ -50,7 +51,6 @@ 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'
|
||||
|
||||
|
||||
// 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(
|
||||
@@ -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(() => {
|
||||
|
||||
+220
-286
@@ -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>
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
+459
-191
@@ -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/d33b5ff9a32a748a22ac99e543a9eedd2e600062/src/vs/workbench/contrib/void/common/modelCapabilities.ts#L146-L170`
|
||||
|
||||
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,115 +141,72 @@ registerAction2(class extends Action2 {
|
||||
});
|
||||
}
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
|
||||
|
||||
// Get the views service to check if the sidebar is open
|
||||
// const viewsService = accessor.get(IViewsService)
|
||||
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', {})
|
||||
|
||||
const wasAlreadyOpen = viewsService.isViewContainerVisible(VOID_VIEW_CONTAINER_ID)
|
||||
if (!wasAlreadyOpen) {
|
||||
await commandService.executeCommand(VOID_OPEN_SIDEBAR_ACTION_ID)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// if was already open
|
||||
|
||||
const model = accessor.get(ICodeEditorService).getActiveCodeEditor()?.getModel()
|
||||
if (!model) return
|
||||
|
||||
const editor = editorService.getActiveCodeEditor()
|
||||
const selectionRange = roundRangeToLines(editor?.getSelection(), { emptySelectionBehavior: 'null' })
|
||||
|
||||
// if has no selection, close + return
|
||||
if (!selectionRange) {
|
||||
viewsService.closeViewContainer(VOID_VIEW_CONTAINER_ID);
|
||||
return;
|
||||
}
|
||||
|
||||
// if has selection, add it
|
||||
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 },
|
||||
})
|
||||
|
||||
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 },
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -220,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()
|
||||
|
||||
|
||||
}
|
||||
})
|
||||
@@ -250,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';
|
||||
|
||||
|
||||
|
||||
@@ -26,25 +24,24 @@ export interface ITerminalToolService {
|
||||
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) => {
|
||||
@@ -117,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
|
||||
|
||||
@@ -204,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];
|
||||
@@ -269,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
|
||||
}
|
||||
|
||||
@@ -285,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 ?
|
||||
@@ -337,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
|
||||
@@ -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,9 +56,6 @@ import './tooltipService.js'
|
||||
// register onboarding service
|
||||
import './voidOnboardingService.js'
|
||||
|
||||
// register misc service
|
||||
import './miscWokrbenchContrib.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'),
|
||||
},
|
||||
@@ -463,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') {
|
||||
@@ -486,13 +499,11 @@ ${directoryStr}
|
||||
- The remaining contents of the file should proceed as usual.`)
|
||||
|
||||
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()}.`)
|
||||
@@ -525,66 +536,40 @@ ${details.map((d, i) => `${i + 1}. ${d}`).join('\n\n')}`)
|
||||
// chat_systemMessage({ chatMode, workspaceFolders: [], openedURIs: [], activeURI: 'pee', persistentTerminalIDs: [], directoryStr: 'lol', }))
|
||||
// }
|
||||
|
||||
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 chat_userMessageContent = async (instructions: string, currSelns: StagingSelectionItem[] | null,
|
||||
opts: { directoryStrService: IDirectoryStrService, fileService: IFileService }
|
||||
opts: { type: 'references' } | { type: 'fullCode', voidModelService: IVoidModelService }
|
||||
) => {
|
||||
|
||||
const lineNumAddition = (range: [number, number]) => ` (lines ${range[0]}:${range[1]})`
|
||||
let selnsStrs: string[] = []
|
||||
selnsStrs = await Promise.all(currSelns?.map(async (s) => {
|
||||
|
||||
if (s.type === 'File' || s.type === 'CodeSelection') {
|
||||
const { val } = await readFile(opts.fileService, s.uri, 2_000_000)
|
||||
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
|
||||
}
|
||||
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]}`
|
||||
|
||||
const uris = await opts.directoryStrService.getAllURIsInDirectory(s.uri, { maxResults: 100 })
|
||||
const strOfFiles = await Promise.all(uris.map(async uri => {
|
||||
const { val, truncated } = await readFile(opts.fileService, uri, 100_000)
|
||||
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
|
||||
}
|
||||
else
|
||||
if (opts.type === 'references') {
|
||||
selnsStrs = currSelns?.map((s) => {
|
||||
if (s.type === 'File') return `${s.uri.fsPath}`
|
||||
if (s.type === 'CodeSelection') return `${s.uri.fsPath}${lineNumAddition(s.range)}`
|
||||
if (s.type === 'Folder') return `${s.uri.fsPath}/`
|
||||
return ''
|
||||
}) ?? [])
|
||||
}) ?? []
|
||||
}
|
||||
if (opts.type === 'fullCode') {
|
||||
selnsStrs = await Promise.all(currSelns?.map(async (s) => {
|
||||
if (s.type === 'File' || s.type === 'CodeSelection') {
|
||||
const voidModelService = opts.voidModelService
|
||||
const { model } = await voidModelService.getModelSafe(s.uri)
|
||||
if (!model) return ''
|
||||
const val = model.getValue(EndOfLinePreference.LF)
|
||||
|
||||
const lineNumAdd = s.type === 'CodeSelection' ? lineNumAddition(s.range) : ''
|
||||
const str = `${s.uri.fsPath}${lineNumAdd}\n${tripleTick[0]}${s.language}\n${val}\n${tripleTick[1]}`
|
||||
return str
|
||||
}
|
||||
if (s.type === 'Folder') {
|
||||
// TODO
|
||||
return ''
|
||||
}
|
||||
return ''
|
||||
}) ?? [])
|
||||
}
|
||||
|
||||
const selnsStr = selnsStrs.join('\n') ?? ''
|
||||
let 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: { name: ToolName, args: object } }
|
||||
)[];
|
||||
} | {
|
||||
role: 'user';
|
||||
parts: (
|
||||
| { text: string; }
|
||||
| { functionResponse: { name: ToolName, response: { result: 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)
|
||||
|
||||
@@ -10,24 +10,14 @@ import { Ollama } from 'ollama';
|
||||
import OpenAI, { ClientOptions } from 'openai';
|
||||
import { MistralCore } from '@mistralai/mistralai/core.js';
|
||||
import { fimComplete } from '@mistralai/mistralai/funcs/fimComplete.js';
|
||||
import { GoogleGenerativeAI, Tool as GeminiTool, SchemaType, FunctionDeclaration, FunctionDeclarationSchemaProperty } from '@google/generative-ai';
|
||||
import { GoogleAuth } from 'google-auth-library'
|
||||
// import { GoogleAuth } from 'google-auth-library'
|
||||
/* eslint-enable */
|
||||
|
||||
import { AnthropicLLMChatMessage, 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 { 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 () => {
|
||||
// module‑level 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 () => {
|
||||
// // module‑level 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,13 +88,16 @@ 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
|
||||
const thisConfig = settingsOfProvider[providerName]
|
||||
@@ -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,9 +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 } = 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 })
|
||||
@@ -209,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
|
||||
@@ -231,22 +215,20 @@ 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,
|
||||
} = 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 { canIOReasoning, openSourceThinkTags, } = reasoningCapabilities || {}
|
||||
const reasoningInfo = getSendableReasoningInfo('Chat', providerName, modelName_, modelSelectionOptions) // user's modelName_ here
|
||||
const includeInPayload = providerReasoningIOSettings?.input?.includeInPayload?.(reasoningInfo) || {}
|
||||
|
||||
console.log('include', includeInPayload)
|
||||
console.log('reasoningInfo', reasoningInfo)
|
||||
// tools
|
||||
const potentialTools = chatMode !== null ? openAITools(chatMode) : null
|
||||
const nativeToolsObj = potentialTools && specialToolFormat === 'openai-style' ?
|
||||
@@ -315,7 +297,6 @@ const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onE
|
||||
fullReasoningSoFar += newReasoning
|
||||
}
|
||||
|
||||
// call onText
|
||||
onText({
|
||||
fullText: fullTextSoFar,
|
||||
fullReasoning: fullReasoningSoFar,
|
||||
@@ -328,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 });
|
||||
}
|
||||
@@ -418,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
|
||||
@@ -457,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
|
||||
@@ -542,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 })
|
||||
@@ -563,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
|
||||
@@ -640,164 +620,6 @@ const sendOllamaFIM = ({ messages, onFinalMessage, onError, settingsOfProvider,
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------- GEMINI NATIVE IMPLEMENTATION ----------------
|
||||
|
||||
|
||||
|
||||
const toGeminiFunctionDecl = (toolInfo: InternalToolInfo) => {
|
||||
const { name, description, params } = toolInfo
|
||||
const paramsWithType: { [k: string]: FunctionDeclarationSchemaProperty } = {}
|
||||
for (const key in params) {
|
||||
paramsWithType[key] = { type: SchemaType.STRING, ...params[key] }
|
||||
}
|
||||
return {
|
||||
name,
|
||||
description,
|
||||
parameters: {
|
||||
type: SchemaType.OBJECT,
|
||||
properties: paramsWithType,
|
||||
}
|
||||
} 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) || {}
|
||||
|
||||
// tools
|
||||
const potentialTools = chatMode !== null ? geminiTools(chatMode) : null
|
||||
const nativeToolsObj = potentialTools && specialToolFormat === 'gemini-style' ?
|
||||
{ tools: potentialTools } as const
|
||||
: {}
|
||||
|
||||
// instance
|
||||
const genAI = new GoogleGenerativeAI(
|
||||
thisConfig.apiKey
|
||||
);
|
||||
const model = genAI.getGenerativeModel({
|
||||
systemInstruction: separateSystemMessage,
|
||||
model: modelName,
|
||||
});
|
||||
|
||||
// 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 = ''
|
||||
|
||||
model.generateContentStream({
|
||||
systemInstruction: separateSystemMessage ?? undefined,
|
||||
contents: messages as any,
|
||||
...includeInPayload,
|
||||
...nativeToolsObj,
|
||||
})
|
||||
.then(async ({ stream, response }) => {
|
||||
_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 ?? {})
|
||||
}
|
||||
|
||||
// (do not handle reasoning yet)
|
||||
|
||||
// call onText
|
||||
onText({
|
||||
fullText: fullTextSoFar,
|
||||
fullReasoning: fullReasoningSoFar,
|
||||
toolCall: isAToolName(toolName) ? { name: toolName, rawParams: {}, isDone: false, doneParams: [], id: 'dummy' } : undefined,
|
||||
})
|
||||
}
|
||||
|
||||
// on final
|
||||
if (!fullTextSoFar && !fullReasoningSoFar && !toolName) {
|
||||
onError({ message: 'Void: Response from model was empty.', fullError: null })
|
||||
} else {
|
||||
const toolId = generateUuid() // gemini does not generate tool IDs. Generate one
|
||||
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 = {
|
||||
@@ -825,7 +647,7 @@ export const sendLLMMessageToProviderImplementation = {
|
||||
list: null,
|
||||
},
|
||||
gemini: {
|
||||
sendChat: (params) => sendGeminiChat(params),
|
||||
sendChat: (params) => _sendOpenAICompatibleChat(params),
|
||||
sendFIM: null,
|
||||
list: null,
|
||||
},
|
||||
@@ -866,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
|
||||
|
||||
Reference in New Issue
Block a user