diff --git a/HOW_TO_CONTRIBUTE.md b/HOW_TO_CONTRIBUTE.md index 15323527..693ddd5c 100644 --- a/HOW_TO_CONTRIBUTE.md +++ b/HOW_TO_CONTRIBUTE.md @@ -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 Cmd+Shift+B (Mac). - Press Ctrl+Shift+B (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 Ctrl+R (Cmd+R) inside the new window to reload and see your new changes. It's faster than Ctrl+Shift+P 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 Cmd+Shift+B 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 Cmd+Shift+B, 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 diff --git a/package-lock.json b/package-lock.json index 513936cc..ab8f8512 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 23de71cf..2198d012 100644 --- a/package.json +++ b/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", diff --git a/src/vs/workbench/contrib/void/browser/autocompleteService.ts b/src/vs/workbench/contrib/void/browser/autocompleteService.ts index 95ec65c2..bfd62321 100644 --- a/src/vs/workbench/contrib/void/browser/autocompleteService.ts +++ b/src/vs/workbench/contrib/void/browser/autocompleteService.ts @@ -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) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 2cf6569c..ff2a2172 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -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, messageIdx: number): void { diff --git a/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts b/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts index 5ae17065..65b45d33 100644 --- a/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts +++ b/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts @@ -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 } diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index b310c355..240ce9a4 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -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] | 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((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, '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, '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) diff --git a/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts b/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts index a63fde7e..26ff9d2b 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts @@ -44,6 +44,8 @@ export interface IEditCodeService { callBeforeStartApplying(opts: CallBeforeStartApplyingOpts): Promise; startApplying(opts: StartApplyingOpts): [URI, Promise] | 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; diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx index a6858032..a668dbe7 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx @@ -183,7 +183,7 @@ export const StatusIndicator = ({ indicatorColor, title, className, ...props }: {title && {title}}
{ +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 } if (currStreamState === 'idle-has-changes') { return <> - {/* */} {currStreamState === 'idle-no-changes' && } - +
diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx index a60a21ee..d0188f37 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx @@ -20,7 +20,7 @@ import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js'; import { ChatMode, displayInfoOfProviderName, FeatureName, isFeatureNameDisabled } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'; import { WarningBox } from '../void-settings-tsx/WarningBox.js'; import { getModelCapabilities, getIsReasoningEnabledState } from '../../../../common/modelCapabilities.js'; -import { AlertTriangle, Ban, Check, ChevronRight, Dot, FileIcon, Pencil, Undo, Undo2, X, Flag, Copy as CopyIcon, Info, CirclePlus, Ellipsis, CircleEllipsis } from 'lucide-react'; +import { AlertTriangle, File, Ban, Check, ChevronRight, Dot, FileIcon, Pencil, Undo, Undo2, X, Flag, Copy as CopyIcon, Info, CirclePlus, Ellipsis, CircleEllipsis, Folder, ALargeSmall, TypeOutline, Text } from 'lucide-react'; import { ChatMessage, CheckpointEntry, StagingSelectionItem, ToolMessage } from '../../../../common/chatThreadServiceTypes.js'; import { approvalTypeOfToolName, LintErrorItem, ToolApprovalType, toolApprovalTypes, ToolCallParams } from '../../../../common/toolsServiceTypes.js'; import { ApplyButtonsHTML, CopyButton, IconShell1, JumpToFileButton, JumpToTerminalButton, StatusIndicator, StatusIndicatorForApplyButton, useApplyButtonState } from '../markdown/ApplyBlockHoverButtons.js'; @@ -30,8 +30,8 @@ import { MAX_FILE_CHARS_PAGE, MAX_TERMINAL_INACTIVE_TIME, ToolName, toolNames } import { RawToolCallObj } from '../../../../common/sendLLMMessageTypes.js'; import ErrorBoundary from './ErrorBoundary.js'; import { ToolApprovalTypeSwitch } from '../void-settings-tsx/Settings.js'; -import { persistentTerminalNameOfId } from '../../../terminalToolService.js'; +import { persistentTerminalNameOfId } from '../../../terminalToolService.js'; export const IconX = ({ size, className = '', ...props }: { size: number, className?: string } & React.SVGProps) => { @@ -352,14 +352,13 @@ export const VoidChatArea: React.FC = ({
-
+
{featureName === 'Chat' && }
)} -
{isStreaming && loadingIcon} @@ -382,7 +381,6 @@ export const VoidChatArea: React.FC = ({ - type ButtonProps = ButtonHTMLAttributes const DEFAULT_BUTTON_SIZE = 22; export const ButtonSubmit = ({ className, disabled, ...props }: ButtonProps & Required>) => { @@ -465,10 +463,19 @@ const ScrollToBottomContainer = ({ children, className, style, scrollContainerRe ); }; - -export const getRelative = (uri: URI, accessor: ReturnType) => { - const chatThreadService = accessor.get('IChatThreadService') - return chatThreadService.getRelativeStr(uri) || uri.fsPath +const getRelative = (uri: URI, accessor: ReturnType) => { + const workspaceContextService = accessor.get('IWorkspaceContextService') + let path: string + const isInside = workspaceContextService.isInsideWorkspace(uri) + if (isInside) { + const f = workspaceContextService.getWorkspace().folders.find(f => uri.fsPath.startsWith(f.uri.fsPath)) + if (f) { path = uri.fsPath.replace(f.uri.fsPath, '') } + else { path = uri.fsPath } + } + else { + path = uri.fsPath + } + return path || undefined } export const getFolderName = (pathStr: string) => { @@ -484,14 +491,15 @@ export const getFolderName = (pathStr: string) => { return lastTwo.join('/') + '/' } -export const getBasename = (pathStr: string) => { +export const getBasename = (pathStr: string, parts: number = 1) => { // 'unixify' path pathStr = pathStr.replace(/[/\\]+/g, '/') // replace any / or \ or \\ with / - const parts = pathStr.split('/') // split on / - if (parts.length === 0) return pathStr - return parts[parts.length - 1] + const allParts = pathStr.split('/') // split on / + if (allParts.length === 0) return pathStr + return allParts.slice(-parts).join('/') } + export const SelectedFiles = ( { type, selections, setSelections, showProspectiveSelections, messageIdx, }: | { type: 'past', selections: StagingSelectionItem[]; setSelections?: undefined, showProspectiveSelections?: undefined, messageIdx: number, } @@ -568,6 +576,13 @@ export const SelectedFiles = ( : selection.type === 'Folder' ? selection.type + selection.language + selection.state + selection.uri.fsPath : i + const SelectionIcon = ( + selection.type === 'File' ? File + : selection.type === 'Folder' ? Folder + : selection.type === 'CodeSelection' ? Text + : (undefined as never) + ) + return
+ {} + { // file name and range getBasename(selection.uri.fsPath) + (selection.type === 'CodeSelection' ? ` (${selection.range[0]}-${selection.range[1]})` : '') } {selection.type === 'File' && selection.state.wasAddedAsCurrentFile && messageIdx === undefined && currentURI?.fsPath === selection.uri.fsPath ? - + {`(Current File)`} : null } {type === 'staging' && !isThisSelectionProspective ? // X button - { e.stopPropagation(); // don't open/close selection if (type !== 'staging') return; setSelections([...selections.slice(0, i), ...selections.slice(i + 1)]) }} - size={10} - /> + > + +
: <> }
@@ -659,8 +680,6 @@ export const SelectedFiles = ( } - - type ToolHeaderParams = { icon?: React.ReactNode; title: React.ReactNode; @@ -741,12 +760,11 @@ const ToolHeaderWrapper = ({
{info && } {isError && >[0] & { content: string }) => { + const accessor = useAccessor() + const isError = toolMessage.type === 'tool_error' + const isRejected = toolMessage.type === 'rejected' + + const title = getTitle(toolMessage) + + const { desc1, desc1Info } = toolNameToDesc(toolMessage.name, toolMessage.params, accessor) + const icon = null + + const { rawParams, params } = toolMessage + const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, isRejected, } + + if (toolMessage.type === 'running_now' || toolMessage.type === 'tool_request') { + componentParams.children = + + + componentParams.desc2 = + } + else if (toolMessage.type === 'success' || toolMessage.type === 'rejected' || toolMessage.type === 'tool_error') { + // add apply box + if (params) { + const applyBoxId = getApplyBoxId({ + threadId: threadId, + messageIdx: messageIdx, + tokenIdx: 'N/A', + }) + + componentParams.desc2 = + } + + // add children + if (toolMessage.type !== 'tool_error') { + const { result } = toolMessage + + componentParams.bottomChildren = + + componentParams.children = + + + } + else { + // error + const { result } = toolMessage + if (params) { + componentParams.children = + {/* error */} + + {result} + + + {/* content */} + + + } + else { + componentParams.children = + {result} + + } + } + } + + return +} const SimplifiedToolHeader = ({ title, @@ -919,6 +1015,7 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isCheckpointGhost, curr // cancel any streams on this thread const threadId = chatThreadsService.state.currentThreadId + await chatThreadsService.abortRunning(threadId) // update state @@ -966,243 +1063,244 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isCheckpointGhost, curr setSelections={setStagingSelections} > setIsDisabled(!text)} - onFocus={() => { - setIsFocused(true) - chatThreadsService.setCurrentlyFocusedMessageIdx(messageIdx); - }} - onBlur={() => { - setIsFocused(false) - }} - onKeyDown={onKeyDown} - fnsRef={textAreaFnsRef} - multiline={true} - /> - - } + enableAtToMention + ref={setTextAreaRef} + className='min-h-[81px] max-h-[500px] px-0.5' + placeholder="Edit your message..." + onChangeText={(text) => setIsDisabled(!text)} + onFocus={() => { + setIsFocused(true) + chatThreadsService.setCurrentlyFocusedMessageIdx(messageIdx); + }} + onBlur={() => { + setIsFocused(false) + }} + onKeyDown={onKeyDown} + fnsRef={textAreaFnsRef} + multiline={true} + /> + +} - const isMsgAfterCheckpoint = currCheckpointIdx !== undefined && currCheckpointIdx === messageIdx - 1 +const isMsgAfterCheckpoint = currCheckpointIdx !== undefined && currCheckpointIdx === messageIdx - 1 - return
setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} - > -
{ if (mode === 'display') { onOpenEdit() } }} - > - {chatbubbleContents} -
+ ${isCheckpointGhost && !isMsgAfterCheckpoint ? 'opacity-50 pointer-events-none' : ''} + `} + onMouseEnter={() => setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} +> +
{ if (mode === 'display') { onOpenEdit() } }} + > + {chatbubbleContents} +
-
- { - if (mode === 'display') { - onOpenEdit() - } else if (mode === 'edit') { - onCloseEdit() - } - }} - /> -
+
+ { + if (mode === 'display') { + onOpenEdit() + } else if (mode === 'edit') { + onCloseEdit() + } + }} + /> +
-
+
} const SmallProseWrapper = ({ children }: { children: React.ReactNode }) => { - return
:first-child]:!mt-0 - [&>:last-child]:!mb-0 +[&>:first-child]:!mt-0 +[&>:last-child]:!mb-0 - prose-h1:text-[14px] - prose-h1:my-4 +prose-h1:text-[14px] +prose-h1:my-4 - prose-h2:text-[13px] - prose-h2:my-4 +prose-h2:text-[13px] +prose-h2:my-4 - prose-h3:text-[13px] - prose-h3:my-3 +prose-h3:text-[13px] +prose-h3:my-3 - prose-h4:text-[13px] - prose-h4:my-2 +prose-h4:text-[13px] +prose-h4:my-2 - prose-p:my-2 - prose-p:leading-snug - prose-hr:my-2 +prose-p:my-2 +prose-p:leading-snug +prose-hr:my-2 - prose-ul:my-2 - prose-ul:pl-4 - prose-ul:list-outside - prose-ul:list-disc - prose-ul:leading-snug +prose-ul:my-2 +prose-ul:pl-4 +prose-ul:list-outside +prose-ul:list-disc +prose-ul:leading-snug - prose-ol:my-2 - prose-ol:pl-4 - prose-ol:list-outside - prose-ol:list-decimal - prose-ol:leading-snug +prose-ol:my-2 +prose-ol:pl-4 +prose-ol:list-outside +prose-ol:list-decimal +prose-ol:leading-snug - marker:text-inherit +marker:text-inherit - prose-blockquote:pl-2 - prose-blockquote:my-2 +prose-blockquote:pl-2 +prose-blockquote:my-2 - prose-code:text-void-fg-3 - prose-code:text-[12px] - prose-code:before:content-none - prose-code:after:content-none +prose-code:text-void-fg-3 +prose-code:text-[12px] +prose-code:before:content-none +prose-code:after:content-none - prose-pre:text-[12px] - prose-pre:p-2 - prose-pre:my-2 +prose-pre:text-[12px] +prose-pre:p-2 +prose-pre:my-2 - prose-table:text-[13px] - '> - {children} -
+prose-table:text-[13px] +'> + {children} +
} const ProseWrapper = ({ children }: { children: React.ReactNode }) => { - return
- {children} -
+> + {children} + } const AssistantMessageComponent = ({ chatMessage, isCheckpointGhost, isCommitted, messageIdx }: { chatMessage: ChatMessage & { role: 'assistant' }, isCheckpointGhost: boolean, messageIdx: number, isCommitted: boolean }) => { - const accessor = useAccessor() - const chatThreadsService = accessor.get('IChatThreadService') +const accessor = useAccessor() +const chatThreadsService = accessor.get('IChatThreadService') - const reasoningStr = chatMessage.reasoning?.trim() || null - const hasReasoning = !!reasoningStr - const isDoneReasoning = !!chatMessage.displayContent - const thread = chatThreadsService.getCurrentThread() +const reasoningStr = chatMessage.reasoning?.trim() || null +const hasReasoning = !!reasoningStr +const isDoneReasoning = !!chatMessage.displayContent +const thread = chatThreadsService.getCurrentThread() - const chatMessageLocation: ChatMessageLocation = { - threadId: thread.id, - messageIdx: messageIdx, - } +const chatMessageLocation: ChatMessageLocation = { + threadId: thread.id, + messageIdx: messageIdx, +} - const isEmpty = !chatMessage.displayContent && !chatMessage.reasoning - if (isEmpty) return null +const isEmpty = !chatMessage.displayContent && !chatMessage.reasoning +if (isEmpty) return null - return <> - {/* reasoning token */} - {hasReasoning && -
- - - - - -
- } +return <> + {/* reasoning token */} + {hasReasoning && +
+ + + + + +
+ } - {/* assistant message */} - {chatMessage.displayContent && -
- - - -
- } - + {/* assistant message */} + {chatMessage.displayContent && +
+ + + +
+ } + } const ReasoningWrapper = ({ isDoneReasoning, isStreaming, children }: { isDoneReasoning: boolean, isStreaming: boolean, children: React.ReactNode }) => { - const isDone = isDoneReasoning || !isStreaming - const isWriting = !isDone - const [isOpen, setIsOpen] = useState(isWriting) - useEffect(() => { - if (!isWriting) setIsOpen(false) // if just finished reasoning, close - }, [isWriting]) - return : ''} isOpen={isOpen} onClick={() => setIsOpen(v => !v)}> - -
- {children} -
-
-
+const isDone = isDoneReasoning || !isStreaming +const isWriting = !isDone +const [isOpen, setIsOpen] = useState(isWriting) +useEffect(() => { + if (!isWriting) setIsOpen(false) // if just finished reasoning, close +}, [isWriting]) +return : ''} isOpen={isOpen} onClick={() => setIsOpen(v => !v)}> + +
+ {children} +
+
+
} @@ -1211,11 +1309,12 @@ const ReasoningWrapper = ({ isDoneReasoning, isStreaming, children }: { isDoneRe // should either be past or "-ing" tense, not present tense. Eg. when the LLM searches for something, the user expects it to say "I searched for X" or "I am searching for X". Not "I search X". const loadingTitleWrapper = (item: React.ReactNode): React.ReactNode => { - return - {item} - - +return + {item} + + } + const titleOfToolName = { 'read_file': { done: 'Read file', proposed: 'Read file', running: loadingTitleWrapper('Reading file') }, 'ls_dir': { done: 'Inspected folder', proposed: 'Inspect folder', running: loadingTitleWrapper('Inspecting folder') }, @@ -1225,7 +1324,7 @@ const titleOfToolName = { 'create_file_or_folder': { done: `Created`, proposed: `Create`, running: loadingTitleWrapper(`Creating`) }, 'delete_file_or_folder': { done: `Deleted`, proposed: `Delete`, running: loadingTitleWrapper(`Deleting`) }, 'edit_file': { done: `Edited file`, proposed: 'Edit file', running: loadingTitleWrapper('Editing file') }, - + 'rewrite_file': { done: `Wrote file`, proposed: 'Write file', running: loadingTitleWrapper('Writing file') }, 'run_command': { done: `Ran terminal`, proposed: 'Run terminal', running: loadingTitleWrapper('Running terminal') }, 'run_persistent_command': { done: `Ran terminal`, proposed: 'Run terminal', running: loadingTitleWrapper('Running terminal') }, @@ -1236,6 +1335,7 @@ const titleOfToolName = { 'search_in_file': { done: 'Searched in file', proposed: 'Search in file', running: loadingTitleWrapper('Searching in file') }, } as const satisfies Record + const getTitle = (toolMessage: Pick): React.ReactNode => { const t = toolMessage if (!toolNames.includes(t.name as ToolName)) return t.name // good measure @@ -1287,6 +1387,7 @@ const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName const toolParams = _toolParams as ToolCallParams['search_in_file']; return { desc1: `"${toolParams.query}"`, + desc1Info: getRelative(toolParams.uri, accessor), }; }, 'create_file_or_folder': () => { @@ -1303,6 +1404,13 @@ const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName desc1Info: getRelative(toolParams.uri, accessor), } }, + 'rewrite_file': () => { + const toolParams = _toolParams as ToolCallParams['rewrite_file'] + return { + desc1: getBasename(toolParams.uri.fsPath), + desc1Info: getRelative(toolParams.uri, accessor), + } + }, 'edit_file': () => { const toolParams = _toolParams as ToolCallParams['edit_file'] return { @@ -1314,7 +1422,7 @@ const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName const toolParams = _toolParams as ToolCallParams['run_command'] return { desc1: `"${toolParams.command}"`, - } + } }, 'run_persistent_command': () => { const toolParams = _toolParams as ToolCallParams['run_persistent_command'] @@ -1414,7 +1522,7 @@ const ToolRequestAcceptRejectButtons = ({ toolName }: { toolName: ToolName }) => : null - return
+ return
{approveButton} {cancelButton} {approvalToggle} @@ -1452,10 +1560,10 @@ export const ListableToolItem = ({ name, onClick, isSmall, className, showDot }: -const EditToolChildren = ({ uri, changeDiff }: { uri: URI | undefined, changeDiff: string }) => { +const EditToolChildren = ({ uri, code }: { uri: URI | undefined, code: string }) => { return
- +
} @@ -1506,7 +1614,6 @@ const EditToolHeaderButtons = ({ applyBoxId, uri, codeStr }: { applyBoxId: strin {currStreamState === 'idle-no-changes' && } -
} @@ -1539,7 +1646,6 @@ const CanceledTool = ({ toolName }: { toolName: ToolName }) => { } - const CommandTool = ({ toolMessage, type, threadId }: { threadId: string } & ({ toolMessage: Exclude, { type: 'invalid_params' }> type: 'run_command' @@ -1638,6 +1744,7 @@ const CommandTool = ({ toolMessage, type, threadId }: { threadId: string } & ({ } + type ResultWrapper = (props: { toolMessage: Exclude, { type: 'invalid_params' }>, messageIdx: number, threadId: string }) => React.ReactNode const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, } } = { 'read_file': { @@ -1859,7 +1966,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, const rel = getRelative(params.searchInFolder, accessor) if (rel) info.push(`Only search in ${rel}`) } - if (params.isRegex) { info.push(`Treat search as regex`) } + if (params.isRegex) { info.push(`Uses regex search`) } componentParams.info = info.join('; ') } @@ -1911,7 +2018,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, const infoarr: string[] = [] const uriStr = getRelative(params.uri, accessor) if (uriStr) infoarr.push(uriStr) - if (params.isRegex) infoarr.push('Treat search as regex') + if (params.isRegex) infoarr.push('Uses regex search') componentParams.info = infoarr.join('; ') if (toolMessage.type === 'success') { @@ -2071,84 +2178,14 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, return } }, + 'rewrite_file': { + resultWrapper: (params) => { + return + } + }, 'edit_file': { - resultWrapper: ({ toolMessage, messageIdx, threadId }) => { - const accessor = useAccessor() - const isError = toolMessage.type === 'tool_error' - const isRejected = toolMessage.type === 'rejected' - - const title = getTitle(toolMessage) - - const { desc1, desc1Info } = toolNameToDesc(toolMessage.name, toolMessage.params, accessor) - const icon = null - - const { rawParams, params } = toolMessage - const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, isRejected, } - - if (toolMessage.type === 'running_now' || toolMessage.type === 'tool_request') { - componentParams.children = - - - componentParams.desc2 = - } - else if (toolMessage.type === 'success' || toolMessage.type === 'rejected' || toolMessage.type === 'tool_error') { - // add apply box - if (params) { - const applyBoxId = getApplyBoxId({ - threadId: threadId, - messageIdx: messageIdx, - tokenIdx: 'N/A', - }) - - componentParams.desc2 = - } - - // add children - if (toolMessage.type !== 'tool_error') { - const { result } = toolMessage - - componentParams.bottomChildren = - - componentParams.children = - - - } - else { - // error - const { result } = toolMessage - if (params) { - componentParams.children = - {/* error */} - - {result} - - - {/* content */} - - - } - else { - componentParams.children = - {result} - - } - } - } - - return + resultWrapper: (params) => { + return } }, @@ -2165,7 +2202,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, return } }, - 'open_persistent_terminal': { + 'open_persistent_terminal': { resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() const terminalToolsService = accessor.get('ITerminalToolService') @@ -2674,17 +2711,18 @@ const CommandBarInChat = () => { } + const EditToolSoFar = ({ toolCallSoFar, }: { toolCallSoFar: RawToolCallObj }) => { - const uri = URI.file(toolCallSoFar.rawParams.uri ?? 'unknown') + const uri = toolCallSoFar.rawParams.uri ? URI.file(toolCallSoFar.rawParams.uri) : undefined - const title = titleOfToolName['edit_file'].proposed + const title = titleOfToolName[toolCallSoFar.name].proposed const uriDone = toolCallSoFar.doneParams.includes('uri') const desc1 = {uriDone ? getBasename(toolCallSoFar.rawParams['uri'] ?? 'unknown') - : `Generating`} + : `Running`} @@ -2692,11 +2730,11 @@ const EditToolSoFar = ({ toolCallSoFar, }: { toolCallSoFar: RawToolCallObj }) => return } + desc2={uri && } > @@ -2705,6 +2743,7 @@ const EditToolSoFar = ({ toolCallSoFar, }: { toolCallSoFar: RawToolCallObj }) => } + export const SidebarChat = () => { const textAreaRef = useRef(null) const textAreaFnsRef = useRef(null) @@ -2756,16 +2795,15 @@ export const SidebarChat = () => { const sidebarRef = useRef(null) const scrollContainerRef = useRef(null) + const onSubmit = useCallback(async (_forceSubmit?: string) => { - const onSubmit = useCallback(async () => { - - if (isDisabled) return + if (isDisabled && !_forceSubmit) return if (isRunning) return const threadId = chatThreadsService.state.currentThreadId // send message to LLM - const userMessage = textAreaRef.current?.value ?? '' + const userMessage = _forceSubmit || textAreaRef.current?.value || '' try { await chatThreadsService.addUserMessageAndStreamResponse({ userMessage, threadId }) @@ -2779,7 +2817,7 @@ export const SidebarChat = () => { }, [chatThreadsService, isDisabled, isRunning, textAreaRef, textAreaFnsRef, setSelections, settingsState]) - const onAbort = async () => { + const onAbort = async () => { const threadId = currentThread.id await chatThreadsService.abortRunning(threadId) } @@ -2835,7 +2873,7 @@ export const SidebarChat = () => { // the tool currently being generated const generatingTool = toolIsGenerating ? - toolCallSoFar.name === 'edit_file' ? @@ -2896,7 +2934,6 @@ export const SidebarChat = () => { - const inputChatArea = { onClickAnywhere={() => { textAreaRef.current?.focus() }} > { chatThreadsService.setCurrentlyFocusedMessageIdx(undefined) }} @@ -2926,6 +2964,26 @@ export const SidebarChat = () => { const isLandingPage = previousMessages.length === 0 + const initiallySuggestedPromptsHTML =
+ {[ + 'Summarize my codebase', + 'How do types work in Rust?', + 'Create a .voidrules file for me' + ].map((text, index) => ( +
onSubmit(text)} + > + {text} +
+ ))} +
+ + + console.log('!!!', Object.keys(chatThreadsState.allThreads).length) + + const threadPageInput =
@@ -2949,11 +3007,16 @@ export const SidebarChat = () => { {landingPageInput} - {Object.values(chatThreadsState.allThreads).length > 0 && // show if there are threads + {Object.keys(chatThreadsState.allThreads).length > 1 ? // show if there are threads
Previous Threads
+ : + +
Suggestions
+ {initiallySuggestedPromptsHTML} +
}
@@ -2994,6 +3057,3 @@ export const SidebarChat = () => { ) } - - - diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx index 6e96aa22..5e31c59b 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx @@ -176,8 +176,8 @@ export const PastThreadsList = ({ className = '' }: { className?: string }) => { return (
- {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 && (
setShowAll(true)} > Show {sortedThreadIds.length - numInitialThreads} more... @@ -207,7 +207,7 @@ export const PastThreadsList = ({ className = '' }: { className?: string }) => { )} {hasMoreThreads && showAll && (
setShowAll(false)} > Show less @@ -384,6 +384,8 @@ const PastThreadElement = ({ pastThread, idx, hoveredIdx, setHoveredIdx, isRunni null} {/* name */} {firstMsg} + + {`(${numMessages})`}
diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx index 69132cfd..9a01792f 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx @@ -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 = ({ ctor, prop return
{children}
} -type GenerateNextOptions = (newPathText: string) => Option[] +type GenerateNextOptions = (optionText: string) => Promise type Option = { - name: string, - displayName: string, + nameInMenu: string, + iconInMenu: ForwardRefExoticComponent & RefAttributes>, // 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, 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, 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, path: string[], optionText: string): Promise => { + + 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(); + + 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, 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, 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) => void; onChangeHeight?: (newHeight: number) => void; } -export const VoidInputBox2 = forwardRef(function X({ initValue, placeholder, multiline, fnsRef, className, onKeyDown, onFocus, onBlur, onChangeText }, ref) { +export const VoidInputBox2 = forwardRef(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(null) const selectedOptionRef = useRef(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([]); + // logic for @ to mention vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv + const [optionPath, setOptionPath] = useState([]); const [optionIdx, setOptionIdx] = useState(0); const [options, setOptions] = useState([]); - const [newPathText, setNewPathText] = useState(''); - - + const [optionText, setOptionText] = useState(''); const insertTextAtCursor = (text: string) => { const textarea = textAreaRef.current; if (!textarea) return; @@ -173,7 +363,7 @@ export const VoidInputBox2 = forwardRef(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(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(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) => { + + 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(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(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(fun inline: 'nearest', }); } - }, [optionIdx, isMenuOpen, newPathText, selectedOptionRef]); - - + }, [optionIdx, isMenuOpen, optionText, selectedOptionRef]); const measureRef = useRef(null); const gapPx = 2 @@ -307,7 +570,7 @@ export const VoidInputBox2 = forwardRef(fun } = useFloating({ open: isMenuOpen, onOpenChange: setIsMenuOpen, - placement: 'top', + placement: 'bottom', middleware: [ offset({ mainAxis: gapPx, crossAxis: offsetPx }), @@ -320,13 +583,9 @@ export const VoidInputBox2 = forwardRef(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(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(fun {isMenuOpen && (
(fun }} onWheel={(e) => e.stopPropagation()} > -
- {/* Path navigation breadcrumbs */} -
- {[...path, newPathText].join(' > ')} + {/* Breadcrumbs Header */} +
+ {optionPath.length || optionText ? +
+ {optionPath.map((path, index) => ( + + {path} + + + ))} + {optionText} +
+ :
Enter text to filter...
+ } +
+ + + {/* Options list */} +
+
+ {options.length === 0 ? +
No results found
+ : options.map((o, oIdx) => { + + return ( + // Option +
{ onSelectOption(); }} + onMouseOver={() => { setOptionIdx(oIdx) }} + > + {} + {o.nameInMenu} + {o.nextOptions || o.generateNextOptions ? ( + + ) : null} +
+ ) + }) + }
- - {/* Options list */} - {options.length === 0 ? ( -
No options available
- ) : ( - options.map((o, oIdx) => ( -
{ onSelectOption(); }} - > -
- {o.displayName} - {o.nextOptions || o.generateNextOptions ? ( - - - - ) : null} -
-
- )) - )}
)} diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx index 2c02e0cb..fdeddf76 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx @@ -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), diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/VoidOnboarding.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/VoidOnboarding.tsx index 87eafad0..81a76547 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/VoidOnboarding.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/VoidOnboarding.tsx @@ -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) => { + + // Create a new props object without the disabled attribute + const { disabled, ...buttonProps } = props; + return ( @@ -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) }} /> { voidSettingsService.setGlobalSetting('isOnboardingComplete', true); }} + onClick={() => { + voidSettingsService.setGlobalSetting('isOnboardingComplete', true); + voidMetricsService.capture('Completed Onboarding', { selectedProviderName, wantToUseOption }) + }} ringSize={voidSettingsState.globalSettings.isOnboardingComplete ? 'screen' : undefined} >Enter the Void
@@ -590,7 +604,7 @@ const VoidOnboardingContent = () => { {/* Slice of Void image */}
- + {!isLinux && }
@@ -618,15 +632,16 @@ const VoidOnboardingContent = () => {
+
diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx index 4359afa5..7e07184f 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx @@ -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 @@ -206,7 +206,7 @@ export const AddModelInputBox = ({ providerName: permanentProviderName, classNam const numModels = providerName === null ? 0 : settingsState.settingsOfProvider[providerName].models.length if (showCheckmark) { - return + return } if (!isOpen) { @@ -339,6 +339,13 @@ export const ModelDump = () => { : 'Disabled' ) + + const detailAboutModel = type === 'autodetected' ? + + : type === 'default' ? undefined + : + + return
{ {/* left part is width:full */}
{isNewProviderName ? providerTitle : ''} - {modelName} + {modelName}{detailAboutModel}
{/* right part is anything that fits */}
{ // : (isHidden ? `'${modelName}' won't appear in dropdowns` : ``) // } > - {type === 'autodetected' ? '(detected locally)' : type === 'default' ? '' : '(custom model)'} + + + {/* {type === 'autodetected' ? '(detected locally)' : type === 'default' ? '' : '(custom model)'} */} { - 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) } }); diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index ffd74a46..188bb658 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -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 diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index 61aeacc1..f08f41bc 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -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> = { [K in keyof T as SnakeCase>]: 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}`).join('\n') return `\ -${i + 1}. ${t.name} -Description: ${t.description} -Format: -<${t.name}>${!params ? '' : `\n${params}`} -` + ${i + 1}. ${t.name} + Description: ${t.description} + Format: + <${t.name}>${!params ? '' : `\n${params}`} + ` }).join('\n\n')}` } export const reParsedToolXMLString = (toolName: ToolName, toolParams: RawToolParamsObj) => { const params = Object.keys(toolParams).map(paramName => `<${paramName}>${toolParams[paramName as ToolParamName]}`).join('\n') return `\ -<${toolName}>${!params ? '' : `\n${params}`} -` + <${toolName}>${!params ? '' : `\n${params}`} + ` .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(', ')}` : ''} `) @@ -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]}` diff --git a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts index 63bfc997..b6c466f4 100644 --- a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts @@ -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': {}, diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts index 232bdfab..4a515e50 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts @@ -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