mirror of
https://github.com/voideditor/void
synced 2026-05-22 17:08:25 +00:00
Merge branch 'model-selection' into model-selection-into-rebase
This commit is contained in:
commit
524413db5a
20 changed files with 1520 additions and 908 deletions
|
|
@ -23,11 +23,11 @@ Most of Void's code lives in the folder `src/vs/workbench/contrib/void/`.
|
|||
|
||||
## Building Void
|
||||
|
||||
### a. Build Prerequisites - Mac
|
||||
### a. Mac - Build Prerequisites
|
||||
|
||||
If you're using a Mac, you need Python and XCode. You probably have these by default.
|
||||
|
||||
### b. Build Prerequisites - Windows
|
||||
### b. Windows - Build Prerequisites
|
||||
|
||||
If you're using a Windows computer, first get [Visual Studio 2022](https://visualstudio.microsoft.com/thank-you-downloading-visual-studio/?sku=Community) (recommended) or [VS Build Tools](https://visualstudio.microsoft.com/thank-you-downloading-visual-studio/?sku=BuildTools) (not recommended). If you already have both, you might need to run the next few steps on both of them.
|
||||
|
||||
|
|
@ -42,7 +42,7 @@ Go to the "Individual Components" tab and select:
|
|||
|
||||
Finally, click Install.
|
||||
|
||||
### c. Build Prerequisites - Linux
|
||||
### c. Linux - Build Prerequisites
|
||||
|
||||
First, run `npm install -g node-gyp`. Then:
|
||||
|
||||
|
|
@ -50,27 +50,28 @@ First, run `npm install -g node-gyp`. Then:
|
|||
- Red Hat (Fedora, etc): `sudo dnf install @development-tools gcc gcc-c++ make libsecret-devel krb5-devel libX11-devel libxkbfile-devel`.
|
||||
- Others: see [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute).
|
||||
|
||||
### d. Building Void
|
||||
### d. Building Void from inside VSCode
|
||||
|
||||
To build Void, open `void/` inside VSCode. Then open your terminal and run:
|
||||
|
||||
1. `npm install` to install all dependencies.
|
||||
2. `npm run watchreact` to build Void's browser dependencies like React. (If this doesn't work, try `npm run buildreact`).
|
||||
3. Build Void.
|
||||
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. Run Void.
|
||||
3. Run Void.
|
||||
- Run `./scripts/code.sh` (Mac/Linux).
|
||||
- Run `./scripts/code.bat` (Windows).
|
||||
6. Nice-to-knows.
|
||||
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).
|
||||
|
||||
If you get any errors, scroll down for common fixes.
|
||||
|
||||
#### Building Void from Terminal
|
||||
|
||||
Alternatively, if you want to build Void from the terminal, instead of pressing <kbd>Cmd+Shift+B</kbd> you can run `npm run watch`. The build is done when you see something like this:
|
||||
To build Void from the terminal instead of from inside VSCode, follow the steps above, but instead of pressing <kbd>Cmd+Shift+B</kbd>, run `npm run watch`. The build is done when you see something like this:
|
||||
|
||||
```
|
||||
[watch-extensions] [00:37:39] Finished compilation extensions with 0 errors after 19303 ms
|
||||
|
|
@ -80,15 +81,17 @@ Alternatively, if you want to build Void from the terminal, instead of pressing
|
|||
```
|
||||
|
||||
|
||||
|
||||
#### Common Fixes
|
||||
|
||||
- Make sure you followed the prerequisite steps.
|
||||
- Make sure you followed the prerequisite steps above.
|
||||
- Make sure you have Node version `20.18.2` (the version in `.nvmrc`)!
|
||||
- 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.
|
||||
- If you have any questions, feel free to [submit an issue](https://github.com/voideditor/void/issues/new). You can also refer to VSCode's complete [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute) page.
|
||||
- If you get errors like `npm error libtool: error: unrecognised option: '-static'`, make sure you have GNU libtool instead of BSD libtool (BSD is the default in macos)
|
||||
- If you get errors like `npm error libtool: error: unrecognised option: '-static'`, when running ./scripts/code.sh, make sure you have GNU libtool instead of BSD libtool (BSD is the default in macos)
|
||||
- If you get erorrs like `The SUID sandbox helper binary was found, but is not configured correctly` when running ./scripts/code.sh, run
|
||||
`sudo chown root:root .build/electron/chrome-sandbox && sudo chmod 4755 .build/electron/chrome-sandbox` and then run `./scripts/code.sh` again.
|
||||
- If you have any other questions, feel free to [submit an issue](https://github.com/voideditor/void/issues/new). You can also refer to VSCode's complete [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute) page.
|
||||
|
||||
|
||||
## Packaging
|
||||
|
|
|
|||
128
package-lock.json
generated
128
package-lock.json
generated
|
|
@ -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,6 +44,18 @@
|
|||
"@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",
|
||||
|
|
@ -1713,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": {
|
||||
|
|
@ -5219,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"
|
||||
},
|
||||
|
|
@ -8816,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",
|
||||
|
|
@ -9456,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",
|
||||
|
|
@ -9583,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": {
|
||||
|
|
@ -18955,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"
|
||||
}
|
||||
|
|
@ -19429,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",
|
||||
|
|
@ -19441,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",
|
||||
|
|
|
|||
20
package.json
20
package.json
|
|
@ -95,16 +95,16 @@
|
|||
"@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",
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { ITextModel } from '../../../../editor/common/model.js';
|
||||
import { EndOfLinePreference, ITextModel } from '../../../../editor/common/model.js';
|
||||
import { Position } from '../../../../editor/common/core/position.js';
|
||||
import { InlineCompletion, } from '../../../../editor/common/languages.js';
|
||||
import { Range } from '../../../../editor/common/core/range.js';
|
||||
|
|
@ -425,7 +425,7 @@ const toInlineCompletions = ({ autocompletionMatchup, autocompletion, prefixAndS
|
|||
type PrefixAndSuffixInfo = { prefix: string, suffix: string, prefixLines: string[], suffixLines: string[], prefixToTheLeftOfCursor: string, suffixToTheRightOfCursor: string }
|
||||
const getPrefixAndSuffixInfo = (model: ITextModel, position: Position): PrefixAndSuffixInfo => {
|
||||
|
||||
const fullText = model.getValue();
|
||||
const fullText = model.getValue(EndOfLinePreference.LF);
|
||||
|
||||
const cursorOffset = model.getOffsetAt(position)
|
||||
const prefix = fullText.substring(0, cursorOffset)
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ const CHAT_RETRIES = 3
|
|||
const RETRY_DELAY = 2500
|
||||
|
||||
|
||||
export const findStagingSelectionIndex = (currentSelections: StagingSelectionItem[] | undefined, newSelection: StagingSelectionItem): number | null => {
|
||||
const findStagingSelectionIndex = (currentSelections: StagingSelectionItem[] | undefined, newSelection: StagingSelectionItem): number | null => {
|
||||
if (!currentSelections) return null
|
||||
|
||||
for (let i = 0; i < currentSelections.length; i += 1) {
|
||||
|
|
@ -235,6 +235,8 @@ export interface IChatThreadService {
|
|||
isCurrentlyFocusingMessage(): boolean;
|
||||
setCurrentlyFocusedMessageIdx(messageIdx: number | undefined): void;
|
||||
|
||||
addNewStagingSelection(newSelection: StagingSelectionItem): void;
|
||||
|
||||
dangerousSetState: (newState: ThreadsState) => void;
|
||||
resetState: () => void;
|
||||
|
||||
|
|
@ -543,6 +545,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
}
|
||||
// once validated, add checkpoint for edit
|
||||
if (toolName === 'edit_file') { this._addToolEditCheckpoint({ threadId, uri: (toolParams as ToolCallParams['edit_file']).uri }) }
|
||||
if (toolName === 'rewrite_file') { this._addToolEditCheckpoint({ threadId, uri: (toolParams as ToolCallParams['rewrite_file']).uri }) }
|
||||
|
||||
// 2. if tool requires approval, break from the loop, awaiting approval
|
||||
|
||||
|
|
@ -842,7 +845,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
// if (fsPath in lastIdxOfURI) continue // if already visisted, don't visit again
|
||||
// const { model } = this._voidModelService.getModelFromFsPath(fsPath)
|
||||
// if (!model) continue
|
||||
// currStrOfFsPath[fsPath] = model.getValue()
|
||||
// currStrOfFsPath[fsPath] = model.getValue(EndOfLinePreference.LF)
|
||||
// }
|
||||
|
||||
return { voidFileSnapshotOfURI }
|
||||
|
|
@ -1575,6 +1578,39 @@ We only need to do it for files that were edited since `from`, ie files between
|
|||
// this.jumpToCheckpointBeforeMessageIdx({ threadId, messageIdx, jumpToUserModified: true })
|
||||
}
|
||||
|
||||
|
||||
addNewStagingSelection(newSelection: StagingSelectionItem): void {
|
||||
|
||||
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 })
|
||||
}
|
||||
|
||||
// if matches with existing selection, overwrite (since text may change)
|
||||
const idx = findStagingSelectionIndex(selections, newSelection)
|
||||
if (idx !== null && idx !== -1) {
|
||||
setSelections([
|
||||
...selections!.slice(0, idx),
|
||||
newSelection,
|
||||
...selections!.slice(idx + 1, Infinity)
|
||||
])
|
||||
}
|
||||
// if no match, add it
|
||||
else {
|
||||
setSelections([...(selections ?? []), newSelection])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// set message.state
|
||||
private _setCurrentMessageState(state: Partial<UserMessageState>, messageIdx: number): void {
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import { IDirectoryStrService } from './directoryStrService.js';
|
|||
import { ITerminalToolService } from './terminalToolService.js';
|
||||
import { IVoidModelService } from '../common/voidModelService.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { EndOfLinePreference } from '../../../../editor/common/model.js';
|
||||
|
||||
|
||||
|
||||
|
|
@ -447,7 +448,7 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess
|
|||
const uri = URI.joinPath(folder.uri, '.voidrules')
|
||||
const { model } = this.voidModelService.getModel(uri)
|
||||
if (!model) continue
|
||||
voidRules += model.getValue() + '\n\n';
|
||||
voidRules += model.getValue(EndOfLinePreference.LF) + '\n\n';
|
||||
}
|
||||
return voidRules.trim();
|
||||
}
|
||||
|
|
@ -482,8 +483,8 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess
|
|||
})
|
||||
const includeXMLToolDefinitions = !specialToolFormat
|
||||
|
||||
const runningTerminalIds = this.terminalToolService.listPersistentTerminalIds()
|
||||
const systemMessage = chat_systemMessage({ workspaceFolders, openedURIs, directoryStr, activeURI, runningTerminalIds, chatMode, includeXMLToolDefinitions })
|
||||
const persistentTerminalIDs = this.terminalToolService.listPersistentTerminalIds()
|
||||
const systemMessage = chat_systemMessage({ workspaceFolders, openedURIs, directoryStr, activeURI, persistentTerminalIDs, chatMode, includeXMLToolDefinitions })
|
||||
return systemMessage
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +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, searchReplace_systemMessage, searchReplace_userMessage, } from '../common/prompt/prompts.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';
|
||||
|
|
@ -1164,6 +1164,57 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
}
|
||||
|
||||
|
||||
public instantlyApplySearchReplaceBlocks({ uri, searchReplaceBlocks }: { uri: URI, searchReplaceBlocks: string }) {
|
||||
// start diffzone
|
||||
const res = this._startStreamingDiffZone({
|
||||
uri,
|
||||
streamRequestIdRef: { current: null },
|
||||
startBehavior: 'keep-conflicts',
|
||||
linkedCtrlKZone: null,
|
||||
onWillUndo: () => { },
|
||||
})
|
||||
if (!res) return
|
||||
const { diffZone, onFinishEdit } = res
|
||||
|
||||
|
||||
const onDone = () => {
|
||||
diffZone._streamState = { isStreaming: false, }
|
||||
this._onDidChangeStreamingInDiffZone.fire({ uri, diffareaid: diffZone.diffareaid })
|
||||
this._refreshStylesAndDiffsInURI(uri)
|
||||
onFinishEdit()
|
||||
}
|
||||
|
||||
|
||||
this._instantlyApplySRBlocks(uri, searchReplaceBlocks)
|
||||
|
||||
|
||||
onDone()
|
||||
}
|
||||
|
||||
|
||||
public instantlyApplyNewContent({ uri, newContent }: { uri: URI, newContent: string }) {
|
||||
// start diffzone
|
||||
const res = this._startStreamingDiffZone({
|
||||
uri,
|
||||
streamRequestIdRef: { current: null },
|
||||
startBehavior: 'keep-conflicts',
|
||||
linkedCtrlKZone: null,
|
||||
onWillUndo: () => { },
|
||||
})
|
||||
if (!res) return
|
||||
const { diffZone, onFinishEdit } = res
|
||||
|
||||
|
||||
const onDone = () => {
|
||||
diffZone._streamState = { isStreaming: false, }
|
||||
this._onDidChangeStreamingInDiffZone.fire({ uri, diffareaid: diffZone.diffareaid })
|
||||
this._refreshStylesAndDiffsInURI(uri)
|
||||
onFinishEdit()
|
||||
}
|
||||
|
||||
this._writeURIText(uri, newContent, 'wholeFileRange', { shouldRealignDiffAreas: false })
|
||||
onDone()
|
||||
}
|
||||
|
||||
|
||||
private _findOverlappingDiffArea({ startLine, endLine, uri, filter }: { startLine: number, endLine: number, uri: URI, filter?: (diffArea: DiffArea) => boolean }): DiffArea | null {
|
||||
|
|
@ -1509,6 +1560,76 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
}
|
||||
|
||||
|
||||
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
|
||||
|
||||
}
|
||||
|
||||
|
||||
private _instantlyApplySRBlocks(uri: URI, blocksStr: string) {
|
||||
const blocks = extractSearchReplaceBlocks(blocksStr)
|
||||
if (blocks.length === 0) throw new Error(`No Search/Replace blocks were received!`)
|
||||
|
||||
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)
|
||||
|
||||
const replacements: { origStart: number; origEnd: number; block: ExtractedSearchReplaceBlock }[] = []
|
||||
for (const b of blocks) {
|
||||
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))
|
||||
|
||||
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)
|
||||
|
||||
// ensure no overlap
|
||||
for (let i = 1; i < replacements.length; i++) {
|
||||
if (replacements[i].origStart <= replacements[i - 1].origEnd) {
|
||||
throw new Error(this._errContentOfInvalidStr('Has overlap', replacements[i]?.block?.orig))
|
||||
}
|
||||
}
|
||||
|
||||
// apply each replacement from right to left (so indexes don't shift)
|
||||
let newCode: string = modelStr
|
||||
for (let i = replacements.length - 1; i >= 0; i--) {
|
||||
const { origStart, origEnd, block } = replacements[i]
|
||||
newCode = newCode.slice(0, origStart) + block.final + newCode.slice(origEnd + 1, Infinity)
|
||||
}
|
||||
|
||||
this._writeURIText(uri, newCode,
|
||||
'wholeFileRange',
|
||||
{ shouldRealignDiffAreas: true }
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
private _initializeSearchAndReplaceStream(opts: StartApplyingOpts & { from: 'ClickApply' }): [DiffZone, Promise<void>] | undefined {
|
||||
const { from, applyStr, } = opts
|
||||
const featureName: FeatureName = 'Apply'
|
||||
|
|
@ -1526,10 +1647,10 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
|
||||
// build messages - ask LLM to generate search/replace block text
|
||||
const originalFileCode = model.getValue(EndOfLinePreference.LF)
|
||||
const userMessageContent = searchReplace_userMessage({ originalCode: originalFileCode, applyStr: applyStr })
|
||||
const userMessageContent = searchReplaceGivenDescription_userMessage({ originalCode: originalFileCode, applyStr: applyStr })
|
||||
|
||||
const { messages, separateSystemMessage: separateSystemMessage } = this._convertToLLMMessageService.prepareLLMSimpleMessages({
|
||||
systemMessage: searchReplace_systemMessage,
|
||||
systemMessage: searchReplaceGivenDescription_systemMessage,
|
||||
simpleMessages: [{ role: 'user', content: userMessageContent, }],
|
||||
featureName,
|
||||
modelSelection,
|
||||
|
|
@ -1577,27 +1698,6 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
}
|
||||
|
||||
|
||||
const 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
|
||||
|
||||
}
|
||||
|
||||
const onDone = () => {
|
||||
diffZone._streamState = { isStreaming: false, }
|
||||
this._onDidChangeStreamingInDiffZone.fire({ uri, diffareaid: diffZone.diffareaid })
|
||||
|
|
@ -1652,6 +1752,159 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
let resMessageDonePromise: () => void = () => { }
|
||||
const messageDonePromise = new Promise<void>((res, rej) => { resMessageDonePromise = res })
|
||||
|
||||
|
||||
const onText = (params: { fullText: string; fullReasoning: string }) => {
|
||||
const { fullText } = params
|
||||
// blocks are [done done done ... {writingFinal|writingOriginal}]
|
||||
// ^
|
||||
// currStreamingBlockNum
|
||||
|
||||
const blocks = extractSearchReplaceBlocks(fullText)
|
||||
|
||||
for (let blockNum = currStreamingBlockNum; blockNum < blocks.length; blockNum += 1) {
|
||||
const block = blocks[blockNum]
|
||||
|
||||
if (block.state === 'writingOriginal') {
|
||||
// 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)
|
||||
if (typeof originalRange !== 'string') {
|
||||
const [startLine, _] = convertOriginalRangeToFinalRange(originalRange)
|
||||
diffZone._streamState.line = startLine
|
||||
shouldUpdateOrigStreamStyle = false
|
||||
}
|
||||
}
|
||||
|
||||
// // starting line is at least the number of lines in the generated code minus 1
|
||||
// const numLinesInOrig = numLinesOfStr(block.orig)
|
||||
// const newLine = Math.max(numLinesInOrig - 1, 1, diffZone._streamState.line ?? 1)
|
||||
// if (newLine !== diffZone._streamState.line) {
|
||||
// diffZone._streamState.line = newLine
|
||||
// this._refreshStylesAndDiffsInURI(uri)
|
||||
// }
|
||||
|
||||
|
||||
// must be done writing original to move on to writing streamed content
|
||||
continue
|
||||
}
|
||||
shouldUpdateOrigStreamStyle = true
|
||||
|
||||
|
||||
// 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)
|
||||
// if error
|
||||
// Check for overlap with existing modified ranges
|
||||
const hasOverlap = addedTrackingZoneOfBlockNum.some(trackingZone => {
|
||||
const [existingStart, existingEnd] = trackingZone.metadata.originalBounds;
|
||||
const hasNoOverlap = endLine < existingStart || startLine > existingEnd
|
||||
return !hasNoOverlap
|
||||
});
|
||||
|
||||
if (typeof originalBounds === 'string' || hasOverlap) {
|
||||
const errorMessage = typeof originalBounds === 'string' ? originalBounds : 'Has overlap' as const
|
||||
|
||||
console.log('--------------Error finding text in code:')
|
||||
console.log('originalFileCode', { originalFileCode })
|
||||
console.log('fullText', { fullText })
|
||||
console.log('error:', errorMessage)
|
||||
console.log('block.orig:', block.orig)
|
||||
console.log('---------')
|
||||
const content = this._errContentOfInvalidStr(errorMessage, block.orig)
|
||||
messages.push(
|
||||
{ role: 'assistant', content: fullText }, // latest output
|
||||
{ role: 'user', content: content } // user explanation of what's wrong
|
||||
)
|
||||
|
||||
// REVERT ALL BLOCKS
|
||||
currStreamingBlockNum = 0
|
||||
latestStreamLocationMutable = null
|
||||
shouldUpdateOrigStreamStyle = true
|
||||
oldBlocks = []
|
||||
for (const trackingZone of addedTrackingZoneOfBlockNum)
|
||||
this._deleteTrackingZone(trackingZone)
|
||||
addedTrackingZoneOfBlockNum.splice(0, Infinity)
|
||||
|
||||
this._writeURIText(uri, originalFileCode, 'wholeFileRange', { shouldRealignDiffAreas: true })
|
||||
|
||||
// abort and resolve
|
||||
shouldSendAnotherMessage = true
|
||||
if (streamRequestIdRef.current) {
|
||||
weAreAborting = true
|
||||
this._llmMessageService.abort(streamRequestIdRef.current)
|
||||
weAreAborting = false
|
||||
}
|
||||
diffZone._streamState.line = 1
|
||||
resMessageDonePromise()
|
||||
this._refreshStylesAndDiffsInURI(uri)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
|
||||
const [startLine, endLine] = convertOriginalRangeToFinalRange(originalBounds)
|
||||
|
||||
// console.log('---------adding-------')
|
||||
// console.log('CURRENT TEXT!!!', { current: model?.getValue(EndOfLinePreference.LF) })
|
||||
// console.log('block', deepClone(block))
|
||||
// console.log('origBounds', originalBounds)
|
||||
// console.log('start end', startLine, endLine)
|
||||
|
||||
// otherwise if no error, add the position as a diffarea
|
||||
const adding: Omit<TrackingZone<SearchReplaceDiffAreaMetadata>, 'diffareaid'> = {
|
||||
type: 'TrackingZone',
|
||||
startLine: startLine,
|
||||
endLine: endLine,
|
||||
_URI: uri,
|
||||
metadata: {
|
||||
originalBounds: [...originalBounds],
|
||||
originalCode: block.orig,
|
||||
},
|
||||
}
|
||||
const trackingZone = this._addDiffArea(adding)
|
||||
addedTrackingZoneOfBlockNum.push(trackingZone)
|
||||
latestStreamLocationMutable = { line: startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 }
|
||||
} // end adding diffarea
|
||||
|
||||
|
||||
// should always be in streaming state here
|
||||
if (!diffZone._streamState.isStreaming) {
|
||||
console.error('DiffZone was not in streaming state in _initializeSearchAndReplaceStream')
|
||||
continue
|
||||
}
|
||||
|
||||
// if a block is done, finish it by writing all
|
||||
if (block.state === 'done') {
|
||||
const { startLine: finalStartLine, endLine: finalEndLine } = addedTrackingZoneOfBlockNum[blockNum]
|
||||
this._writeURIText(uri, block.final,
|
||||
{ startLineNumber: finalStartLine, startColumn: 1, endLineNumber: finalEndLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed
|
||||
{ shouldRealignDiffAreas: true }
|
||||
)
|
||||
diffZone._streamState.line = finalEndLine + 1
|
||||
currStreamingBlockNum = blockNum + 1
|
||||
continue
|
||||
}
|
||||
|
||||
// write the added text to the file
|
||||
if (!latestStreamLocationMutable) continue
|
||||
const oldBlock = oldBlocks[blockNum]
|
||||
const oldFinalLen = (oldBlock?.final ?? '').length
|
||||
const deltaFinalText = block.final.substring(oldFinalLen, Infinity)
|
||||
|
||||
this._writeStreamedDiffZoneLLMText(uri, block.orig, block.final, deltaFinalText, latestStreamLocationMutable)
|
||||
oldBlocks = blocks // oldblocks is only used if writingFinal
|
||||
|
||||
// const { endLine: currentEndLine } = addedTrackingZoneOfBlockNum[blockNum] // would be bad to do this because a lot of the bottom lines might be the same. more accurate to go with latestStreamLocationMutable
|
||||
// diffZone._streamState.line = currentEndLine
|
||||
diffZone._streamState.line = latestStreamLocationMutable.line
|
||||
|
||||
} // end for
|
||||
|
||||
this._refreshStylesAndDiffsInURI(uri)
|
||||
}
|
||||
|
||||
streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({
|
||||
messagesType: 'chatMessages',
|
||||
logging: { loggingName: `Edit (Search/Replace) - ${from}` },
|
||||
|
|
@ -1661,201 +1914,25 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
separateSystemMessage,
|
||||
chatMode: null, // not chat
|
||||
onText: (params) => {
|
||||
const { fullText } = params
|
||||
// blocks are [done done done ... {writingFinal|writingOriginal}]
|
||||
// ^
|
||||
// currStreamingBlockNum
|
||||
|
||||
const blocks = extractSearchReplaceBlocks(fullText)
|
||||
|
||||
for (let blockNum = currStreamingBlockNum; blockNum < blocks.length; blockNum += 1) {
|
||||
const block = blocks[blockNum]
|
||||
|
||||
if (block.state === 'writingOriginal') {
|
||||
// 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)
|
||||
if (typeof originalRange !== 'string') {
|
||||
const [startLine, _] = convertOriginalRangeToFinalRange(originalRange)
|
||||
diffZone._streamState.line = startLine
|
||||
shouldUpdateOrigStreamStyle = false
|
||||
}
|
||||
}
|
||||
|
||||
// // starting line is at least the number of lines in the generated code minus 1
|
||||
// const numLinesInOrig = numLinesOfStr(block.orig)
|
||||
// const newLine = Math.max(numLinesInOrig - 1, 1, diffZone._streamState.line ?? 1)
|
||||
// if (newLine !== diffZone._streamState.line) {
|
||||
// diffZone._streamState.line = newLine
|
||||
// this._refreshStylesAndDiffsInURI(uri)
|
||||
// }
|
||||
|
||||
|
||||
// must be done writing original to move on to writing streamed content
|
||||
continue
|
||||
}
|
||||
shouldUpdateOrigStreamStyle = true
|
||||
|
||||
|
||||
// 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)
|
||||
// if error
|
||||
// Check for overlap with existing modified ranges
|
||||
const hasOverlap = addedTrackingZoneOfBlockNum.some(trackingZone => {
|
||||
const [existingStart, existingEnd] = trackingZone.metadata.originalBounds;
|
||||
const hasNoOverlap = endLine < existingStart || startLine > existingEnd
|
||||
return !hasNoOverlap
|
||||
});
|
||||
|
||||
if (typeof originalBounds === 'string' || hasOverlap) {
|
||||
const errorMessage = typeof originalBounds === 'string' ? originalBounds : 'Has overlap' as const
|
||||
|
||||
console.log('--------------Error finding text in code:')
|
||||
console.log('originalFileCode', { originalFileCode })
|
||||
console.log('fullText', { fullText })
|
||||
console.log('error:', errorMessage)
|
||||
console.log('block.orig:', block.orig)
|
||||
console.log('---------')
|
||||
const content = errContentOfInvalidStr(errorMessage, block.orig)
|
||||
messages.push(
|
||||
{ role: 'assistant', content: fullText }, // latest output
|
||||
{ role: 'user', content: content } // user explanation of what's wrong
|
||||
)
|
||||
|
||||
// REVERT ALL BLOCKS
|
||||
currStreamingBlockNum = 0
|
||||
latestStreamLocationMutable = null
|
||||
shouldUpdateOrigStreamStyle = true
|
||||
oldBlocks = []
|
||||
for (const trackingZone of addedTrackingZoneOfBlockNum)
|
||||
this._deleteTrackingZone(trackingZone)
|
||||
addedTrackingZoneOfBlockNum.splice(0, Infinity)
|
||||
|
||||
this._writeURIText(uri, originalFileCode, 'wholeFileRange', { shouldRealignDiffAreas: true })
|
||||
|
||||
// abort and resolve
|
||||
shouldSendAnotherMessage = true
|
||||
if (streamRequestIdRef.current) {
|
||||
weAreAborting = true
|
||||
this._llmMessageService.abort(streamRequestIdRef.current)
|
||||
weAreAborting = false
|
||||
}
|
||||
diffZone._streamState.line = 1
|
||||
resMessageDonePromise()
|
||||
this._refreshStylesAndDiffsInURI(uri)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
|
||||
const [startLine, endLine] = convertOriginalRangeToFinalRange(originalBounds)
|
||||
|
||||
// console.log('---------adding-------')
|
||||
// console.log('CURRENT TEXT!!!', { current: model?.getValue() })
|
||||
// console.log('block', deepClone(block))
|
||||
// console.log('origBounds', originalBounds)
|
||||
// console.log('start end', startLine, endLine)
|
||||
|
||||
// otherwise if no error, add the position as a diffarea
|
||||
const adding: Omit<TrackingZone<SearchReplaceDiffAreaMetadata>, 'diffareaid'> = {
|
||||
type: 'TrackingZone',
|
||||
startLine: startLine,
|
||||
endLine: endLine,
|
||||
_URI: uri,
|
||||
metadata: {
|
||||
originalBounds: [...originalBounds],
|
||||
originalCode: block.orig,
|
||||
},
|
||||
}
|
||||
const trackingZone = this._addDiffArea(adding)
|
||||
addedTrackingZoneOfBlockNum.push(trackingZone)
|
||||
latestStreamLocationMutable = { line: startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 }
|
||||
} // end adding diffarea
|
||||
|
||||
|
||||
// should always be in streaming state here
|
||||
if (!diffZone._streamState.isStreaming) {
|
||||
console.error('DiffZone was not in streaming state in _initializeSearchAndReplaceStream')
|
||||
continue
|
||||
}
|
||||
|
||||
// if a block is done, finish it by writing all
|
||||
if (block.state === 'done') {
|
||||
const { startLine: finalStartLine, endLine: finalEndLine } = addedTrackingZoneOfBlockNum[blockNum]
|
||||
this._writeURIText(uri, block.final,
|
||||
{ startLineNumber: finalStartLine, startColumn: 1, endLineNumber: finalEndLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed
|
||||
{ shouldRealignDiffAreas: true }
|
||||
)
|
||||
diffZone._streamState.line = finalEndLine + 1
|
||||
currStreamingBlockNum = blockNum + 1
|
||||
continue
|
||||
}
|
||||
|
||||
// write the added text to the file
|
||||
if (!latestStreamLocationMutable) continue
|
||||
const oldBlock = oldBlocks[blockNum]
|
||||
const oldFinalLen = (oldBlock?.final ?? '').length
|
||||
const deltaFinalText = block.final.substring(oldFinalLen, Infinity)
|
||||
|
||||
this._writeStreamedDiffZoneLLMText(uri, block.orig, block.final, deltaFinalText, latestStreamLocationMutable)
|
||||
oldBlocks = blocks // oldblocks is only used if writingFinal
|
||||
|
||||
// const { endLine: currentEndLine } = addedTrackingZoneOfBlockNum[blockNum] // would be bad to do this because a lot of the bottom lines might be the same. more accurate to go with latestStreamLocationMutable
|
||||
// diffZone._streamState.line = currentEndLine
|
||||
diffZone._streamState.line = latestStreamLocationMutable.line
|
||||
|
||||
} // end for
|
||||
|
||||
this._refreshStylesAndDiffsInURI(uri)
|
||||
onText(params)
|
||||
},
|
||||
onFinalMessage: async (params) => {
|
||||
const { fullText } = params
|
||||
onText(params)
|
||||
|
||||
|
||||
// 1. wait 500ms and fix lint errors - call lint error workflow
|
||||
// (update react state to say "Fixing errors")
|
||||
const blocks = extractSearchReplaceBlocks(fullText)
|
||||
|
||||
if (blocks.length === 0) {
|
||||
this._notificationService.info(`Void: We ran Apply, but the LLM didn't output any changes.`)
|
||||
}
|
||||
// writeover the whole file
|
||||
let newCode = originalFileCode
|
||||
|
||||
// IMPORTANT - sort by lineNum
|
||||
addedTrackingZoneOfBlockNum.sort((a, b) => a.metadata.originalBounds[0] - b.metadata.originalBounds[0])
|
||||
|
||||
// const { model } = this._voidModelService.getModel(uri)
|
||||
// console.log('DONE - editCode!', { fullText })
|
||||
// console.log('CURRENT TEXT!!!', { current: model?.getValue() })
|
||||
// console.log('addedTrackingZoneOfBlockNum', addedTrackingZoneOfBlockNum)
|
||||
// console.log('blocks', deepClone(blocks))
|
||||
|
||||
for (let blockNum = addedTrackingZoneOfBlockNum.length - 1; blockNum >= 0; blockNum -= 1) {
|
||||
const { originalBounds } = addedTrackingZoneOfBlockNum[blockNum].metadata
|
||||
const finalCode = blocks[blockNum].final
|
||||
|
||||
if (finalCode === null) continue
|
||||
|
||||
const [originalStart, originalEnd] = originalBounds
|
||||
const lines = newCode.split('\n')
|
||||
newCode = [
|
||||
...lines.slice(0, (originalStart - 1)),
|
||||
...finalCode.split('\n'),
|
||||
...lines.slice((originalEnd - 1) + 1, Infinity)
|
||||
].join('\n')
|
||||
this._notificationService.info(`Void: We ran Fast Apply, but the LLM didn't output any changes.`)
|
||||
}
|
||||
|
||||
this._writeURIText(uri, newCode,
|
||||
'wholeFileRange',
|
||||
{ shouldRealignDiffAreas: true }
|
||||
)
|
||||
|
||||
onDone()
|
||||
resMessageDonePromise()
|
||||
try {
|
||||
this._instantlyApplySRBlocks(uri, fullText)
|
||||
onDone()
|
||||
resMessageDonePromise()
|
||||
}
|
||||
catch (e) {
|
||||
onError(e)
|
||||
}
|
||||
},
|
||||
onError: (e) => {
|
||||
onError(e)
|
||||
|
|
|
|||
|
|
@ -44,6 +44,8 @@ export interface IEditCodeService {
|
|||
|
||||
callBeforeStartApplying(opts: CallBeforeStartApplyingOpts): Promise<void>;
|
||||
startApplying(opts: StartApplyingOpts): [URI, Promise<void>] | null;
|
||||
instantlyApplySearchReplaceBlocks(opts: { uri: URI; searchReplaceBlocks: string }): void;
|
||||
instantlyApplyNewContent(opts: { uri: URI; newContent: string }): void;
|
||||
addCtrlKZone(opts: AddCtrlKOpts): number | undefined;
|
||||
removeCtrlKZone(opts: { diffareaid: number }): void;
|
||||
|
||||
|
|
|
|||
|
|
@ -183,7 +183,7 @@ export const StatusIndicator = ({ indicatorColor, title, className, ...props }:
|
|||
{title && <span className='opacity-80'>{title}</span>}
|
||||
<div
|
||||
className={` size-1.5 rounded-full border
|
||||
${indicatorColor === 'dark' ? 'bg-void-bg-3 border-void-border-1' :
|
||||
${indicatorColor === 'dark' ? 'bg-[rgba(0,0,0,0)] border-void-border-1' :
|
||||
indicatorColor === 'orange' ? 'bg-orange-500 border-orange-500 shadow-[0_0_4px_0px_rgba(234,88,12,0.6)]' :
|
||||
indicatorColor === 'green' ? 'bg-green-500 border-green-500 shadow-[0_0_4px_0px_rgba(22,163,74,0.6)]' :
|
||||
indicatorColor === 'yellow' ? 'bg-yellow-500 border-yellow-500 shadow-[0_0_4px_0px_rgba(22,163,74,0.6)]' :
|
||||
|
|
@ -231,7 +231,7 @@ export const StatusIndicatorForApplyButton = ({ applyBoxId, uri }: { applyBoxId:
|
|||
}
|
||||
|
||||
|
||||
export const ApplyButtonsHTML = ({ codeStr, applyBoxId, reapplyIcon, uri }: { codeStr: string, applyBoxId: string, reapplyIcon: boolean, 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')
|
||||
|
|
@ -287,12 +287,6 @@ export const ApplyButtonsHTML = ({ codeStr, applyBoxId, reapplyIcon, uri }: { co
|
|||
if (uri) editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false })
|
||||
}, [applyBoxId, editCodeService])
|
||||
|
||||
// const onReapply = useCallback(() => {
|
||||
// onReject()
|
||||
// onClickSubmit()
|
||||
// }, [onReject, onClickSubmit])
|
||||
|
||||
|
||||
if (currStreamState === 'streaming') {
|
||||
return <IconShell1
|
||||
|
||||
|
|
@ -306,18 +300,14 @@ export const ApplyButtonsHTML = ({ codeStr, applyBoxId, reapplyIcon, uri }: { co
|
|||
if (currStreamState === 'idle-no-changes') {
|
||||
|
||||
return <IconShell1
|
||||
Icon={reapplyIcon ? RotateCw : Play}
|
||||
Icon={Play}
|
||||
onClick={onClickSubmit}
|
||||
{...tooltipPropsForApplyBlock({ tooltipName: reapplyIcon ? 'Reapply' : 'Apply' })}
|
||||
{...tooltipPropsForApplyBlock({ tooltipName: 'Apply' })}
|
||||
/>
|
||||
}
|
||||
|
||||
if (currStreamState === 'idle-has-changes') {
|
||||
return <>
|
||||
{/* <IconShell1
|
||||
Icon={RotateCw}
|
||||
onClick={onReapply}
|
||||
/> */}
|
||||
<IconShell1
|
||||
Icon={X}
|
||||
onClick={onReject}
|
||||
|
|
@ -375,7 +365,7 @@ export const BlockCodeApplyWrapper = ({
|
|||
<div className={`${canApply ? '' : 'hidden'} flex items-center gap-1`}>
|
||||
<JumpToFileButton uri={uri} />
|
||||
{currStreamState === 'idle-no-changes' && <CopyButton codeStr={initValue} toolTipName='Copy' />}
|
||||
<ApplyButtonsHTML uri={uri} applyBoxId={applyBoxId} codeStr={initValue} reapplyIcon={false} />
|
||||
<ApplyButtonsHTML uri={uri} applyBoxId={applyBoxId} codeStr={initValue} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -176,8 +176,8 @@ export const PastThreadsList = ({ className = '' }: { className?: string }) => {
|
|||
|
||||
return (
|
||||
<div className={`flex flex-col mb-2 gap-2 w-full text-nowrap text-void-fg-3 select-none relative ${className}`}>
|
||||
{displayThreads.length === 0
|
||||
? <></> // No chats yet... Suggestion: Tell me about my codebase Suggestion: Create a new .voidrules file in the root of my repo
|
||||
{displayThreads.length === 0 // this should never happen
|
||||
? <></>
|
||||
: displayThreads.map((threadId, i) => {
|
||||
const pastThread = allThreads[threadId];
|
||||
if (!pastThread) {
|
||||
|
|
@ -199,7 +199,7 @@ export const PastThreadsList = ({ className = '' }: { className?: string }) => {
|
|||
|
||||
{hasMoreThreads && !showAll && (
|
||||
<div
|
||||
className="text-void-fg-3 opacity-60 hover:opacity-100 hover:brightness-115 cursor-pointer p-1 text-xs"
|
||||
className="text-void-fg-3 opacity-80 hover:opacity-100 hover:brightness-115 cursor-pointer p-1 text-xs"
|
||||
onClick={() => setShowAll(true)}
|
||||
>
|
||||
Show {sortedThreadIds.length - numInitialThreads} more...
|
||||
|
|
@ -207,7 +207,7 @@ export const PastThreadsList = ({ className = '' }: { className?: string }) => {
|
|||
)}
|
||||
{hasMoreThreads && showAll && (
|
||||
<div
|
||||
className="text-void-fg-3 opacity-60 hover:opacity-100 hover:brightness-115 cursor-pointer p-1 text-xs"
|
||||
className="text-void-fg-3 opacity-80 hover:opacity-100 hover:brightness-115 cursor-pointer p-1 text-xs"
|
||||
onClick={() => setShowAll(false)}
|
||||
>
|
||||
Show less
|
||||
|
|
@ -384,6 +384,8 @@ const PastThreadElement = ({ pastThread, idx, hoveredIdx, setHoveredIdx, isRunni
|
|||
null}
|
||||
{/* name */}
|
||||
<span className="truncate overflow-hidden text-ellipsis">{firstMsg}</span>
|
||||
|
||||
<span className='opacity-60'>{`(${numMessages})`}</span>
|
||||
</span>
|
||||
|
||||
<div className="flex items-center gap-x-1 opacity-60">
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import React, { forwardRef, MutableRefObject, useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
|
||||
import React, { forwardRef, ForwardRefExoticComponent, MutableRefObject, RefAttributes, useCallback, useEffect, useId, useMemo, useRef, useState } from 'react';
|
||||
import { IInputBoxStyles, InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js';
|
||||
import { defaultCheckboxStyles, defaultInputBoxStyles, defaultSelectBoxStyles } from '../../../../../../../platform/theme/browser/defaultStyles.js';
|
||||
import { SelectBox } from '../../../../../../../base/browser/ui/selectBox/selectBox.js';
|
||||
|
|
@ -16,6 +16,10 @@ import { ITextModel } from '../../../../../../../editor/common/model.js';
|
|||
import { asCssVariable } from '../../../../../../../platform/theme/common/colorUtils.js';
|
||||
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 } from '../sidebar-tsx/SidebarChat.js';
|
||||
import { ChevronRight, File, Folder, FolderClosed, LucideProps } from 'lucide-react';
|
||||
import { StagingSelectionItem } from '../../../../common/chatThreadServiceTypes.js';
|
||||
|
||||
|
||||
// type guard
|
||||
|
|
@ -48,48 +52,233 @@ export const WidgetComponent = <CtorParams extends any[], Instance>({ ctor, prop
|
|||
return <div ref={containerRef} className={className === undefined ? `w-full` : className}>{children}</div>
|
||||
}
|
||||
|
||||
type GenerateNextOptions = (newPathText: string) => Option[]
|
||||
type GenerateNextOptions = (optionText: string) => Promise<Option[]>
|
||||
|
||||
type Option = {
|
||||
name: string,
|
||||
displayName: string,
|
||||
nameInMenu: string,
|
||||
iconInMenu: ForwardRefExoticComponent<Omit<LucideProps, "ref"> & RefAttributes<SVGSVGElement>>, // type for lucide-react components
|
||||
} & (
|
||||
| { nextOptions: Option[], generateNextOptions?: undefined }
|
||||
| { nextOptions?: undefined, generateNextOptions: GenerateNextOptions }
|
||||
| { 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, }
|
||||
)
|
||||
|
||||
|
||||
const getOptionsAtPath = (accessor: ReturnType<typeof useAccessor>, path: string[], newPathText: string) => {
|
||||
const isSubsequence = (text: string, pattern: string): boolean => {
|
||||
|
||||
text = text.toLowerCase()
|
||||
pattern = pattern.toLowerCase()
|
||||
|
||||
if (pattern === '') return true;
|
||||
if (text === '') return false;
|
||||
if (pattern.length > text.length) return false;
|
||||
|
||||
const seq: boolean[][] = Array(pattern.length + 1)
|
||||
.fill(null)
|
||||
.map(() => Array(text.length + 1).fill(false));
|
||||
|
||||
for (let j = 0; j <= text.length; j++) {
|
||||
seq[0][j] = true;
|
||||
}
|
||||
|
||||
for (let i = 1; i <= pattern.length; i++) {
|
||||
for (let j = 1; j <= text.length; j++) {
|
||||
if (pattern[i - 1] === text[j - 1]) {
|
||||
seq[i][j] = seq[i - 1][j - 1];
|
||||
} else {
|
||||
seq[i][j] = seq[i][j - 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
return seq[pattern.length][text.length];
|
||||
};
|
||||
|
||||
|
||||
const scoreSubsequence = (text: string, pattern: string): number => {
|
||||
if (pattern === '') return 0;
|
||||
|
||||
text = text.toLowerCase();
|
||||
pattern = pattern.toLowerCase();
|
||||
|
||||
// We'll use dynamic programming to find the longest consecutive substring
|
||||
const n = text.length;
|
||||
const m = pattern.length;
|
||||
|
||||
// This will track our maximum consecutive match length
|
||||
let maxConsecutive = 0;
|
||||
|
||||
// For each starting position in the text
|
||||
for (let i = 0; i < n; i++) {
|
||||
// Check for matches starting from this position
|
||||
let consecutiveCount = 0;
|
||||
|
||||
// For each character in the pattern
|
||||
for (let j = 0; j < m; j++) {
|
||||
// If we have a match and we're still within text bounds
|
||||
if (i + j < n && text[i + j] === pattern[j]) {
|
||||
consecutiveCount++;
|
||||
} else {
|
||||
// Break on first non-match
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Update our maximum
|
||||
maxConsecutive = Math.max(maxConsecutive, consecutiveCount);
|
||||
}
|
||||
|
||||
return maxConsecutive;
|
||||
}
|
||||
|
||||
|
||||
export function getRelativeWorkspacePath(accessor: ReturnType<typeof useAccessor>, uri: URI): string {
|
||||
const workspaceService = accessor.get('IWorkspaceContextService');
|
||||
const workspaceFolders = workspaceService.getWorkspace().folders;
|
||||
|
||||
if (!workspaceFolders.length) {
|
||||
return uri.fsPath; // No workspace folders, return original path
|
||||
}
|
||||
|
||||
// Sort workspace folders by path length (descending) to match the most specific folder first
|
||||
const sortedFolders = [...workspaceFolders].sort((a, b) =>
|
||||
b.uri.fsPath.length - a.uri.fsPath.length
|
||||
);
|
||||
|
||||
// Add trailing slash to paths for exact matching
|
||||
const uriPath = uri.fsPath.endsWith('/') ? uri.fsPath : uri.fsPath + '/';
|
||||
|
||||
// Check if the URI is inside any workspace folder
|
||||
for (const folder of sortedFolders) {
|
||||
|
||||
|
||||
const folderPath = folder.uri.fsPath.endsWith('/') ? folder.uri.fsPath : folder.uri.fsPath + '/';
|
||||
if (uriPath.startsWith(folderPath)) {
|
||||
// Calculate the relative path by removing the workspace folder path
|
||||
let relativePath = uri.fsPath.slice(folder.uri.fsPath.length);
|
||||
// Remove leading slash if present
|
||||
if (relativePath.startsWith('/')) {
|
||||
relativePath = relativePath.slice(1);
|
||||
}
|
||||
console.log({ folderPath, relativePath, uriPath });
|
||||
|
||||
return relativePath;
|
||||
}
|
||||
}
|
||||
|
||||
// URI is not in any workspace folder, return original path
|
||||
return uri.fsPath;
|
||||
}
|
||||
|
||||
|
||||
|
||||
const numOptionsToShow = 100
|
||||
|
||||
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 {
|
||||
|
||||
const searchResults = (await (await toolsService.callTool.search_pathnames_only({
|
||||
query: t,
|
||||
includePattern: null,
|
||||
pageNumber: 1,
|
||||
})).result).uris
|
||||
|
||||
if (searchFor === 'files') {
|
||||
const res: Option[] = searchResults.map(uri => {
|
||||
const relativePath = getRelativeWorkspacePath(accessor, uri)
|
||||
return {
|
||||
leafNodeType: 'File',
|
||||
uri: uri,
|
||||
iconInMenu: File,
|
||||
nameInMenu: relativePath,
|
||||
nameToPaste: getBasename(relativePath, 2),
|
||||
}
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
else if (searchFor === 'folders') {
|
||||
// Extract unique directory paths from the results
|
||||
const directoryMap = new Map<string, URI>();
|
||||
|
||||
for (const uri of searchResults) {
|
||||
if (!uri) continue;
|
||||
|
||||
// Get the full path and extract directories
|
||||
const relativePath = getRelativeWorkspacePath(accessor, uri)
|
||||
const pathParts = relativePath.split('/');
|
||||
|
||||
// Get workspace info
|
||||
const workspaceService = accessor.get('IWorkspaceContextService');
|
||||
const workspaceFolders = workspaceService.getWorkspace().folders;
|
||||
|
||||
// Find the workspace folder containing this URI
|
||||
let workspaceFolderUri: URI | undefined;
|
||||
if (workspaceFolders.length) {
|
||||
// Sort workspace folders by path length (descending) to match the most specific folder first
|
||||
const sortedFolders = [...workspaceFolders].sort((a, b) =>
|
||||
b.uri.fsPath.length - a.uri.fsPath.length
|
||||
);
|
||||
|
||||
// Find the containing workspace folder
|
||||
for (const folder of sortedFolders) {
|
||||
const folderPath = folder.uri.fsPath.endsWith('/') ? folder.uri.fsPath : folder.uri.fsPath + '/';
|
||||
const uriPath = uri.fsPath.endsWith('/') ? uri.fsPath : uri.fsPath + '/';
|
||||
|
||||
if (uriPath.startsWith(folderPath)) {
|
||||
workspaceFolderUri = folder.uri;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (workspaceFolderUri) {
|
||||
// Add each directory and its parents to the map
|
||||
let currentPath = '';
|
||||
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(
|
||||
workspaceFolderUri,
|
||||
currentPath.startsWith('/') ? currentPath.substring(1) : currentPath
|
||||
);
|
||||
|
||||
directoryMap.set(currentPath, directoryUri);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Convert map to array
|
||||
return Array.from(directoryMap.entries()).map(([relativePath, uri]) => ({
|
||||
leafNodeType: 'Folder',
|
||||
uri: uri,
|
||||
iconInMenu: Folder, // Folder
|
||||
nameInMenu: relativePath,
|
||||
nameToPaste: getBasename(relativePath, 2)
|
||||
})) satisfies Option[];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching directories:', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const allOptions: Option[] = [
|
||||
{
|
||||
name: 'files',
|
||||
displayName: 'files',
|
||||
generateNextOptions: () => [
|
||||
{ name: 'a.txt', displayName: 'a.txt', },
|
||||
{ name: 'b.txt', displayName: 'b.txt', },
|
||||
{ name: 'c.txt', displayName: 'c.txt', },
|
||||
{ name: 'd.txt', displayName: 'd.txt', },
|
||||
{ name: 'e.txt', displayName: 'e.txt', },
|
||||
{ name: 'f.txt', displayName: 'f.txt', },
|
||||
{ name: 'g.txt', displayName: 'g.txt', },
|
||||
{ name: '!a.txt', displayName: '!a.txt', },
|
||||
{ name: '!b.txt', displayName: '!b.txt', },
|
||||
{ name: '!c.txt', displayName: '!c.txt', },
|
||||
{ name: '!d.txt', displayName: '!d.txt', },
|
||||
{ name: '!e.txt', displayName: '!e.txt', },
|
||||
{ name: '!f.txt', displayName: '!f.txt', },
|
||||
{ name: '!g.txt', displayName: '!g.txt', },
|
||||
]
|
||||
nameInMenu: 'files',
|
||||
iconInMenu: File,
|
||||
generateNextOptions: async (t) => (await searchForFilesOrFolders(t, 'files')) || [],
|
||||
},
|
||||
{
|
||||
name: 'folders',
|
||||
displayName: 'folders',
|
||||
nextOptions: [
|
||||
{ name: 'FOLDER', displayName: 'FOLDER', },
|
||||
]
|
||||
nameInMenu: 'folders',
|
||||
iconInMenu: FolderClosed,
|
||||
generateNextOptions: async (t) => (await searchForFilesOrFolders(t, 'folders')) || [],
|
||||
},
|
||||
]
|
||||
|
||||
|
|
@ -100,9 +289,9 @@ const getOptionsAtPath = (accessor: ReturnType<typeof useAccessor>, path: string
|
|||
|
||||
for (const pn of path) {
|
||||
|
||||
const selectedOption = nextOptionsAtPath.find(o => o.name.toLowerCase() === pn.toLowerCase())
|
||||
const selectedOption = nextOptionsAtPath.find(o => o.nameInMenu.toLowerCase() === pn.toLowerCase())
|
||||
|
||||
if (!selectedOption) return;
|
||||
if (!selectedOption) return [];
|
||||
|
||||
nextOptionsAtPath = selectedOption.nextOptions! // assume nextOptions exists until we hit the very last option (the path will never contain the last possible option)
|
||||
generateNextOptionsAtPath = selectedOption.generateNextOptions
|
||||
|
|
@ -111,11 +300,17 @@ const getOptionsAtPath = (accessor: ReturnType<typeof useAccessor>, path: string
|
|||
|
||||
|
||||
if (generateNextOptionsAtPath) {
|
||||
nextOptionsAtPath = generateNextOptionsAtPath(newPathText)
|
||||
nextOptionsAtPath = await generateNextOptionsAtPath(optionText)
|
||||
}
|
||||
|
||||
const optionsAtPath = nextOptionsAtPath.filter(o => o.name.includes(newPathText))
|
||||
|
||||
const optionsAtPath = nextOptionsAtPath
|
||||
.filter(o => isSubsequence(o.nameInMenu, optionText))
|
||||
.sort((a, b) => { // this is a hack but good for now
|
||||
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
|
||||
|
||||
return optionsAtPath
|
||||
|
||||
|
|
@ -128,6 +323,7 @@ type InputBox2Props = {
|
|||
initValue?: string | null;
|
||||
placeholder: string;
|
||||
multiline: boolean;
|
||||
enableAtToMention?: boolean;
|
||||
fnsRef?: { current: null | TextAreaFns };
|
||||
className?: string;
|
||||
onChangeText?: (value: string) => void;
|
||||
|
|
@ -136,34 +332,28 @@ type InputBox2Props = {
|
|||
onBlur?: (e: React.FocusEvent<HTMLTextAreaElement>) => void;
|
||||
onChangeHeight?: (newHeight: number) => void;
|
||||
}
|
||||
export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(function X({ initValue, placeholder, multiline, fnsRef, className, onKeyDown, onFocus, onBlur, onChangeText }, ref) {
|
||||
export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(function X({ initValue, placeholder, multiline, enableAtToMention, fnsRef, className, onKeyDown, onFocus, onBlur, onChangeText }, ref) {
|
||||
|
||||
|
||||
// mirrors whatever is in ref
|
||||
const accessor = useAccessor()
|
||||
const toolsService = accessor.get('IToolsService')
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const chatThreadService = accessor.get('IChatThreadService')
|
||||
const languageService = accessor.get('ILanguageService')
|
||||
|
||||
const textAreaRef = useRef<HTMLTextAreaElement | null>(null)
|
||||
const selectedOptionRef = useRef<HTMLDivElement>(null);
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const [isMenuOpen, _setIsMenuOpen] = useState(false); // the @ to mention menu
|
||||
const setIsMenuOpen: typeof _setIsMenuOpen = (value) => {
|
||||
if (!enableAtToMention) { return; } // never open menu if not enabled
|
||||
_setIsMenuOpen(value);
|
||||
}
|
||||
|
||||
const [path, setPath] = useState<string[]>([]);
|
||||
// logic for @ to mention vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
|
||||
const [optionPath, setOptionPath] = useState<string[]>([]);
|
||||
const [optionIdx, setOptionIdx] = useState<number>(0);
|
||||
const [options, setOptions] = useState<Option[]>([]);
|
||||
const [newPathText, setNewPathText] = useState<string>('');
|
||||
|
||||
|
||||
const [optionText, setOptionText] = useState<string>('');
|
||||
const insertTextAtCursor = (text: string) => {
|
||||
const textarea = textAreaRef.current;
|
||||
if (!textarea) return;
|
||||
|
|
@ -173,7 +363,7 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
|
|||
|
||||
// The most reliable way to simulate typing is to use execCommand
|
||||
// which will trigger all the appropriate native events
|
||||
document.execCommand('insertText', false, text);
|
||||
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
|
||||
|
|
@ -184,68 +374,142 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
|
|||
};
|
||||
|
||||
|
||||
|
||||
const onSelectOption = () => {
|
||||
const onSelectOption = async () => {
|
||||
|
||||
if (!options.length) { return; }
|
||||
|
||||
const option = options[optionIdx];
|
||||
const newPath = [...path, option.name]
|
||||
const newPath = [...optionPath, option.nameInMenu]
|
||||
const isLastOption = !option.generateNextOptions && !option.nextOptions
|
||||
|
||||
setPath(newPath)
|
||||
setNewPathText('')
|
||||
setOptionPath(newPath)
|
||||
setOptionText('')
|
||||
setOptionIdx(0)
|
||||
if (isLastOption) {
|
||||
setIsMenuOpen(false)
|
||||
insertTextAtCursor(`TODO-${option.displayName}`)
|
||||
insertTextAtCursor(option.nameToPaste)
|
||||
|
||||
const newSelection: StagingSelectionItem = option.leafNodeType === 'File' ? {
|
||||
type: 'File',
|
||||
uri: option.uri,
|
||||
language: languageService.guessLanguageIdByFilepathOrFirstLine(option.uri) || '',
|
||||
state: { wasAddedAsCurrentFile: false }
|
||||
} : option.leafNodeType === 'Folder' ? {
|
||||
type: 'Folder',
|
||||
uri: option.uri,
|
||||
language: undefined,
|
||||
state: undefined,
|
||||
} : (undefined as never)
|
||||
chatThreadService.addNewStagingSelection(newSelection)
|
||||
console.log('selected', option.uri?.fsPath)
|
||||
}
|
||||
else {
|
||||
setOptions(getOptionsAtPath(accessor, newPath, '') || [])
|
||||
const newOpts = await getOptionsAtPath(accessor, newPath, '') || []
|
||||
setOptions(newOpts)
|
||||
}
|
||||
}
|
||||
|
||||
const onRemoveOption = () => {
|
||||
const newPath = [...path.slice(0, path.length - 1)]
|
||||
setPath(newPath)
|
||||
setNewPathText('')
|
||||
const onRemoveOption = async () => {
|
||||
const newPath = [...optionPath.slice(0, optionPath.length - 1)]
|
||||
setOptionPath(newPath)
|
||||
setOptionText('')
|
||||
setOptionIdx(0)
|
||||
setOptions(getOptionsAtPath(accessor, newPath, '') || [])
|
||||
const newOpts = await getOptionsAtPath(accessor, newPath, '') || []
|
||||
setOptions(newOpts)
|
||||
}
|
||||
|
||||
const onOpenOptionMenu = () => {
|
||||
setPath([])
|
||||
setNewPathText('')
|
||||
const onOpenOptionMenu = async () => {
|
||||
setOptionPath([])
|
||||
setOptionText('')
|
||||
setIsMenuOpen(true);
|
||||
setOptionIdx(0);
|
||||
setOptions(getOptionsAtPath(accessor, [], '') || []);
|
||||
const newOpts = await getOptionsAtPath(accessor, [], '') || []
|
||||
setOptions(newOpts);
|
||||
}
|
||||
const onCloseOptionMenu = () => {
|
||||
setIsMenuOpen(false);
|
||||
}
|
||||
|
||||
const onNavigateUp = () => {
|
||||
const onNavigateUp = (step = 1, periodic = true) => {
|
||||
if (options.length === 0) return;
|
||||
setOptionIdx((prevIdx) => (prevIdx - 1 + options.length) % options.length);
|
||||
setOptionIdx((prevIdx) => {
|
||||
const newIdx = prevIdx - step;
|
||||
return periodic ? (newIdx + options.length) % options.length : Math.max(0, newIdx);
|
||||
});
|
||||
}
|
||||
const onNavigateDown = () => {
|
||||
const onNavigateDown = (step = 1, periodic = true) => {
|
||||
if (options.length === 0) return;
|
||||
setOptionIdx((prevIdx) => (prevIdx + 1) % options.length);
|
||||
setOptionIdx((prevIdx) => {
|
||||
const newIdx = prevIdx + step;
|
||||
return periodic ? newIdx % options.length : Math.min(options.length - 1, newIdx);
|
||||
});
|
||||
}
|
||||
|
||||
const onPathTextChange = (newStr: string) => {
|
||||
setNewPathText(newStr);
|
||||
setOptions(getOptionsAtPath(accessor, path, newStr) || []);
|
||||
|
||||
const onNavigateToTop = () => {
|
||||
if (options.length === 0) return;
|
||||
setOptionIdx(0);
|
||||
}
|
||||
const onNavigateToBottom = () => {
|
||||
if (options.length === 0) return;
|
||||
setOptionIdx(options.length - 1);
|
||||
}
|
||||
|
||||
const debounceTimerRef = useRef<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Cleanup function to cancel any pending timeouts when unmounting
|
||||
return () => {
|
||||
if (debounceTimerRef.current !== null) {
|
||||
window.clearTimeout(debounceTimerRef.current);
|
||||
debounceTimerRef.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// debounced
|
||||
const onPathTextChange = useCallback((newStr: string) => {
|
||||
|
||||
setOptionText(newStr);
|
||||
|
||||
if (debounceTimerRef.current !== null) {
|
||||
window.clearTimeout(debounceTimerRef.current);
|
||||
}
|
||||
|
||||
// Set a new timeout to fetch options after a delay
|
||||
debounceTimerRef.current = window.setTimeout(async () => {
|
||||
const newOpts = await getOptionsAtPath(accessor, optionPath, newStr) || [];
|
||||
setOptions(newOpts);
|
||||
setOptionIdx(0);
|
||||
debounceTimerRef.current = null;
|
||||
}, 300);
|
||||
}, [optionPath, accessor]);
|
||||
|
||||
const onMenuKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
|
||||
const isCommandKeyPressed = e.altKey || e.ctrlKey || e.metaKey;
|
||||
|
||||
if (e.key === 'ArrowUp') {
|
||||
onNavigateUp();
|
||||
if (isCommandKeyPressed) {
|
||||
onNavigateToTop()
|
||||
} else {
|
||||
if (e.altKey) {
|
||||
onNavigateUp(10, false);
|
||||
} else {
|
||||
onNavigateUp();
|
||||
}
|
||||
}
|
||||
} else if (e.key === 'ArrowDown') {
|
||||
onNavigateDown();
|
||||
if (isCommandKeyPressed) {
|
||||
onNavigateToBottom()
|
||||
} else {
|
||||
if (e.altKey) {
|
||||
onNavigateDown(10, false);
|
||||
} else {
|
||||
onNavigateDown();
|
||||
}
|
||||
}
|
||||
} else if (e.key === 'ArrowLeft') {
|
||||
onSelectOption();
|
||||
onRemoveOption();
|
||||
} else if (e.key === 'ArrowRight') {
|
||||
onSelectOption();
|
||||
} else if (e.key === 'Enter') {
|
||||
|
|
@ -254,25 +518,26 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
|
|||
onCloseOptionMenu()
|
||||
} else if (e.key === 'Backspace') {
|
||||
|
||||
if (!newPathText) { // No text remaining
|
||||
if (path.length === 0) {
|
||||
if (!optionText) { // No text remaining
|
||||
if (optionPath.length === 0) {
|
||||
onCloseOptionMenu()
|
||||
return; // don't prevent defaults (backspaces the @ symbol)
|
||||
} else {
|
||||
onRemoveOption();
|
||||
}
|
||||
}
|
||||
else if (e.altKey || e.ctrlKey || e.metaKey) { // Ctrl+Backspace
|
||||
else if (isCommandKeyPressed) { // Ctrl+Backspace
|
||||
onPathTextChange('')
|
||||
}
|
||||
else { // Backspace
|
||||
onPathTextChange(newPathText.slice(0, -1))
|
||||
onPathTextChange(optionText.slice(0, -1))
|
||||
}
|
||||
} else if (e.key.length === 1) {
|
||||
if (e.altKey || e.ctrlKey || e.metaKey) { // Ctrl+letter
|
||||
if (isCommandKeyPressed) { // Ctrl+letter
|
||||
// do nothing
|
||||
}
|
||||
else { // letter
|
||||
onPathTextChange(newPathText + e.key)
|
||||
onPathTextChange(optionText + e.key)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -281,7 +546,7 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
|
|||
|
||||
};
|
||||
|
||||
// scroll the selected optionIdx into view on optionIdx and newPathText changes
|
||||
// scroll the selected optionIdx into view on optionIdx and optionText changes
|
||||
useEffect(() => {
|
||||
if (isMenuOpen && selectedOptionRef.current) {
|
||||
selectedOptionRef.current.scrollIntoView({
|
||||
|
|
@ -290,9 +555,7 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
|
|||
inline: 'nearest',
|
||||
});
|
||||
}
|
||||
}, [optionIdx, isMenuOpen, newPathText, selectedOptionRef]);
|
||||
|
||||
|
||||
}, [optionIdx, isMenuOpen, optionText, selectedOptionRef]);
|
||||
|
||||
const measureRef = useRef<HTMLDivElement>(null);
|
||||
const gapPx = 2
|
||||
|
|
@ -307,7 +570,7 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
|
|||
} = useFloating({
|
||||
open: isMenuOpen,
|
||||
onOpenChange: setIsMenuOpen,
|
||||
placement: 'top',
|
||||
placement: 'bottom',
|
||||
|
||||
middleware: [
|
||||
offset({ mainAxis: gapPx, crossAxis: offsetPx }),
|
||||
|
|
@ -320,13 +583,9 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
|
|||
padding: 8,
|
||||
}),
|
||||
size({
|
||||
apply({ availableHeight, elements, rects }) {
|
||||
const maxHeight = Math.min(availableHeight)
|
||||
|
||||
apply({ elements, rects }) {
|
||||
// Just set width on the floating element and let content handle scrolling
|
||||
Object.assign(elements.floating.style, {
|
||||
maxHeight: `${maxHeight}px`,
|
||||
overflowY: 'auto',
|
||||
// Ensure the width isn't constrained by the parent
|
||||
width: `${Math.max(
|
||||
rects.reference.width,
|
||||
measureRef.current?.offsetWidth ?? 0
|
||||
|
|
@ -364,7 +623,7 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
|
|||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, [isMenuOpen, refs.floating, refs.reference]);
|
||||
|
||||
// logic for @ to mention ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
|
||||
const [isEnabled, setEnabled] = useState(true)
|
||||
|
|
@ -471,7 +730,7 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
|
|||
{isMenuOpen && (
|
||||
<div
|
||||
ref={refs.setFloating}
|
||||
className="z-[100] bg-void-bg-1 border-void-border-3 border rounded shadow-lg"
|
||||
className="z-[100] border-void-border-3 bg-void-bg-2-alt border rounded shadow-lg flex flex-col overflow-hidden"
|
||||
style={{
|
||||
position: strategy,
|
||||
top: y ?? 0,
|
||||
|
|
@ -480,41 +739,53 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
|
|||
}}
|
||||
onWheel={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="py-1">
|
||||
{/* Path navigation breadcrumbs */}
|
||||
<div className="px-2 py-1 text-void-fg-3 text-sm border-b border-void-border-3">
|
||||
{[...path, newPathText].join(' > ')}
|
||||
{/* Breadcrumbs Header */}
|
||||
<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) => (
|
||||
<React.Fragment key={index}>
|
||||
<span>{path}</span>
|
||||
<ChevronRight size={12} className="mx-1" />
|
||||
</React.Fragment>
|
||||
))}
|
||||
<span>{optionText}</span>
|
||||
</div>
|
||||
: <div className='opacity-60'>Enter text to filter...</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 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) => {
|
||||
|
||||
return (
|
||||
// Option
|
||||
<div
|
||||
ref={oIdx === optionIdx ? selectedOptionRef : null}
|
||||
key={o.nameInMenu}
|
||||
className={`
|
||||
flex items-center gap-2
|
||||
px-3 py-0.5 cursor-pointer bg-void-bg-2-alt
|
||||
${oIdx === optionIdx ? 'bg-void-bg-2-hover' : ''}
|
||||
`}
|
||||
onClick={() => { onSelectOption(); }}
|
||||
onMouseOver={() => { setOptionIdx(oIdx) }}
|
||||
>
|
||||
{<o.iconInMenu size={12} />}
|
||||
<span className="text-void-fg-1">{o.nameInMenu}</span>
|
||||
{o.nextOptions || o.generateNextOptions ? (
|
||||
<ChevronRight size={12} />
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
|
||||
{/* Options list */}
|
||||
{options.length === 0 ? (
|
||||
<div className="px-3 py-2 text-void-fg-3">No options available</div>
|
||||
) : (
|
||||
options.map((o, oIdx) => (
|
||||
<div
|
||||
ref={oIdx === optionIdx ? selectedOptionRef : null}
|
||||
|
||||
key={o.name}
|
||||
className={`px-3 py-1.5 cursor-pointer bg-void-bg-2 ${oIdx === optionIdx ? 'bg-void-bg-2-hover' : ''}`}
|
||||
onClick={() => { onSelectOption(); }}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<span className="text-void-fg-1">{o.displayName}</span>
|
||||
{o.nextOptions || o.generateNextOptions ? (
|
||||
<svg className="ml-2 h-3 w-3 text-void-fg-3" viewBox="0 0 12 12" fill="none">
|
||||
<path
|
||||
d="M4.5 2.5L8 6L4.5 9.5"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ import { IEditCodeService } from '../../../editCodeServiceInterface.js'
|
|||
import { IToolsService } from '../../../toolsService.js'
|
||||
import { IConvertToLLMMessageService } from '../../../convertToLLMMessageService.js'
|
||||
import { ITerminalService } from '../../../../../terminal/browser/terminal.js'
|
||||
import { ISearchService } from '../../../../../../services/search/common/search.js'
|
||||
|
||||
|
||||
// normally to do this you'd use a useEffect that calls .onDidChangeState(), but useEffect mounts too late and misses initial state changes
|
||||
|
|
@ -205,6 +206,7 @@ const getReactAccessor = (accessor: ServicesAccessor) => {
|
|||
ILanguageDetectionService: accessor.get(ILanguageDetectionService),
|
||||
ILanguageFeaturesService: accessor.get(ILanguageFeaturesService),
|
||||
IKeybindingService: accessor.get(IKeybindingService),
|
||||
ISearchService: accessor.get(ISearchService),
|
||||
|
||||
IExplorerService: accessor.get(IExplorerService),
|
||||
IEnvironmentService: accessor.get(IEnvironmentService),
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.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';
|
||||
|
||||
const OVERRIDE_VALUE = false
|
||||
|
||||
|
|
@ -131,16 +132,25 @@ const FadeIn = ({ children, className, delayMs = 0, durationMs, ...props }: { ch
|
|||
// prev/next
|
||||
|
||||
const NextButton = ({ onClick, ...props }: { onClick: () => void } & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
|
||||
|
||||
// Create a new props object without the disabled attribute
|
||||
const { disabled, ...buttonProps } = props;
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className="px-6 py-2 bg-zinc-100 enabled:hover:bg-zinc-100 disabled:bg-zinc-100/40 disabled:cursor-not-allowed rounded text-black duration-600 transition-all"
|
||||
{...props.disabled && {
|
||||
onClick={disabled ? undefined : onClick}
|
||||
onDoubleClick={onClick}
|
||||
className={`px-6 py-2 bg-zinc-100 ${disabled
|
||||
? 'bg-zinc-100/40 cursor-not-allowed'
|
||||
: 'hover:bg-zinc-100'
|
||||
} rounded text-black duration-600 transition-all
|
||||
`}
|
||||
{...disabled && {
|
||||
'data-tooltip-id': 'void-tooltip',
|
||||
'data-tooltip-content': 'Please enter all required fields or choose another provider',
|
||||
'data-tooltip-place': 'top',
|
||||
"data-tooltip-content": 'Please enter all required fields or choose another provider', // (double-click to proceed anyway, can come back in Settings)
|
||||
"data-tooltip-place": 'top',
|
||||
}}
|
||||
{...props}
|
||||
{...buttonProps}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
|
|
@ -465,6 +475,7 @@ const VoidOnboardingContent = () => {
|
|||
|
||||
const accessor = useAccessor()
|
||||
const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
const voidMetricsService = accessor.get('IMetricsService')
|
||||
|
||||
const voidSettingsState = useSettingsState()
|
||||
|
||||
|
|
@ -535,7 +546,10 @@ const VoidOnboardingContent = () => {
|
|||
onClick={() => { setPageIndex(pageIndex - 1) }}
|
||||
/>
|
||||
<PrimaryActionButton
|
||||
onClick={() => { voidSettingsService.setGlobalSetting('isOnboardingComplete', true); }}
|
||||
onClick={() => {
|
||||
voidSettingsService.setGlobalSetting('isOnboardingComplete', true);
|
||||
voidMetricsService.capture('Completed Onboarding', { selectedProviderName, wantToUseOption })
|
||||
}}
|
||||
ringSize={voidSettingsState.globalSettings.isOnboardingComplete ? 'screen' : undefined}
|
||||
>Enter the Void</PrimaryActionButton>
|
||||
</div>
|
||||
|
|
@ -590,7 +604,7 @@ const VoidOnboardingContent = () => {
|
|||
|
||||
{/* Slice of Void image */}
|
||||
<div className='max-w-md w-full h-[30vh] mx-auto flex items-center justify-center'>
|
||||
<VoidIcon />
|
||||
{!isLinux && <VoidIcon />}
|
||||
</div>
|
||||
|
||||
|
||||
|
|
@ -618,15 +632,16 @@ const VoidOnboardingContent = () => {
|
|||
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 w-full max-w-[800px] mx-auto mt-8">
|
||||
|
||||
<button
|
||||
onClick={() => { setWantToUseOption('smart'); setPageIndex(pageIndex + 1); }}
|
||||
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">
|
||||
<Brain size={24} className="text-void-fg-2 mr-2" />
|
||||
<div className="text-lg font-medium text-void-fg-1">Intelligent</div>
|
||||
<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['smart']}</div>
|
||||
<div className="text-sm text-void-fg-2 text-left">{basicDescOfWantToUseOption['cheap']}</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
|
|
@ -641,14 +656,14 @@ const VoidOnboardingContent = () => {
|
|||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => { setWantToUseOption('cheap'); setPageIndex(pageIndex + 1); }}
|
||||
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">
|
||||
<DollarSign size={24} className="text-void-fg-2 mr-2" />
|
||||
<div className="text-lg font-medium text-void-fg-1">Affordable</div>
|
||||
<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['cheap']}</div>
|
||||
<div className="text-sm text-void-fg-2 text-left">{basicDescOfWantToUseOption['smart']}</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, Voi
|
|||
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, } 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'
|
||||
|
|
@ -147,7 +147,7 @@ const AddButton = ({ disabled, text = 'Add', ...props }: { disabled?: boolean, t
|
|||
|
||||
return <button
|
||||
disabled={disabled}
|
||||
className={`bg-[#0e70c0] px-3 py-1 text-white dark:text-black rounded-sm ${!disabled ? 'hover:bg-[#1177cb] cursor-pointer' : 'opacity-50 cursor-not-allowed bg-opacity-70'}`}
|
||||
className={`bg-[#0e70c0] px-3 py-1 text-white rounded-sm ${!disabled ? 'hover:bg-[#1177cb] cursor-pointer' : 'opacity-50 cursor-not-allowed bg-opacity-70'}`}
|
||||
{...props}
|
||||
>{text}</button>
|
||||
|
||||
|
|
@ -206,7 +206,7 @@ export const AddModelInputBox = ({ providerName: permanentProviderName, classNam
|
|||
const numModels = providerName === null ? 0 : settingsState.settingsOfProvider[providerName].models.length
|
||||
|
||||
if (showCheckmark) {
|
||||
return <AnimatedCheckmarkButton text='Added' className={`bg-[#0e70c0] text-white dark:text-black px-3 py-1 rounded-sm ${className}`} />
|
||||
return <AnimatedCheckmarkButton text='Added' className={`bg-[#0e70c0] text-white px-3 py-1 rounded-sm ${className}`} />
|
||||
}
|
||||
|
||||
if (!isOpen) {
|
||||
|
|
@ -339,6 +339,13 @@ export const ModelDump = () => {
|
|||
: '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 === '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' />
|
||||
|
||||
|
||||
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
|
||||
`}
|
||||
|
|
@ -346,7 +353,7 @@ export const ModelDump = () => {
|
|||
{/* left part is width:full */}
|
||||
<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-4'
|
||||
|
|
@ -356,7 +363,9 @@ export const ModelDump = () => {
|
|||
// : (isHidden ? `'${modelName}' won't appear in dropdowns` : ``)
|
||||
// }
|
||||
>
|
||||
<span className='opacity-50 truncate'>{type === 'autodetected' ? '(detected locally)' : type === 'default' ? '' : '(custom model)'}</span>
|
||||
|
||||
|
||||
{/* <span className='opacity-50 truncate'>{type === 'autodetected' ? '(detected locally)' : type === 'default' ? '' : '(custom model)'}</span> */}
|
||||
|
||||
<VoidSwitch
|
||||
value={value}
|
||||
|
|
|
|||
|
|
@ -30,31 +30,6 @@ import { getActiveWindow } from '../../../../base/browser/dom.js';
|
|||
// ---------- Register commands and keybindings ----------
|
||||
|
||||
|
||||
const findStagingSelectionIndex = (currentSelections: StagingSelectionItem[] | undefined, newSelection: StagingSelectionItem): number | null => {
|
||||
if (!currentSelections) return null
|
||||
|
||||
for (let i = 0; i < currentSelections.length; i += 1) {
|
||||
const s = currentSelections[i]
|
||||
|
||||
if (s.uri.fsPath !== newSelection.uri.fsPath) continue
|
||||
|
||||
if (s.type === 'File' && newSelection.type === 'File') {
|
||||
return i
|
||||
}
|
||||
if (s.type === 'CodeSelection' && newSelection.type === 'CodeSelection') {
|
||||
if (s.uri.fsPath !== newSelection.uri.fsPath) continue
|
||||
// if there's any collision return true
|
||||
const [oldStart, oldEnd] = s.range
|
||||
const [newStart, newEnd] = newSelection.range
|
||||
if (oldStart !== newStart || oldEnd !== newEnd) continue
|
||||
return i
|
||||
}
|
||||
if (s.type === 'Folder' && newSelection.type === 'Folder') {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export const roundRangeToLines = (range: IRange | null | undefined, options: { emptySelectionBehavior: 'null' | 'line' }) => {
|
||||
if (!range)
|
||||
|
|
@ -104,8 +79,6 @@ registerAction2(class extends Action2 {
|
|||
})
|
||||
|
||||
|
||||
|
||||
|
||||
// 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 {
|
||||
|
|
@ -147,36 +120,9 @@ registerAction2(class extends Action2 {
|
|||
state: { wasAddedAsCurrentFile: false }
|
||||
}
|
||||
|
||||
// update the staging selections
|
||||
const chatThreadService = accessor.get(IChatThreadService)
|
||||
|
||||
const focusedMessageIdx = chatThreadService.getCurrentFocusedMessageIdx()
|
||||
|
||||
// set the selections to the proper value
|
||||
let selections: StagingSelectionItem[] = []
|
||||
let setSelections = (s: StagingSelectionItem[]) => { }
|
||||
|
||||
if (focusedMessageIdx === undefined) {
|
||||
selections = chatThreadService.getCurrentThreadState().stagingSelections
|
||||
setSelections = (s: StagingSelectionItem[]) => chatThreadService.setCurrentThreadState({ stagingSelections: s })
|
||||
} else {
|
||||
selections = chatThreadService.getCurrentMessageState(focusedMessageIdx).stagingSelections
|
||||
setSelections = (s) => chatThreadService.setCurrentMessageState(focusedMessageIdx, { stagingSelections: s })
|
||||
}
|
||||
|
||||
// if matches with existing selection, overwrite (since text may change)
|
||||
const idx = findStagingSelectionIndex(selections, newSelection)
|
||||
if (idx !== null && idx !== -1) {
|
||||
setSelections([
|
||||
...selections!.slice(0, idx),
|
||||
newSelection,
|
||||
...selections!.slice(idx + 1, Infinity)
|
||||
])
|
||||
}
|
||||
// if no match, add it
|
||||
else {
|
||||
setSelections([...(selections ?? []), newSelection])
|
||||
}
|
||||
chatThreadService.addNewStagingSelection(newSelection)
|
||||
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import { ITerminalToolService } from './terminalToolService.js'
|
|||
import { LintErrorItem, ToolCallParams, ToolResultType } from '../common/toolsServiceTypes.js'
|
||||
import { IVoidModelService } from '../common/voidModelService.js'
|
||||
import { EndOfLinePreference } from '../../../../editor/common/model.js'
|
||||
import { basename } from '../../../../base/common/path.js'
|
||||
import { IVoidCommandBarService } from './voidCommandBarService.js'
|
||||
import { computeDirectoryTree1Deep, IDirectoryStrService, stringifyDirectoryTree1Deep } from './directoryStrService.js'
|
||||
import { IMarkerService, MarkerSeverity } from '../../../../platform/markers/common/markers.js'
|
||||
|
|
@ -38,7 +37,8 @@ const isFalsy = (u: unknown) => {
|
|||
}
|
||||
|
||||
const validateStr = (argName: string, value: unknown) => {
|
||||
if (typeof value !== 'string') throw new Error(`Invalid LLM output format: ${argName} must be a string, but it's a ${typeof value}. Value: ${value}.`)
|
||||
if (value === null) throw new Error(`Invalid LLM output: ${argName} was null.`)
|
||||
if (typeof value !== 'string') throw new Error(`Invalid LLM output format: ${argName} must be a string, but its type is "${typeof value}". Full value: ${JSON.stringify(value)}.`)
|
||||
return value
|
||||
}
|
||||
|
||||
|
|
@ -46,7 +46,8 @@ const validateStr = (argName: string, value: unknown) => {
|
|||
// We are NOT checking to make sure in workspace
|
||||
// TODO!!!! check to make sure folder/file exists
|
||||
const validateURI = (uriStr: unknown) => {
|
||||
if (typeof uriStr !== 'string') throw new Error(`Invalid LLM output format: Provided uri must be a string, but it's a ${typeof uriStr}. Value: ${uriStr}.`)
|
||||
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)}.`)
|
||||
const uri = URI.file(uriStr)
|
||||
return uri
|
||||
}
|
||||
|
|
@ -234,11 +235,18 @@ export class ToolsService implements IToolsService {
|
|||
return { uri, isRecursive, isFolder }
|
||||
},
|
||||
|
||||
edit_file: (params: RawToolParamsObj) => {
|
||||
const { uri: uriStr, change_diff: changeDiffUnknown } = params
|
||||
rewrite_file: (params: RawToolParamsObj) => {
|
||||
const { uri: uriStr, new_content: newContentUnknown } = params
|
||||
const uri = validateURI(uriStr)
|
||||
const changeDiff = validateStr('changeDiff', changeDiffUnknown)
|
||||
return { uri, changeDiff }
|
||||
const newContent = validateStr('newContent', newContentUnknown)
|
||||
return { uri, newContent }
|
||||
},
|
||||
|
||||
edit_file: (params: RawToolParamsObj) => {
|
||||
const { uri: uriStr, search_replace_blocks: searchReplaceBlocksUnknown } = params
|
||||
const uri = validateURI(uriStr)
|
||||
const searchReplaceBlocks = validateStr('searchReplaceBlocks', searchReplaceBlocksUnknown)
|
||||
return { uri, searchReplaceBlocks }
|
||||
},
|
||||
|
||||
// ---
|
||||
|
|
@ -310,6 +318,7 @@ export class ToolsService implements IToolsService {
|
|||
const query = queryBuilder.file(workspaceContextService.getWorkspace().folders.map(f => f.uri), {
|
||||
filePattern: queryStr,
|
||||
includePattern: includePattern ?? undefined,
|
||||
sortByScore: true, // makes results 10x better
|
||||
})
|
||||
const data = await searchService.fileSearch(query, CancellationToken.None)
|
||||
|
||||
|
|
@ -385,45 +394,45 @@ export class ToolsService implements IToolsService {
|
|||
return { result: {} }
|
||||
},
|
||||
|
||||
edit_file: async ({ uri, changeDiff }) => {
|
||||
rewrite_file: async ({ uri, newContent }) => {
|
||||
await voidModelService.initializeModel(uri)
|
||||
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.`)
|
||||
}
|
||||
const opts = {
|
||||
uri,
|
||||
applyStr: changeDiff,
|
||||
from: 'ClickApply',
|
||||
startBehavior: 'keep-conflicts',
|
||||
} as const
|
||||
editCodeService.instantlyApplyNewContent({ uri, newContent })
|
||||
// at end, get lint errors
|
||||
const lintErrorsPromise = Promise.resolve().then(async () => {
|
||||
await timeout(2000)
|
||||
const { lintErrors } = this._getLintErrors(uri)
|
||||
return { lintErrors }
|
||||
})
|
||||
return { result: lintErrorsPromise }
|
||||
},
|
||||
|
||||
await editCodeService.callBeforeStartApplying(opts)
|
||||
const res = editCodeService.startApplying(opts)
|
||||
if (!res) throw new Error(`The Apply model did not start running on ${basename(uri.fsPath)}. Please try again.`)
|
||||
const [diffZoneURI, applyDonePromise] = res
|
||||
|
||||
const interruptTool = () => { // must reject the applyPromiseDone promise
|
||||
editCodeService.interruptURIStreaming({ uri: diffZoneURI })
|
||||
edit_file: async ({ uri, searchReplaceBlocks }) => {
|
||||
await voidModelService.initializeModel(uri)
|
||||
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.`)
|
||||
}
|
||||
console.log('aaaa', searchReplaceBlocks)
|
||||
editCodeService.instantlyApplySearchReplaceBlocks({ uri, searchReplaceBlocks })
|
||||
|
||||
// at end, get lint errors
|
||||
const lintErrorsPromise = applyDonePromise.then(async () => {
|
||||
const lintErrorsPromise = Promise.resolve().then(async () => {
|
||||
await timeout(2000)
|
||||
const { lintErrors } = this._getLintErrors(uri)
|
||||
return { lintErrors }
|
||||
})
|
||||
|
||||
return { result: lintErrorsPromise, interruptTool }
|
||||
return { result: lintErrorsPromise }
|
||||
},
|
||||
// ---
|
||||
run_command: async ({ command, cwd, terminalId }) => {
|
||||
const { resPromise, interrupt } = await this.terminalToolService.runCommand(command, { type: 'ephemeral', cwd, terminalId })
|
||||
console.log('qqq', interrupt)
|
||||
return { result: resPromise, interruptTool: interrupt }
|
||||
},
|
||||
run_persistent_command: async ({ command, persistentTerminalId }) => {
|
||||
const { resPromise, interrupt } = await this.terminalToolService.runCommand(command, { type: 'persistent', persistentTerminalId })
|
||||
console.log('qqq', interrupt)
|
||||
return { result: resPromise, interruptTool: interrupt }
|
||||
},
|
||||
open_persistent_terminal: async ({ cwd }) => {
|
||||
|
|
@ -496,6 +505,15 @@ export class ToolsService implements IToolsService {
|
|||
|
||||
return `Change successfully made to ${params.uri.fsPath}.${lintErrsString}`
|
||||
},
|
||||
rewrite_file: (params, result) => {
|
||||
const lintErrsString = (
|
||||
this.voidSettingsService.state.globalSettings.includeToolLintErrors ?
|
||||
(result.lintErrors ? ` Lint errors found after change:\n${stringifyLintErrors(result.lintErrors)}.\nIf this is related to a change made while calling this tool, you might want to fix the error.`
|
||||
: ` No lint errors found.`)
|
||||
: '')
|
||||
|
||||
return `Change successfully made to ${params.uri.fsPath}.${lintErrsString}`
|
||||
},
|
||||
run_command: (params, result) => {
|
||||
const { resolveReason, result: result_, } = result
|
||||
// success
|
||||
|
|
|
|||
|
|
@ -34,6 +34,94 @@ export const MAX_TERMINAL_BG_COMMAND_TIME = 5
|
|||
export const MAX_PREFIX_SUFFIX_CHARS = 20_000
|
||||
|
||||
|
||||
export const ORIGINAL = `<<<<<<< ORIGINAL`
|
||||
export const DIVIDER = `=======`
|
||||
export const FINAL = `>>>>>>> UPDATED`
|
||||
|
||||
|
||||
|
||||
const searchReplaceBlockTemplate = `\
|
||||
${ORIGINAL}
|
||||
// ... original code goes here
|
||||
${DIVIDER}
|
||||
// ... final code goes here
|
||||
${FINAL}
|
||||
|
||||
${ORIGINAL}
|
||||
// ... original code goes here
|
||||
${DIVIDER}
|
||||
// ... final code goes here
|
||||
${FINAL}`
|
||||
|
||||
|
||||
|
||||
|
||||
const createSearchReplaceBlocks_systemMessage = `\
|
||||
You are a coding assistant that takes in a diff, and outputs SEARCH/REPLACE code blocks to implement the change(s) in the diff.
|
||||
The diff will be labeled \`DIFF\` and the original file will be labeled \`ORIGINAL_FILE\`.
|
||||
|
||||
Format your SEARCH/REPLACE blocks as follows:
|
||||
${tripleTick[0]}
|
||||
${searchReplaceBlockTemplate}
|
||||
${tripleTick[1]}
|
||||
|
||||
1. Your SEARCH/REPLACE block(s) must implement the diff EXACTLY. Do NOT leave anything out.
|
||||
|
||||
2. You are allowed to output multiple SEARCH/REPLACE blocks to implement the change.
|
||||
|
||||
3. Assume any comments in the diff are PART OF THE CHANGE. Include them in the output.
|
||||
|
||||
4. Your output should consist ONLY of SEARCH/REPLACE blocks. Do NOT output any text or explanations before or after this.
|
||||
|
||||
5. The ORIGINAL code in each SEARCH/REPLACE block must EXACTLY match lines in the original file. Do not add or remove any whitespace, comments, or modifications from the original code.
|
||||
|
||||
6. Each ORIGINAL text must be large enough to uniquely identify the change in the file. However, bias towards writing as little as possible.
|
||||
|
||||
7. Each ORIGINAL text must be DISJOINT from all other ORIGINAL text.
|
||||
|
||||
## EXAMPLE 1
|
||||
DIFF
|
||||
${tripleTick[0]}
|
||||
// ... existing code
|
||||
let x = 6.5
|
||||
// ... existing code
|
||||
${tripleTick[1]}
|
||||
|
||||
ORIGINAL_FILE
|
||||
${tripleTick[0]}
|
||||
let w = 5
|
||||
let x = 6
|
||||
let y = 7
|
||||
let z = 8
|
||||
${tripleTick[1]}
|
||||
|
||||
ACCEPTED OUTPUT
|
||||
${tripleTick[0]}
|
||||
${ORIGINAL}
|
||||
let x = 6
|
||||
${DIVIDER}
|
||||
let x = 6.5
|
||||
${FINAL}
|
||||
${tripleTick[1]}`
|
||||
|
||||
|
||||
const replaceTool_description = `\
|
||||
A string of SEARCH/REPLACE block(s) which will be applied to the given file.
|
||||
Your SEARCH/REPLACE blocks string must be formatted as follows:
|
||||
${searchReplaceBlockTemplate}
|
||||
|
||||
## Guidelines:
|
||||
|
||||
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.
|
||||
|
||||
3. Each ORIGINAL text must be large enough to uniquely identify the change. However, bias towards writing as little as possible.
|
||||
|
||||
4. Each ORIGINAL text must be DISJOINT from all other ORIGINAL text.
|
||||
|
||||
5. This field is a STRING (not an array).`
|
||||
|
||||
|
||||
// ======================================================== tools ========================================================
|
||||
const changesExampleContent = `\
|
||||
|
|
@ -45,7 +133,7 @@ const changesExampleContent = `\
|
|||
// {{change 3}}
|
||||
// ... existing code ...`
|
||||
|
||||
const editToolDiffExample = `\
|
||||
const editToolDescriptionExample = `\
|
||||
${tripleTick[0]}
|
||||
${changesExampleContent}
|
||||
${tripleTick[1]}`
|
||||
|
|
@ -76,6 +164,7 @@ const paginationParam = {
|
|||
} as const
|
||||
|
||||
|
||||
|
||||
const terminalDescHelper = `You can use this tool to run any command: sed, grep, etc. Do not edit any files with this tool; use edit_file instead. When working with git and other tools that open an editor (e.g. git diff), you should pipe to cat to get all results and not get stuck in vim.`
|
||||
|
||||
const cwdHelper = 'Optional. The directory in which to run the command. Defaults to the first workspace folder.'
|
||||
|
|
@ -94,13 +183,15 @@ export type SnakeCaseKeys<T extends Record<string, any>> = {
|
|||
[K in keyof T as SnakeCase<Extract<K, string>>]: T[K]
|
||||
};
|
||||
|
||||
|
||||
|
||||
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' ? editToolDiffExample : chatSuggestionDiffExample}`
|
||||
Here's an example of a good output:\n${type === 'edit tool' ? editToolDescriptionExample : chatSuggestionDiffExample}`
|
||||
|
||||
|
||||
// export const voidTools = {
|
||||
|
|
@ -209,17 +300,23 @@ export const voidTools
|
|||
},
|
||||
},
|
||||
|
||||
edit_file: { // APPLY TOOL
|
||||
edit_file: {
|
||||
name: 'edit_file',
|
||||
description: `Edits the contents of a file given the file's URI and a description.`,
|
||||
description: `Edit the contents of a file. You must provide the file's URI as well as a SINGLE string of SEARCH/REPLACE block(s) that will be used to apply the edit.`,
|
||||
params: {
|
||||
...uriParam('file'),
|
||||
change_diff: {
|
||||
description: applyToolDescription('edit tool')
|
||||
}
|
||||
search_replace_blocks: { description: replaceTool_description }
|
||||
},
|
||||
},
|
||||
|
||||
rewrite_file: {
|
||||
name: 'rewrite_file',
|
||||
description: `Edits a file, deleting all the old contents and replacing them with your new contents. Use this tool if you want to edit a file you just created.`,
|
||||
params: {
|
||||
...uriParam('file'),
|
||||
new_content: { description: `The new contents of the file. Must be a string.` }
|
||||
},
|
||||
},
|
||||
run_command: {
|
||||
name: 'run_command',
|
||||
description: `Runs a terminal command and waits for the result (times out after ${MAX_TERMINAL_INACTIVE_TIME}s of inactivity). ${terminalDescHelper}`,
|
||||
|
|
@ -247,6 +344,8 @@ export const voidTools
|
|||
cwd: { description: cwdHelper },
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
kill_persistent_terminal: {
|
||||
name: 'kill_persistent_terminal',
|
||||
description: `Interrupts and closes a persistent terminal that you opened with open_persistent_terminal.`,
|
||||
|
|
@ -287,19 +386,19 @@ const toolCallDefinitionsXMLString = (tools: InternalToolInfo[]) => {
|
|||
return `${tools.map((t, i) => {
|
||||
const params = Object.keys(t.params).map(paramName => `<${paramName}>${t.params[paramName].description}</${paramName}>`).join('\n')
|
||||
return `\
|
||||
${i + 1}. ${t.name}
|
||||
Description: ${t.description}
|
||||
Format:
|
||||
<${t.name}>${!params ? '' : `\n${params}`}
|
||||
</${t.name}>`
|
||||
${i + 1}. ${t.name}
|
||||
Description: ${t.description}
|
||||
Format:
|
||||
<${t.name}>${!params ? '' : `\n${params}`}
|
||||
</${t.name}>`
|
||||
}).join('\n\n')}`
|
||||
}
|
||||
|
||||
export const reParsedToolXMLString = (toolName: ToolName, toolParams: RawToolParamsObj) => {
|
||||
const params = Object.keys(toolParams).map(paramName => `<${paramName}>${toolParams[paramName as ToolParamName]}</${paramName}>`).join('\n')
|
||||
return `\
|
||||
<${toolName}>${!params ? '' : `\n${params}`}
|
||||
</${toolName}>`
|
||||
<${toolName}>${!params ? '' : `\n${params}`}
|
||||
</${toolName}>`
|
||||
.replace('\t', ' ')
|
||||
}
|
||||
|
||||
|
|
@ -310,28 +409,28 @@ const systemToolsXMLPrompt = (chatMode: ChatMode) => {
|
|||
if (!tools || tools.length === 0) return null
|
||||
|
||||
const toolXMLDefinitions = (`\
|
||||
Available tools:
|
||||
Available tools:
|
||||
|
||||
${toolCallDefinitionsXMLString(tools)}`)
|
||||
${toolCallDefinitionsXMLString(tools)}`)
|
||||
|
||||
const toolCallXMLGuidelines = (`\
|
||||
Tool calling details:
|
||||
- To call a tool, write its name and parameters in one of the XML formats specified above.
|
||||
- After you write the tool call, you must STOP and WAIT for the result.
|
||||
- All parameters are REQUIRED unless noted otherwise.
|
||||
- You are only allowed to output ONE tool call, and it must be at the END of your response.
|
||||
- Your tool call will be executed immediately, and the results will appear in the following user message.`)
|
||||
Tool calling details:
|
||||
- To call a tool, write its name and parameters in one of the XML formats specified above.
|
||||
- After you write the tool call, you must STOP and WAIT for the result.
|
||||
- All parameters are REQUIRED unless noted otherwise.
|
||||
- You are only allowed to output ONE tool call, and it must be at the END of your response.
|
||||
- Your tool call will be executed immediately, and the results will appear in the following user message.`)
|
||||
|
||||
return `\
|
||||
${toolXMLDefinitions}
|
||||
${toolXMLDefinitions}
|
||||
|
||||
${toolCallXMLGuidelines}`
|
||||
${toolCallXMLGuidelines}`
|
||||
}
|
||||
|
||||
// ======================================================== chat (normal, gather, agent) ========================================================
|
||||
|
||||
|
||||
export const chat_systemMessage = ({ workspaceFolders, openedURIs, activeURI, runningTerminalIds, directoryStr, chatMode: mode, includeXMLToolDefinitions }: { workspaceFolders: string[], directoryStr: string, openedURIs: string[], activeURI: string | undefined, runningTerminalIds: string[], chatMode: ChatMode, includeXMLToolDefinitions: boolean }) => {
|
||||
export const chat_systemMessage = ({ workspaceFolders, openedURIs, activeURI, persistentTerminalIDs, directoryStr, chatMode: mode, includeXMLToolDefinitions }: { workspaceFolders: string[], directoryStr: string, openedURIs: string[], activeURI: string | undefined, persistentTerminalIDs: string[], chatMode: ChatMode, includeXMLToolDefinitions: boolean }) => {
|
||||
const header = (`You are an expert coding ${mode === 'agent' ? 'agent' : 'assistant'} whose job is \
|
||||
${mode === 'agent' ? `to help the user develop, run, and make changes to their codebase.`
|
||||
: mode === 'gather' ? `to search, understand, and reference files in the user's codebase.`
|
||||
|
|
@ -353,9 +452,9 @@ ${workspaceFolders.join('\n') || 'NO FOLDERS OPEN'}
|
|||
${activeURI}
|
||||
|
||||
- Open files:
|
||||
${openedURIs.join('\n') || 'NO OPENED FILES'}${''/* separator */}${mode === 'agent' && runningTerminalIds.length !== 0 ? `
|
||||
${openedURIs.join('\n') || 'NO OPENED FILES'}${''/* separator */}${mode === 'agent' && persistentTerminalIDs.length !== 0 ? `
|
||||
|
||||
- Existing persistent terminal IDs: ${runningTerminalIds.join(', ')}` : ''}
|
||||
- Persistent terminal IDs available for you to run commands in: ${persistentTerminalIDs.join(', ')}` : ''}
|
||||
</system_info>`)
|
||||
|
||||
|
||||
|
|
@ -406,6 +505,7 @@ ${directoryStr}
|
|||
|
||||
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()}.`)
|
||||
|
||||
const importantDetails = (`Important notes:
|
||||
|
|
@ -433,7 +533,7 @@ ${details.map((d, i) => `${i + 1}. ${d}`).join('\n\n')}`)
|
|||
// // log all prompts
|
||||
// for (const chatMode of ['agent', 'gather', 'normal'] satisfies ChatMode[]) {
|
||||
// console.log(`========================================= SYSTEM MESSAGE FOR ${chatMode} ===================================\n`,
|
||||
// chat_systemMessage({ chatMode, workspaceFolders: [], openedURIs: [], activeURI: 'pee', runningTerminalIds: [], directoryStr: 'lol', }))
|
||||
// chat_systemMessage({ chatMode, workspaceFolders: [], openedURIs: [], activeURI: 'pee', persistentTerminalIDs: [], directoryStr: 'lol', }))
|
||||
// }
|
||||
|
||||
|
||||
|
|
@ -514,74 +614,17 @@ Please finish writing the new file by applying the change to the original file.
|
|||
|
||||
// ======================================================== apply (fast apply - search/replace) ========================================================
|
||||
|
||||
export const searchReplaceGivenDescription_systemMessage = createSearchReplaceBlocks_systemMessage
|
||||
|
||||
|
||||
export const ORIGINAL = `<<<<<<< ORIGINAL`
|
||||
export const DIVIDER = `=======`
|
||||
export const FINAL = `>>>>>>> UPDATED`
|
||||
|
||||
export const searchReplace_systemMessage = `\
|
||||
You are a coding assistant that takes in a diff, and outputs SEARCH/REPLACE code blocks to implement the change(s) in the diff.
|
||||
The diff will be labeled \`DIFF\` and the original file will be labeled \`ORIGINAL_FILE\`.
|
||||
|
||||
Format your SEARCH/REPLACE blocks as follows:
|
||||
${tripleTick[0]}
|
||||
${ORIGINAL}
|
||||
// ... original code goes here
|
||||
${DIVIDER}
|
||||
// ... final code goes here
|
||||
${FINAL}
|
||||
${tripleTick[1]}
|
||||
|
||||
1. Your SEARCH/REPLACE block(s) must implement the diff EXACTLY. Do NOT leave anything out.
|
||||
|
||||
2. You are allowed to output multiple SEARCH/REPLACE blocks to implement the change.
|
||||
|
||||
3. Assume any comments in the diff are PART OF THE CHANGE. Include them in the output.
|
||||
|
||||
4. Your output should consist ONLY of SEARCH/REPLACE blocks. Do NOT output any text or explanations before or after this.
|
||||
|
||||
5. The ORIGINAL code in each SEARCH/REPLACE block must EXACTLY match lines in the original file. Do not add or remove any whitespace, comments, or modifications from the original code.
|
||||
|
||||
6. Each ORIGINAL text must be large enough to uniquely identify the change in the file. However; bias towards writing as little as possible.
|
||||
|
||||
7. Each ORIGINAL text must be DISJOINT from all other ORIGINAL text.
|
||||
|
||||
## EXAMPLE 1
|
||||
DIFF
|
||||
${tripleTick[0]}
|
||||
// ... existing code
|
||||
let x = 6.5
|
||||
// ... existing code
|
||||
${tripleTick[1]}
|
||||
|
||||
ORIGINAL_FILE
|
||||
${tripleTick[0]}
|
||||
let w = 5
|
||||
let x = 6
|
||||
let y = 7
|
||||
let z = 8
|
||||
${tripleTick[1]}
|
||||
|
||||
## ACCEPTED OUTPUT
|
||||
${tripleTick[0]}
|
||||
${ORIGINAL}
|
||||
let x = 6
|
||||
${DIVIDER}
|
||||
let x = 6.5
|
||||
${FINAL}
|
||||
${tripleTick[1]}
|
||||
`
|
||||
|
||||
export const searchReplace_userMessage = ({ originalCode, applyStr }: { originalCode: string, applyStr: string }) => `\
|
||||
export const searchReplaceGivenDescription_userMessage = ({ originalCode, applyStr }: { originalCode: string, applyStr: string }) => `\
|
||||
DIFF
|
||||
${applyStr}
|
||||
|
||||
ORIGINAL_FILE
|
||||
${tripleTick[0]}
|
||||
${originalCode}
|
||||
${tripleTick[1]}
|
||||
`
|
||||
${tripleTick[1]}`
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export type ShallowDirectoryItem = {
|
|||
export const approvalTypeOfToolName: Partial<{ [T in ToolName]?: 'edits' | 'terminal' }> = {
|
||||
'create_file_or_folder': 'edits',
|
||||
'delete_file_or_folder': 'edits',
|
||||
'rewrite_file': 'edits',
|
||||
'edit_file': 'edits',
|
||||
'run_command': 'terminal',
|
||||
'run_persistent_command': 'terminal',
|
||||
|
|
@ -43,7 +44,8 @@ export type ToolCallParams = {
|
|||
'search_in_file': { uri: URI, query: string, isRegex: boolean },
|
||||
'read_lint_errors': { uri: URI },
|
||||
// ---
|
||||
'edit_file': { uri: URI, changeDiff: string },
|
||||
'rewrite_file': { uri: URI, newContent: string },
|
||||
'edit_file': { uri: URI, searchReplaceBlocks: string },
|
||||
'create_file_or_folder': { uri: URI, isFolder: boolean },
|
||||
'delete_file_or_folder': { uri: URI, isRecursive: boolean, isFolder: boolean },
|
||||
// ---
|
||||
|
|
@ -63,6 +65,7 @@ export type ToolResultType = {
|
|||
'search_in_file': { lines: number[]; },
|
||||
'read_lint_errors': { lintErrors: LintErrorItem[] | null },
|
||||
// ---
|
||||
'rewrite_file': Promise<{ lintErrors: LintErrorItem[] | null }>,
|
||||
'edit_file': Promise<{ lintErrors: LintErrorItem[] | null }>,
|
||||
'create_file_or_folder': {},
|
||||
'delete_file_or_folder': {},
|
||||
|
|
|
|||
|
|
@ -162,6 +162,10 @@ const _sendOpenAICompatibleFIM = async ({ messages: { prefix, suffix, stopTokens
|
|||
|
||||
const toOpenAICompatibleTool = (toolInfo: InternalToolInfo) => {
|
||||
const { name, description, params } = toolInfo
|
||||
|
||||
const paramsWithType: { [s: string]: { description: string; type: 'string' } } = {}
|
||||
for (const key in params) { paramsWithType[key] = { ...params[key], type: 'string' } }
|
||||
|
||||
return {
|
||||
type: 'function',
|
||||
function: {
|
||||
|
|
@ -358,12 +362,14 @@ const _openaiCompatibleList = async ({ onSuccess: onSuccess_, onError: onError_,
|
|||
// ------------ ANTHROPIC (HELPERS) ------------
|
||||
const toAnthropicTool = (toolInfo: InternalToolInfo) => {
|
||||
const { name, description, params } = toolInfo
|
||||
const paramsWithType: { [s: string]: { description: string; type: 'string' } } = {}
|
||||
for (const key in params) { paramsWithType[key] = { ...params[key], type: 'string' } }
|
||||
return {
|
||||
name: name,
|
||||
description: description,
|
||||
input_schema: {
|
||||
type: 'object',
|
||||
properties: params,
|
||||
properties: paramsWithType,
|
||||
// required: Object.keys(params),
|
||||
},
|
||||
} satisfies Anthropic.Messages.Tool
|
||||
|
|
|
|||
Loading…
Reference in a new issue