From b90973b195d7d533e698757b5ae641de61d1f297 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Commaret?= Date: Tue, 11 Feb 2025 11:36:49 +0100 Subject: [PATCH 001/173] Bump: voidVersion --- package.json | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index a4ee38bb..edf72507 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "name": "void-dev", "productName": "Void", + "voidVersion": "1.0.1", "version": "1.94.0", "distro": "this is a commit number if we want to publish on npm", "homepage": "https://voideditor.com", @@ -75,7 +76,10 @@ "extensions-ci": "node ./node_modules/gulp/bin/gulp.js extensions-ci", "extensions-ci-pr": "node ./node_modules/gulp/bin/gulp.js extensions-ci-pr", "perf": "node scripts/code-perf.js", - "update-build-ts-version": "npm install typescript@next && tsc -p ./build/tsconfig.build.json" + "update-build-ts-version": "npm install typescript@next && tsc -p ./build/tsconfig.build.json", + "void-version-patch": "node -e \"const p=require('./package.json');p.voidVersion=require('semver').inc(p.voidVersion,'patch');require('fs').writeFileSync('./package.json',JSON.stringify(p,null,2))\" && git add package.json && git commit -m \"Bump: voidVersion\" && git tag v$(node -e \"console.log(require('./package.json').voidVersion)\") && git push && git push --tags", + "void-version-minor": "node -e \"const p=require('./package.json');p.voidVersion=require('semver').inc(p.voidVersion,'minor');require('fs').writeFileSync('./package.json',JSON.stringify(p,null,2))\" && git add package.json && git commit -m \"Bump: voidVersion minor\" && git tag v$(node -e \"console.log(require('./package.json').voidVersion)\") && git push && git push --tags", + "void-version-major": "node -e \"const p=require('./package.json');p.voidVersion=require('semver').inc(p.voidVersion,'major');require('fs').writeFileSync('./package.json',JSON.stringify(p,null,2))\" && git add package.json && git commit -m \"Bump: voidVersion major\" && git tag v$(node -e \"console.log(require('./package.json').voidVersion)\") && git push && git push --tags" }, "dependencies": { "@anthropic-ai/sdk": "^0.32.1", @@ -273,4 +277,4 @@ "optionalDependencies": { "windows-foreground-love": "0.5.0" } -} +} \ No newline at end of file From 9b7fd0927860a4f01353721005794cef65232660 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Commaret?= Date: Tue, 11 Feb 2025 12:05:47 +0100 Subject: [PATCH 002/173] removed voidVersion from package.json --- package.json | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index edf72507..677a97b2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,6 @@ { "name": "void-dev", "productName": "Void", - "voidVersion": "1.0.1", "version": "1.94.0", "distro": "this is a commit number if we want to publish on npm", "homepage": "https://voideditor.com", @@ -77,9 +76,9 @@ "extensions-ci-pr": "node ./node_modules/gulp/bin/gulp.js extensions-ci-pr", "perf": "node scripts/code-perf.js", "update-build-ts-version": "npm install typescript@next && tsc -p ./build/tsconfig.build.json", - "void-version-patch": "node -e \"const p=require('./package.json');p.voidVersion=require('semver').inc(p.voidVersion,'patch');require('fs').writeFileSync('./package.json',JSON.stringify(p,null,2))\" && git add package.json && git commit -m \"Bump: voidVersion\" && git tag v$(node -e \"console.log(require('./package.json').voidVersion)\") && git push && git push --tags", - "void-version-minor": "node -e \"const p=require('./package.json');p.voidVersion=require('semver').inc(p.voidVersion,'minor');require('fs').writeFileSync('./package.json',JSON.stringify(p,null,2))\" && git add package.json && git commit -m \"Bump: voidVersion minor\" && git tag v$(node -e \"console.log(require('./package.json').voidVersion)\") && git push && git push --tags", - "void-version-major": "node -e \"const p=require('./package.json');p.voidVersion=require('semver').inc(p.voidVersion,'major');require('fs').writeFileSync('./package.json',JSON.stringify(p,null,2))\" && git add package.json && git commit -m \"Bump: voidVersion major\" && git tag v$(node -e \"console.log(require('./package.json').voidVersion)\") && git push && git push --tags" + "void-version-patch": "node -e \"const p=require('./product.json');p.voidVersion=require('semver').inc(p.voidVersion,'patch');require('fs').writeFileSync('./product.json',JSON.stringify(p,null,2))\" && git add product.json && git commit -m \"Bump: voidVersion\" && git tag v$(node -e \"console.log(require('./product.json').voidVersion)\") && git push && git push --tags", + "void-version-minor": "node -e \"const p=require('./product.json');p.voidVersion=require('semver').inc(p.voidVersion,'minor');require('fs').writeFileSync('./product.json',JSON.stringify(p,null,2))\" && git add product.json && git commit -m \"Bump: voidVersion minor\" && git tag v$(node -e \"console.log(require('./product.json').voidVersion)\") && git push && git push --tags", + "void-version-major": "node -e \"const p=require('./product.json');p.voidVersion=require('semver').inc(p.voidVersion,'major');require('fs').writeFileSync('./product.json',JSON.stringify(p,null,2))\" && git add product.json && git commit -m \"Bump: voidVersion major\" && git tag v$(node -e \"console.log(require('./product.json').voidVersion)\") && git push && git push --tags" }, "dependencies": { "@anthropic-ai/sdk": "^0.32.1", @@ -277,4 +276,4 @@ "optionalDependencies": { "windows-foreground-love": "0.5.0" } -} \ No newline at end of file +} From fbb4faacbcda18e84efc099db0efc398daf68814 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Commaret?= Date: Tue, 11 Feb 2025 12:06:24 +0100 Subject: [PATCH 003/173] added voidVersion to product.json --- product.json | 1 + 1 file changed, 1 insertion(+) diff --git a/product.json b/product.json index a79966a3..9b11879a 100644 --- a/product.json +++ b/product.json @@ -1,6 +1,7 @@ { "nameShort": "Void", "nameLong": "Void", + "voidVersion": "1.0.1", "applicationName": "void", "dataFolderName": ".void-editor", "win32MutexName": "voideditor", From ba606d229a8ce511e03c6e060a93c9e8cc985e02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Commaret?= Date: Tue, 11 Feb 2025 12:07:06 +0100 Subject: [PATCH 004/173] Added voidVersion to IProductConfiguration --- src/vs/base/common/product.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/vs/base/common/product.ts b/src/vs/base/common/product.ts index 1f58ce0f..3c2301c6 100644 --- a/src/vs/base/common/product.ts +++ b/src/vs/base/common/product.ts @@ -56,6 +56,7 @@ export type ExtensionVirtualWorkspaceSupport = { export interface IProductConfiguration { readonly version: string; + readonly voidVersion: string; readonly date?: string; readonly quality?: string; readonly commit?: string; From 014c87bc3ec66423ea618da2929a2988d2beab84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Commaret?= Date: Tue, 11 Feb 2025 12:08:03 +0100 Subject: [PATCH 005/173] Fetch and display : voidVersion into dialog handler --- .../workbench/electron-sandbox/parts/dialogs/dialogHandler.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/electron-sandbox/parts/dialogs/dialogHandler.ts b/src/vs/workbench/electron-sandbox/parts/dialogs/dialogHandler.ts index 6c61a590..4372d416 100644 --- a/src/vs/workbench/electron-sandbox/parts/dialogs/dialogHandler.ts +++ b/src/vs/workbench/electron-sandbox/parts/dialogs/dialogHandler.ts @@ -71,6 +71,7 @@ export class NativeDialogHandler extends AbstractDialogHandler { async about(): Promise { let version = this.productService.version; + let voidVersion = this.productService.voidVersion || 'Unknown'; if (this.productService.target) { version = `${version} (${this.productService.target} setup)`; } else if (this.productService.darwinUniversalAssetId) { @@ -81,7 +82,8 @@ export class NativeDialogHandler extends AbstractDialogHandler { const detailString = (useAgo: boolean): string => { return localize({ key: 'aboutDetail', comment: ['Electron, Chromium, Node.js and V8 are product names that need no translation'] }, - "Version: {0}\nCommit: {1}\nDate: {2}\nElectron: {3}\nElectronBuildId: {4}\nChromium: {5}\nNode.js: {6}\nV8: {7}\nOS: {8}", + "Void : {0}\nVSCode Version: {1}\nCommit: {2}\nDate: {3}\nElectron: {4}\nElectronBuildId: {5}\nChromium: {6}\nNode.js: {7}\nV8: {8}\nOS: {9}", + voidVersion, version, this.productService.commit || 'Unknown', this.productService.date ? `${this.productService.date}${useAgo ? ' (' + fromNow(new Date(this.productService.date), true) + ')' : ''}` : 'Unknown', From db69464ddc371b8149eb454e711ce49a9a854793 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Commaret?= Date: Tue, 11 Feb 2025 12:08:53 +0100 Subject: [PATCH 006/173] Bump: voidVersion --- product.json | 76 ++++++++++++++++++++++++++-------------------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/product.json b/product.json index 9b11879a..bea28466 100644 --- a/product.json +++ b/product.json @@ -1,39 +1,39 @@ { - "nameShort": "Void", - "nameLong": "Void", - "voidVersion": "1.0.1", - "applicationName": "void", - "dataFolderName": ".void-editor", - "win32MutexName": "voideditor", - "licenseName": "MIT", - "licenseUrl": "https://github.com/voideditor/void/blob/main/LICENSE.txt", - "serverLicenseUrl": "https://github.com/voideditor/void/blob/main/LICENSE.txt", - "serverGreeting": [], - "serverLicense": [], - "serverLicensePrompt": "", - "serverApplicationName": "void-server", - "serverDataFolderName": ".void-server", - "tunnelApplicationName": "void-tunnel", - "win32DirName": "Void", - "win32NameVersion": "Void", - "win32RegValueName": "VoidEditor", - "win32x64AppId": "{{9D394D01-1728-45A7-B997-A6C82C5452C3}", - "win32arm64AppId": "{{0668DD58-2BDE-4101-8CDA-40252DF8875D}", - "win32x64UserAppId": "{{8BED5DC1-6C55-46E6-9FE6-18F7E6F7C7F1}", - "win32arm64UserAppId": "{{F6C87466-BC82-4A8F-B0FF-18CA366BA4D8}", - "win32AppUserModelId": "Void.Editor", - "win32ShellNameShort": "V&oid", - "win32TunnelServiceMutex": "void-tunnelservice", - "win32TunnelMutex": "void-tunnel", - "darwinBundleIdentifier": "com.voideditor.code", - "linuxIconName": "void-editor", - "licenseFileName": "LICENSE.txt", - "reportIssueUrl": "https://github.com/voideditor/void/issues/new", - "nodejsRepository": "https://nodejs.org", - "urlProtocol": "void-editor", - "extensionsGallery": { - "serviceUrl": "https://open-vsx.org/vscode/gallery", - "itemUrl": "https://open-vsx.org/vscode/item" - }, - "builtInExtensions": [] -} + "nameShort": "Void", + "nameLong": "Void", + "voidVersion": "1.0.2", + "applicationName": "void", + "dataFolderName": ".void-editor", + "win32MutexName": "voideditor", + "licenseName": "MIT", + "licenseUrl": "https://github.com/voideditor/void/blob/main/LICENSE.txt", + "serverLicenseUrl": "https://github.com/voideditor/void/blob/main/LICENSE.txt", + "serverGreeting": [], + "serverLicense": [], + "serverLicensePrompt": "", + "serverApplicationName": "void-server", + "serverDataFolderName": ".void-server", + "tunnelApplicationName": "void-tunnel", + "win32DirName": "Void", + "win32NameVersion": "Void", + "win32RegValueName": "VoidEditor", + "win32x64AppId": "{{9D394D01-1728-45A7-B997-A6C82C5452C3}", + "win32arm64AppId": "{{0668DD58-2BDE-4101-8CDA-40252DF8875D}", + "win32x64UserAppId": "{{8BED5DC1-6C55-46E6-9FE6-18F7E6F7C7F1}", + "win32arm64UserAppId": "{{F6C87466-BC82-4A8F-B0FF-18CA366BA4D8}", + "win32AppUserModelId": "Void.Editor", + "win32ShellNameShort": "V&oid", + "win32TunnelServiceMutex": "void-tunnelservice", + "win32TunnelMutex": "void-tunnel", + "darwinBundleIdentifier": "com.voideditor.code", + "linuxIconName": "void-editor", + "licenseFileName": "LICENSE.txt", + "reportIssueUrl": "https://github.com/voideditor/void/issues/new", + "nodejsRepository": "https://nodejs.org", + "urlProtocol": "void-editor", + "extensionsGallery": { + "serviceUrl": "https://open-vsx.org/vscode/gallery", + "itemUrl": "https://open-vsx.org/vscode/item" + }, + "builtInExtensions": [] +} \ No newline at end of file From e3c826b2531c5be87f37071b0af763c5c2dd658d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Commaret?= Date: Tue, 11 Feb 2025 12:56:53 +0100 Subject: [PATCH 007/173] Bump: voidVersion --- product.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/product.json b/product.json index bea28466..5e730b77 100644 --- a/product.json +++ b/product.json @@ -1,7 +1,9 @@ { "nameShort": "Void", "nameLong": "Void", - "voidVersion": "1.0.2", + "voidVersion": "1.0.3", + "commit": "db69464ddc371b8149eb454e711ce49a9a854793", + "date": "2025-02-11", "applicationName": "void", "dataFolderName": ".void-editor", "win32MutexName": "voideditor", From 7482e2aa823ffde1604a9db862b30f743b9b5538 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Commaret?= Date: Tue, 11 Feb 2025 13:00:23 +0100 Subject: [PATCH 008/173] version : tag, commit, and date from iProductConfiguration --- package.json | 6 +++--- scripts/update-version.js | 27 +++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 scripts/update-version.js diff --git a/package.json b/package.json index 677a97b2..ac9d4135 100644 --- a/package.json +++ b/package.json @@ -76,9 +76,9 @@ "extensions-ci-pr": "node ./node_modules/gulp/bin/gulp.js extensions-ci-pr", "perf": "node scripts/code-perf.js", "update-build-ts-version": "npm install typescript@next && tsc -p ./build/tsconfig.build.json", - "void-version-patch": "node -e \"const p=require('./product.json');p.voidVersion=require('semver').inc(p.voidVersion,'patch');require('fs').writeFileSync('./product.json',JSON.stringify(p,null,2))\" && git add product.json && git commit -m \"Bump: voidVersion\" && git tag v$(node -e \"console.log(require('./product.json').voidVersion)\") && git push && git push --tags", - "void-version-minor": "node -e \"const p=require('./product.json');p.voidVersion=require('semver').inc(p.voidVersion,'minor');require('fs').writeFileSync('./product.json',JSON.stringify(p,null,2))\" && git add product.json && git commit -m \"Bump: voidVersion minor\" && git tag v$(node -e \"console.log(require('./product.json').voidVersion)\") && git push && git push --tags", - "void-version-major": "node -e \"const p=require('./product.json');p.voidVersion=require('semver').inc(p.voidVersion,'major');require('fs').writeFileSync('./product.json',JSON.stringify(p,null,2))\" && git add product.json && git commit -m \"Bump: voidVersion major\" && git tag v$(node -e \"console.log(require('./product.json').voidVersion)\") && git push && git push --tags" + "void-version-patch": "node scripts/update-version.js patch && git add product.json && git commit -m \"Bump: voidVersion\" && git tag v$(node -e \"console.log(require('./product.json').voidVersion)\") && git push && git push --tags", + "void-version-minor": "node scripts/update-version.js minor && git add product.json && git commit -m \"Bump: voidVersion minor\" && git tag v$(node -e \"console.log(require('./product.json').voidVersion)\") && git push && git push --tags", + "void-version-major": "node scripts/update-version.js major && git add product.json && git commit -m \"Bump: voidVersion major\" && git tag v$(node -e \"console.log(require('./product.json').voidVersion)\") && git push && git push --tags" }, "dependencies": { "@anthropic-ai/sdk": "^0.32.1", diff --git a/scripts/update-version.js b/scripts/update-version.js new file mode 100644 index 00000000..a8542ad3 --- /dev/null +++ b/scripts/update-version.js @@ -0,0 +1,27 @@ +const fs = require('fs'); +const { execSync } = require('child_process'); +const semver = require('semver'); + +function updateProductJson(type = 'patch') { + // Read product.json + const productJsonPath = './product.json'; + const product = require('../' + productJsonPath); + + // Update the version + product.voidVersion = semver.inc(product.voidVersion, type); + + // Update the commit hash + product.commit = execSync('git rev-parse HEAD').toString().trim(); + + // Update the date + product.date = new Date().toISOString().split('T')[0]; + + // Write the modifications + fs.writeFileSync(productJsonPath, JSON.stringify(product, null, 2)); + + return product.voidVersion; +} + +// Execute the update +const newVersion = updateProductJson(process.argv[2] || 'patch'); +console.log(`Updated version: ${newVersion}`); From 3b09174587da1aa53cc5d65e3ea5a5ef78efc175 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Commaret?= Date: Tue, 11 Feb 2025 14:19:16 +0100 Subject: [PATCH 009/173] bad hash and date, will be updated --- product.json | 80 ++++++++++++++++++++++++++-------------------------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/product.json b/product.json index 5e730b77..7361886c 100644 --- a/product.json +++ b/product.json @@ -1,41 +1,41 @@ { - "nameShort": "Void", - "nameLong": "Void", - "voidVersion": "1.0.3", - "commit": "db69464ddc371b8149eb454e711ce49a9a854793", - "date": "2025-02-11", - "applicationName": "void", - "dataFolderName": ".void-editor", - "win32MutexName": "voideditor", - "licenseName": "MIT", - "licenseUrl": "https://github.com/voideditor/void/blob/main/LICENSE.txt", - "serverLicenseUrl": "https://github.com/voideditor/void/blob/main/LICENSE.txt", - "serverGreeting": [], - "serverLicense": [], - "serverLicensePrompt": "", - "serverApplicationName": "void-server", - "serverDataFolderName": ".void-server", - "tunnelApplicationName": "void-tunnel", - "win32DirName": "Void", - "win32NameVersion": "Void", - "win32RegValueName": "VoidEditor", - "win32x64AppId": "{{9D394D01-1728-45A7-B997-A6C82C5452C3}", - "win32arm64AppId": "{{0668DD58-2BDE-4101-8CDA-40252DF8875D}", - "win32x64UserAppId": "{{8BED5DC1-6C55-46E6-9FE6-18F7E6F7C7F1}", - "win32arm64UserAppId": "{{F6C87466-BC82-4A8F-B0FF-18CA366BA4D8}", - "win32AppUserModelId": "Void.Editor", - "win32ShellNameShort": "V&oid", - "win32TunnelServiceMutex": "void-tunnelservice", - "win32TunnelMutex": "void-tunnel", - "darwinBundleIdentifier": "com.voideditor.code", - "linuxIconName": "void-editor", - "licenseFileName": "LICENSE.txt", - "reportIssueUrl": "https://github.com/voideditor/void/issues/new", - "nodejsRepository": "https://nodejs.org", - "urlProtocol": "void-editor", - "extensionsGallery": { - "serviceUrl": "https://open-vsx.org/vscode/gallery", - "itemUrl": "https://open-vsx.org/vscode/item" - }, - "builtInExtensions": [] -} \ No newline at end of file + "nameShort": "Void", + "nameLong": "Void", + "voidVersion": "1.0.0", + "commit": "db69464ddc371b8149 eb454e711ce49a9a854793", + "date": "2025-02-11", + "applicationName": "void", + "dataFolderName": ".void-editor", + "win32MutexName": "voideditor", + "licenseName": "MIT", + "licenseUrl": "https://github.com/voideditor/void/blob/main/LICENSE.txt", + "serverLicenseUrl": "https://github.com/voideditor/void/blob/main/LICENSE.txt", + "serverGreeting": [], + "serverLicense": [], + "serverLicensePrompt": "", + "serverApplicationName": "void-server", + "serverDataFolderName": ".void-server", + "tunnelApplicationName": "void-tunnel", + "win32DirName": "Void", + "win32NameVersion": "Void", + "win32RegValueName": "VoidEditor", + "win32x64AppId": "{{9D394D01-1728-45A7-B997-A6C82C5452C3}", + "win32arm64AppId": "{{0668DD58-2BDE-4101-8CDA-40252DF8875D}", + "win32x64UserAppId": "{{8BED5DC1-6C55-46E6-9FE6-18F7E6F7C7F1}", + "win32arm64UserAppId": "{{F6C87466-BC82-4A8F-B0FF-18CA366BA4D8}", + "win32AppUserModelId": "Void.Editor", + "win32ShellNameShort": "V&oid", + "win32TunnelServiceMutex": "void-tunnelservice", + "win32TunnelMutex": "void-tunnel", + "darwinBundleIdentifier": "com.voideditor.code", + "linuxIconName": "void-editor", + "licenseFileName": "LICENSE.txt", + "reportIssueUrl": "https://github.com/voideditor/void/issues/new", + "nodejsRepository": "https://nodejs.org", + "urlProtocol": "void-editor", + "extensionsGallery": { + "serviceUrl": "https://open-vsx.org/vscode/gallery", + "itemUrl": "https://open-vsx.org/vscode/item" + }, + "builtInExtensions": [] +} From ba15b3166152eeea1370573660d700210167102f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Commaret?= Date: Tue, 11 Feb 2025 14:19:38 +0100 Subject: [PATCH 010/173] fix --- product.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/product.json b/product.json index 7361886c..6faa4ab8 100644 --- a/product.json +++ b/product.json @@ -2,7 +2,7 @@ "nameShort": "Void", "nameLong": "Void", "voidVersion": "1.0.0", - "commit": "db69464ddc371b8149 eb454e711ce49a9a854793", + "commit": "db69464ddc371b8149eb454e711ce49a9a854793", "date": "2025-02-11", "applicationName": "void", "dataFolderName": ".void-editor", From 7bceceef85f29e840f6379e7868cabdb9a8e9d8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Commaret?= Date: Tue, 11 Feb 2025 14:21:02 +0100 Subject: [PATCH 011/173] Bump: voidVersion --- product.json | 80 ++++++++++++++++++++++++++-------------------------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/product.json b/product.json index 6faa4ab8..445ee82c 100644 --- a/product.json +++ b/product.json @@ -1,41 +1,41 @@ { - "nameShort": "Void", - "nameLong": "Void", - "voidVersion": "1.0.0", - "commit": "db69464ddc371b8149eb454e711ce49a9a854793", - "date": "2025-02-11", - "applicationName": "void", - "dataFolderName": ".void-editor", - "win32MutexName": "voideditor", - "licenseName": "MIT", - "licenseUrl": "https://github.com/voideditor/void/blob/main/LICENSE.txt", - "serverLicenseUrl": "https://github.com/voideditor/void/blob/main/LICENSE.txt", - "serverGreeting": [], - "serverLicense": [], - "serverLicensePrompt": "", - "serverApplicationName": "void-server", - "serverDataFolderName": ".void-server", - "tunnelApplicationName": "void-tunnel", - "win32DirName": "Void", - "win32NameVersion": "Void", - "win32RegValueName": "VoidEditor", - "win32x64AppId": "{{9D394D01-1728-45A7-B997-A6C82C5452C3}", - "win32arm64AppId": "{{0668DD58-2BDE-4101-8CDA-40252DF8875D}", - "win32x64UserAppId": "{{8BED5DC1-6C55-46E6-9FE6-18F7E6F7C7F1}", - "win32arm64UserAppId": "{{F6C87466-BC82-4A8F-B0FF-18CA366BA4D8}", - "win32AppUserModelId": "Void.Editor", - "win32ShellNameShort": "V&oid", - "win32TunnelServiceMutex": "void-tunnelservice", - "win32TunnelMutex": "void-tunnel", - "darwinBundleIdentifier": "com.voideditor.code", - "linuxIconName": "void-editor", - "licenseFileName": "LICENSE.txt", - "reportIssueUrl": "https://github.com/voideditor/void/issues/new", - "nodejsRepository": "https://nodejs.org", - "urlProtocol": "void-editor", - "extensionsGallery": { - "serviceUrl": "https://open-vsx.org/vscode/gallery", - "itemUrl": "https://open-vsx.org/vscode/item" - }, - "builtInExtensions": [] -} + "nameShort": "Void", + "nameLong": "Void", + "voidVersion": "1.0.1", + "commit": "ba15b3166152eeea1370573660d700210167102f", + "date": "2025-02-11", + "applicationName": "void", + "dataFolderName": ".void-editor", + "win32MutexName": "voideditor", + "licenseName": "MIT", + "licenseUrl": "https://github.com/voideditor/void/blob/main/LICENSE.txt", + "serverLicenseUrl": "https://github.com/voideditor/void/blob/main/LICENSE.txt", + "serverGreeting": [], + "serverLicense": [], + "serverLicensePrompt": "", + "serverApplicationName": "void-server", + "serverDataFolderName": ".void-server", + "tunnelApplicationName": "void-tunnel", + "win32DirName": "Void", + "win32NameVersion": "Void", + "win32RegValueName": "VoidEditor", + "win32x64AppId": "{{9D394D01-1728-45A7-B997-A6C82C5452C3}", + "win32arm64AppId": "{{0668DD58-2BDE-4101-8CDA-40252DF8875D}", + "win32x64UserAppId": "{{8BED5DC1-6C55-46E6-9FE6-18F7E6F7C7F1}", + "win32arm64UserAppId": "{{F6C87466-BC82-4A8F-B0FF-18CA366BA4D8}", + "win32AppUserModelId": "Void.Editor", + "win32ShellNameShort": "V&oid", + "win32TunnelServiceMutex": "void-tunnelservice", + "win32TunnelMutex": "void-tunnel", + "darwinBundleIdentifier": "com.voideditor.code", + "linuxIconName": "void-editor", + "licenseFileName": "LICENSE.txt", + "reportIssueUrl": "https://github.com/voideditor/void/issues/new", + "nodejsRepository": "https://nodejs.org", + "urlProtocol": "void-editor", + "extensionsGallery": { + "serviceUrl": "https://open-vsx.org/vscode/gallery", + "itemUrl": "https://open-vsx.org/vscode/item" + }, + "builtInExtensions": [] +} \ No newline at end of file From 12c686c102734afae7165781d039b35a50f2d340 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Commaret?= Date: Tue, 11 Feb 2025 14:43:30 +0100 Subject: [PATCH 012/173] Bump: voidVersion --- product.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/product.json b/product.json index 445ee82c..e96a1cf3 100644 --- a/product.json +++ b/product.json @@ -1,8 +1,8 @@ { "nameShort": "Void", "nameLong": "Void", - "voidVersion": "1.0.1", - "commit": "ba15b3166152eeea1370573660d700210167102f", + "voidVersion": "1.0.2", + "commit": "7bceceef85f29e840f6379e7868cabdb9a8e9d8b", "date": "2025-02-11", "applicationName": "void", "dataFolderName": ".void-editor", From ac2fb9e36de02cf3f24837df94475c6cf3adfdf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Commaret?= Date: Fri, 7 Mar 2025 14:30:33 +0100 Subject: [PATCH 013/173] Mistral : Added, not fim --- .../contrib/void/common/modelCapabilities.ts | 27 +++++++++++-------- .../contrib/void/common/voidSettingsTypes.ts | 22 ++++++++++++--- .../llmMessage/sendLLMMessage.impl.ts | 9 +++++++ 3 files changed, 44 insertions(+), 14 deletions(-) diff --git a/src/vs/workbench/contrib/void/common/modelCapabilities.ts b/src/vs/workbench/contrib/void/common/modelCapabilities.ts index 13173b50..a66ff08e 100644 --- a/src/vs/workbench/contrib/void/common/modelCapabilities.ts +++ b/src/vs/workbench/contrib/void/common/modelCapabilities.ts @@ -51,13 +51,12 @@ export const defaultModelsOfProvider = { 'llama-3.1-8b-instant', // 'qwen-2.5-coder-32b', // preview mode (experimental) ], - // not supporting mistral right now- it's last on Void usage, and a huge pain to set up since it's nonstandard (it supports codestral FIM but it's on v1/fim/completions, etc) - // mistral: [ // https://docs.mistral.ai/getting-started/models/models_overview/ - // 'codestral-latest', - // 'mistral-large-latest', - // 'ministral-3b-latest', - // 'ministral-8b-latest', - // ], + mistral: [ // https://docs.mistral.ai/getting-started/models/models_overview/ + 'codestral-latest', + 'mistral-large-latest', + 'ministral-3b-latest', + 'ministral-8b-latest', + ], openAICompatible: [], // fallback } as const satisfies Record @@ -135,7 +134,7 @@ const openSourceModelOptions_assumingOAICompat = { supportsTools: false, supportsReasoning: false, }, - 'codestral': { + 'mistral': { supportsFIM: true, supportsSystemMessage: 'system-role', supportsTools: 'openai-style', @@ -214,7 +213,7 @@ const extensiveModelFallback: ProviderSettings['modelOptionsFallback'] = (modelN if (modelName.includes('deepseek')) return toFallback({ ...openSourceModelOptions_assumingOAICompat.deepseekCoderV2, contextWindow: 32_000, maxOutputTokens: 4_096, }) if (modelName.includes('llama3')) return toFallback({ ...openSourceModelOptions_assumingOAICompat.llama3, contextWindow: 32_000, maxOutputTokens: 4_096, }) if (modelName.includes('qwen') && modelName.includes('2.5') && modelName.includes('coder')) return toFallback({ ...openSourceModelOptions_assumingOAICompat['qwen2.5coder'], contextWindow: 32_000, maxOutputTokens: 4_096, }) - if (modelName.includes('codestral')) return toFallback({ ...openSourceModelOptions_assumingOAICompat.codestral, contextWindow: 32_000, maxOutputTokens: 4_096, }) + if (modelName.includes('mistral')) return toFallback({ ...openSourceModelOptions_assumingOAICompat.mistral, contextWindow: 32_000, maxOutputTokens: 4_096, }) if (/\bo1\b/.test(modelName) || /\bo3\b/.test(modelName)) return toFallback(openAIModelOptions['o1']) return toFallback(modelOptionsDefaults) } @@ -555,7 +554,7 @@ const openRouterModelOptions_assumingOpenAICompat = { supportsReasoning: false, }, 'mistralai/codestral-2501': { - ...openSourceModelOptions_assumingOAICompat.codestral, + ...openSourceModelOptions_assumingOAICompat.mistral, contextWindow: 256_000, maxOutputTokens: null, cost: { input: 0.3, output: 0.9 }, @@ -590,6 +589,12 @@ const openRouterSettings: ProviderSettings = { } +const mistralSettings: ProviderSettings = { + ...openSourceModelOptions_assumingOAICompat.mistral, + modelOptions: {}, + modelOptionsFallback: (modelName) => extensiveModelFallback(modelName), +} + // ---------------- model settings of everything above ---------------- @@ -599,7 +604,7 @@ const modelSettingsOfProvider: { [providerName in ProviderName]: ProviderSetting anthropic: anthropicSettings, xAI: xAISettings, gemini: geminiSettings, - + mistral: mistralSettings, // open source models deepseek: deepseekSettings, groq: groqSettings, diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index bda7e3ce..c95b5ecc 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -43,6 +43,9 @@ export const defaultProviderSettings = { xAI: { apiKey: '' }, + mistral: { + apiKey: '' + }, } as const @@ -144,6 +147,11 @@ export const displayInfoOfProviderName = (providerName: ProviderName): DisplayIn title: 'Grok (xAI)', } } + else if (providerName === 'mistral') { + return { + title: 'Mistral.ai API', + } + } throw new Error(`descOfProviderName: Unknown provider name: "${providerName}"`) @@ -170,7 +178,8 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName providerName === 'groq' ? 'gsk_key...' : providerName === 'openAICompatible' ? 'sk-key...' : providerName === 'xAI' ? 'xai-key...' : - '', + providerName === 'mistral' ? 'key...' : + '', subTextMd: providerName === 'anthropic' ? 'Get your [API Key here](https://console.anthropic.com/settings/keys).' : providerName === 'openAI' ? 'Get your [API Key here](https://platform.openai.com/api-keys).' : @@ -179,8 +188,9 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName providerName === 'gemini' ? 'Get your [API Key here](https://aistudio.google.com/apikey).' : providerName === 'groq' ? 'Get your [API Key here](https://console.groq.com/keys).' : providerName === 'xAI' ? 'Get your [API Key here](https://console.x.ai).' : - providerName === 'openAICompatible' ? undefined : - '', + providerName === 'mistral' ? 'Get your [API Key here](https://console.mistral.ai/api-keys).' : + providerName === 'openAICompatible' ? undefined : + '', isPasswordField: true, } } @@ -300,6 +310,12 @@ export const defaultSettingsOfProvider: SettingsOfProvider = { ...modelInfoOfDefaultModelNames(defaultModelsOfProvider.vLLM), _didFillInProviderSettings: undefined, }, + mistral: { // aggregator + ...defaultCustomSettings, + ...defaultProviderSettings.mistral, + ...modelInfoOfDefaultModelNames(defaultModelsOfProvider.mistral), + _didFillInProviderSettings: undefined, + }, } 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 f58eae38..a9c45b76 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 @@ -112,6 +112,10 @@ const newOpenAICompatibleSDK = ({ settingsOfProvider, providerName, includeInPay const thisConfig = settingsOfProvider[providerName] return new OpenAI({ baseURL: 'https://api.x.ai/v1', apiKey: thisConfig.apiKey, ...commonPayloadOpts }) } + else if (providerName === 'mistral') { + const thisConfig = settingsOfProvider[providerName] + return new OpenAI({ baseURL: 'https://api.mistral.ai/v1', apiKey: thisConfig.apiKey, ...commonPayloadOpts }) + } else throw new Error(`Void providerName was invalid: ${providerName}.`) } @@ -524,6 +528,11 @@ export const sendLLMMessageToProviderImplementation = { sendFIM: null, list: null, }, + mistral: { + sendChat: (params) => _sendOpenAICompatibleChat(params), + sendFIM: null, + list: null, + }, } satisfies CallFnOfProvider From bccc6a64ef6ee9054916afe65aafa974a389522f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Commaret?= Date: Sat, 8 Mar 2025 08:23:20 +0100 Subject: [PATCH 014/173] Mistral FIM maybe --- package.json | 1 - src/vs/workbench/contrib/void/common/modelCapabilities.ts | 1 + .../void/electron-main/llmMessage/sendLLMMessage.impl.ts | 4 ++-- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 52387a3e..921a4a1f 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,6 @@ "@google/generative-ai": "^0.22.0", "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", - "@mistralai/mistralai": "^1.5.0", "@parcel/watcher": "2.5.1", "@types/semver": "^7.5.8", "@vscode/deviceid": "^0.1.1", diff --git a/src/vs/workbench/contrib/void/common/modelCapabilities.ts b/src/vs/workbench/contrib/void/common/modelCapabilities.ts index a66ff08e..d6fbd390 100644 --- a/src/vs/workbench/contrib/void/common/modelCapabilities.ts +++ b/src/vs/workbench/contrib/void/common/modelCapabilities.ts @@ -56,6 +56,7 @@ export const defaultModelsOfProvider = { 'mistral-large-latest', 'ministral-3b-latest', 'ministral-8b-latest', + '' ], openAICompatible: [], // fallback } as const satisfies Record 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 a9c45b76..9b07807e 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 @@ -530,8 +530,8 @@ export const sendLLMMessageToProviderImplementation = { }, mistral: { sendChat: (params) => _sendOpenAICompatibleChat(params), - sendFIM: null, - list: null, + sendFIM: (params) => _sendOpenAICompatibleFIM(params), + list: (params) => _openaiCompatibleList(params), }, } satisfies CallFnOfProvider From ec4c52aff9798b34f9efaa2fb796645bf3de9fc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Commaret?= Date: Sat, 8 Mar 2025 08:23:40 +0100 Subject: [PATCH 015/173] remove package --- package-lock.json | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4a403917..f5127fb3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,6 @@ "@google/generative-ai": "^0.22.0", "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", - "@mistralai/mistralai": "^1.5.0", "@parcel/watcher": "2.5.1", "@types/semver": "^7.5.8", "@vscode/deviceid": "^0.1.1", @@ -2539,17 +2538,6 @@ "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-1.1.9.tgz", "integrity": "sha512-n1VPsljTSkthsAFYdiWfC+DKzK2WwcRp83Y1YAqdX552BstvsDjft9YXppjUzp11BPsapDoO1LDgrDB0XVsfNQ==" }, - "node_modules/@mistralai/mistralai": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-1.5.0.tgz", - "integrity": "sha512-AIn8pwAwA/fDvEUvmkt+40zH1ZmfaG3Q7oUWl17GUEC1tU7ZPwYz8Cv9P59lyS1SisHdDSu81oknO7f1ywkz8Q==", - "dependencies": { - "zod-to-json-schema": "^3.24.1" - }, - "peerDependencies": { - "zod": ">= 3" - } - }, "node_modules/@next/env": { "version": "15.2.0", "resolved": "https://registry.npmjs.org/@next/env/-/env-15.2.0.tgz", @@ -23157,15 +23145,6 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } - }, - "node_modules/zod-to-json-schema": { - "version": "3.24.3", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.3.tgz", - "integrity": "sha512-HIAfWdYIt1sssHfYZFCXp4rU1w2r8hVVXYIlmoa0r0gABLs5di3RCqPU5DDROogVz1pAdYBaz7HK5n9pSUNs3A==", - "license": "ISC", - "peerDependencies": { - "zod": "^3.24.1" - } } } } From 4a9c0c864f0762f914c02403830e52ae229a0939 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Fri, 7 Mar 2025 19:18:47 -0800 Subject: [PATCH 016/173] links draft 1 --- .../contrib/void/browser/chatThreadService.ts | 251 +++++++++++++++++- .../react/src/markdown/ChatMarkdownRender.tsx | 125 ++++++++- .../react/src/sidebar-tsx/SidebarChat.tsx | 76 +++--- .../react/src/void-settings-tsx/Settings.tsx | 12 +- 4 files changed, 405 insertions(+), 59 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index f02f9e2c..205568d4 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -21,7 +21,11 @@ import { generateUuid } from '../../../../base/common/uuid.js'; import { getErrorMessage } from '../../../../base/common/errors.js'; import { ChatMode, FeatureName } from '../common/voidSettingsTypes.js'; import { IVoidSettingsService } from '../common/voidSettingsService.js'; - +import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js'; +import { ITextModelService } from '../../../../editor/common/services/resolverService.js'; +import { LocationLink, SymbolKind } from '../../../../editor/common/languages.js'; +import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { Position } from '../../../../editor/common/core/position.js'; const findLastIndex = (arr: T[], condition: (t: T) => boolean): number => { for (let i = arr.length - 1; i >= 0; i--) { @@ -78,6 +82,15 @@ export type FileSelection = { export type StagingSelectionItem = CodeSelection | FileSelection +export type CodespanLocationLink = { + uri: URI, // we handle serialization for this + selection?: { // store as JSON so dont have to worry about serialization + startLineNumber: number + startColumn: number, + endLineNumber: number + endColumn: number, + } | undefined +} | null export type ToolMessage = { role: 'tool'; @@ -133,6 +146,13 @@ export type ChatThreads = { state: { stagingSelections: StagingSelectionItem[]; focusedMessageIdx: number | undefined; // index of the message that is being edited (undefined if none) + + linksOfMessageIdx: { // eg. link = linksOfMessageIdx[4]['RangeFunction'] + [messageIdx: number]: { + [codespanName: string]: CodespanLocationLink + } + } + isCheckedOfSelectionId: { [selectionId: string]: boolean }; // TODO } }; @@ -143,7 +163,8 @@ type ThreadType = ChatThreads[string] const defaultThreadState: ThreadType['state'] = { stagingSelections: [], focusedMessageIdx: undefined, - isCheckedOfSelectionId: {} + isCheckedOfSelectionId: {}, + linksOfMessageIdx: {}, } export type ThreadsState = { @@ -199,12 +220,19 @@ export interface IChatThreadService { isFocusingMessage(): boolean; setFocusedMessageIdx(messageIdx: number | undefined): void; + + + getCodespanLink({ codespanStr, messageIdx, threadId }: { codespanStr: string, messageIdx: number, threadId: string }): CodespanLocationLink | undefined; + addCodespanLink({ newLinkText, newLinkLocation, messageIdx, threadId }: { newLinkText: string, newLinkLocation: CodespanLocationLink, messageIdx: number, threadId: string }): void; + generateCodespanLink(codespanStr: string): Promise + // exposed getters/setters getCurrentMessageState: (messageIdx: number) => UserMessageState setCurrentMessageState: (messageIdx: number, newState: Partial) => void getCurrentThreadState: () => ThreadType['state'] setCurrentThreadState: (newState: Partial) => void + closeStagingSelectionsInCurrentThread(): void; closeStagingSelectionsInMessage(messageIdx: number): void; @@ -243,6 +271,8 @@ class ChatThreadService extends Disposable implements IChatThreadService { @IToolsService private readonly _toolsService: IToolsService, @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, @IVoidSettingsService private readonly _settingsService: IVoidSettingsService, + @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, + @ITextModelService private readonly _textModelService: ITextModelService, ) { super() this.state = { allThreads: {}, currentThreadId: null as unknown as string } // default state @@ -260,6 +290,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { } // !!! this is important for properly restoring URIs from storage + // should probably re-use code from void/src/vs/base/common/marshalling.ts instead. but this is simple enough private _convertThreadDataFromStorage(threadsStr: string): ChatThreads { return JSON.parse(threadsStr, (key, value) => { if (value && typeof value === 'object' && value.$mid === 1) { //$mid is the MarshalledId. $mid === 1 means it is a URI @@ -551,6 +582,220 @@ class ChatThreadService extends Disposable implements IChatThreadService { // ---------- the rest ---------- + // gets the location of codespan link so the user can click on it + async generateCodespanLink(_codespanStr: string): Promise { + + // process codespan to understand what we are searching for + // TODO account for more complicated patterns eg `ITextEditorService.openEditor()` + const filePattern = /^[^\s.]+\.[^\s.]+$/; + const functionPattern = /^[^\s(]+\([^)]*\)$/; + + let target = _codespanStr // the string to search for + let codespanType: 'file' | 'function-or-class' | 'unsearchable' = 'unsearchable'; + if (filePattern.test(target)) { + + codespanType = 'file' + target = _codespanStr + + } else if (functionPattern.test(target)) { + const match = target.match(functionPattern) + if (match && match[0]) { + + codespanType = 'function-or-class' + target = match[0] + + } + } + + if (codespanType === 'unsearchable') { + return null + } + + // get history of all AI and user added files in conversation + store in reverse order (MRU) + const prevUris = this._getAllSelections() + .map(s => s.fileURI) + .filter((uri, index, array) => array.findIndex(u => u.toString() === uri.toString()) === index) // O(n^2) but this is small + .reverse() + + + + if (codespanType === 'file') { + + + const doesUriMatchTarget = (uri: URI) => uri.path.includes(target) + + // check if any prevFiles are the `codespanSearch` + for (const uri of prevUris) { + if (doesUriMatchTarget(uri)) return { uri } + } + + // else search codebase for file + const { uris } = await this._toolsService.callTool['pathname_search']({ queryStr: target, pageNumber: 0 }) + + for (const uri of uris) { + if (doesUriMatchTarget(uri)) return { uri } + } + + } + + + if (codespanType === 'function-or-class') { + + + // check all prevUris for the target + for (const uri of prevUris) { + + const modelRef = await this._textModelService.createModelReference(uri); + const model = modelRef.object.textEditorModel; + + try { + const matches = model.findMatches( + target.split('(')[0].trim(), // remove parameters + false, // searchOnlyEditableRange + false, // isRegex + true, // matchCase + null, // wordSeparators + true // captureMatches + ); + + const firstThree = matches.slice(0, 3); + + // take first 3 occurences, attempt to goto definition on them + + for (const match of firstThree) { + const position = new Position(match.range.startLineNumber, match.range.startColumn); + const definitionProviders = this._languageFeaturesService.definitionProvider.ordered(model); + + for (const provider of definitionProviders) { + const definitions = await provider.provideDefinition(model, position, CancellationToken.None); + if (!definitions) continue; + + const locationLinks: LocationLink[] = []; + + if (Array.isArray(definitions)) { + // Handle Location[] or LocationLink[] + for (const def of definitions) { + if ('uri' in def) { + locationLinks.push({ + uri: def.uri, + range: def.range, + targetSelectionRange: def.range, + originSelectionRange: undefined + }); + } else { + locationLinks.push(def); + } + } + } else { + // Handle single Location + locationLinks.push({ + uri: definitions.uri, + range: definitions.range, + targetSelectionRange: definitions.range, + originSelectionRange: undefined + }); + } + + const definition = locationLinks[0]; + if (!definition) continue; + + // Load definition file model + const defModelRef = await this._textModelService.createModelReference(definition.uri); + const defModel = defModelRef.object.textEditorModel; + + try { + const symbolProviders = this._languageFeaturesService.documentSymbolProvider.ordered(defModel); + + for (const symbolProvider of symbolProviders) { + const symbols = await symbolProvider.provideDocumentSymbols( + defModel, + CancellationToken.None + ); + + if (symbols) { + const symbol = symbols.find(s => { + const symbolRange = s.range; + return symbolRange.startLineNumber <= definition.range.startLineNumber && + symbolRange.endLineNumber >= definition.range.endLineNumber && + (symbolRange.startLineNumber !== definition.range.startLineNumber || symbolRange.startColumn <= definition.range.startColumn) && + (symbolRange.endLineNumber !== definition.range.endLineNumber || symbolRange.endColumn >= definition.range.endColumn); + }); + + + console.log('@@@ symbol', symbol?.name, symbol?.kind) + + // if we got to a class/function get the full range and return + if (symbol?.kind === SymbolKind.Function || symbol?.kind === SymbolKind.Class) { + return { + uri: definition.uri, + selection: { + startLineNumber: definition.range.startLineNumber, + startColumn: definition.range.startColumn, + endLineNumber: definition.range.endLineNumber, + endColumn: definition.range.endColumn, + } + }; + } + } + } + } finally { + defModelRef.dispose(); + } + } + } + } finally { + modelRef.dispose(); + } + } + + // unlike above do not search codebase (doesnt make sense) + + } + + return null + + } + + getCodespanLink({ codespanStr, messageIdx, threadId }: { codespanStr: string, messageIdx: number, threadId: string }): CodespanLocationLink | undefined { + const thread = this.state.allThreads[threadId] + if (!thread) return undefined; + + const links = thread.state.linksOfMessageIdx?.[messageIdx] + if (!links) return undefined; + + const location = links[codespanStr] + if (!location) return undefined; + + return location + } + + async addCodespanLink({ newLinkText, newLinkLocation, messageIdx, threadId }: { newLinkText: string, newLinkLocation: CodespanLocationLink, messageIdx: number, threadId: string }) { + const thread = this.state.allThreads[threadId] + if (!thread) return + + this._setState({ + + allThreads: { + ...this.state.allThreads, + [threadId]: { + ...thread, + state: { + ...thread.state, + linksOfMessageIdx: { + ...thread.state.linksOfMessageIdx, + [messageIdx]: { + ...thread.state.linksOfMessageIdx?.[messageIdx], + [newLinkText]: newLinkLocation + } + } + } + + } + } + }, true) + } + + getCurrentThread(): ChatThreads[string] { const state = this.state const thread = state.allThreads[state.currentThreadId] @@ -721,7 +966,9 @@ class ChatThreadService extends Disposable implements IChatThreadService { getCurrentThreadState = () => { + const currentThread = this.getCurrentThread() + return currentThread.state } diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index 50b8378e..d2b7cc56 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -3,11 +3,17 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import React, { JSX } from 'react' +import React, { JSX, useState } from 'react' import { marked, MarkedToken, Token } from 'marked' import { BlockCode } from './BlockCode.js' import { nameToVscodeLanguage } from '../../../../common/helpers/detectLanguage.js' import { ApplyBlockHoverButtons } from './ApplyBlockHoverButtons.js' +import { useAccessor, useChatThreadsState } from '../util/services.js' +import { Range } from '../../../../../../services/search/common/searchExtTypes.js' +import { CodespanLocationLink } from '../../../chatThreadService.js' +import { IRange } from '../../../../../../../base/common/range.js' +import { ScrollType } from '../../../../../../../editor/common/editorCommon.js' + export type ChatMessageLocation = { threadId: string; @@ -20,7 +26,87 @@ const getApplyBoxId = ({ threadId, messageIdx, tokenIdx }: ApplyBoxLocation) => return `${threadId}-${messageIdx}-${tokenIdx}` } -const RenderToken = ({ token, nested, chatMessageLocationForApply, tokenIdx }: { token: Token | string, nested?: boolean, chatMessageLocationForApply?: ChatMessageLocation, tokenIdx: string }): JSX.Element => { + +const Codespan = ({ text, className, onClick }: { text: string, className?: string, onClick?: () => void }) => { + + return + {text} + + +} + +const CodespanWithLink = ({ text, rawText, chatMessageLocation }: { text: string, rawText: string, chatMessageLocation: ChatMessageLocation }) => { + + const accessor = useAccessor() + + const chatThreadService = accessor.get('IChatThreadService') + const commandSerivce = accessor.get('ICommandService') + const editorService = accessor.get('ICodeEditorService') + + const { messageIdx, threadId } = chatMessageLocation + + const [didComputeCodespanLink, setDidComputeCodespanLink] = useState(false) + + console.log('rerender', didComputeCodespanLink ? 1 : 0) + + let link = undefined + + if (rawText.endsWith("`")) { // if codespan was completed + + // get link from cache + link = chatThreadService.getCodespanLink({ codespanStr: text, messageIdx, threadId }) + + if (link === undefined) { + + // generate link and add to cache + chatThreadService.generateCodespanLink(text) + .then(link => { + + chatThreadService.addCodespanLink({ newLinkText: text, newLinkLocation: link, messageIdx, threadId }) + + setDidComputeCodespanLink(true) + + }) + } + + } + + + const onClick = () => { + + if (!link) return; + const selection = link.selection + + // open the file + commandSerivce.executeCommand('vscode.open', link.uri).then(() => { + + console.log('click:', selection, link.uri) + + // select the text + if (!selection) return; + + const editor = editorService.getActiveCodeEditor() + if (!editor) return; + + editor.setSelection(selection) + editor.revealRange(selection, ScrollType.Immediate) + + }) + + } + + + return +} + +const RenderToken = ({ token, nested, chatMessageLocation, tokenIdx }: { token: Token | string, nested?: boolean, chatMessageLocation?: ChatMessageLocation, tokenIdx: string }): JSX.Element => { // deal with built-in tokens first (assume marked token) const t = token as MarkedToken @@ -35,12 +121,14 @@ const RenderToken = ({ token, nested, chatMessageLocationForApply, tokenIdx }: { if (t.type === "code") { - const applyBoxId = chatMessageLocationForApply ? getApplyBoxId({ - threadId: chatMessageLocationForApply.threadId, - messageIdx: chatMessageLocationForApply.messageIdx, + const applyBoxId = chatMessageLocation ? getApplyBoxId({ + threadId: chatMessageLocation.threadId, + messageIdx: chatMessageLocation.messageIdx, tokenIdx: tokenIdx, }) : null + // TODO user should only be able to apply this when the code has been closed (t.raw ends with "```") + return
- + } @@ -148,7 +236,7 @@ const RenderToken = ({ token, nested, chatMessageLocationForApply, tokenIdx }: { )} - + ))} @@ -162,6 +250,7 @@ const RenderToken = ({ token, nested, chatMessageLocationForApply, tokenIdx }: { ))} @@ -221,11 +310,19 @@ const RenderToken = ({ token, nested, chatMessageLocationForApply, tokenIdx }: { // inline code if (t.type === "codespan") { - return ( - - {t.text} - - ) + + + console.log('chatmessagelocation', chatMessageLocation) + + if (chatMessageLocation) { + return + } + + return } if (t.type === "br") { @@ -244,12 +341,12 @@ const RenderToken = ({ token, nested, chatMessageLocationForApply, tokenIdx }: { ) } -export const ChatMarkdownRender = ({ string, nested = false, chatMessageLocationForApply }: { string: string, nested?: boolean, chatMessageLocationForApply?: ChatMessageLocation }) => { +export const ChatMarkdownRender = ({ string, nested = false, chatMessageLocation }: { string: string, nested?: boolean, chatMessageLocation: ChatMessageLocation | undefined }) => { const tokens = marked.lexer(string); // https://marked.js.org/using_pro#renderer return ( <> {tokens.map((token, index) => ( - + ))} ) 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 cc9d779e..6bcf5ae8 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 @@ -936,19 +936,6 @@ const AssistantMessageComponent = ({ chatMessage, isLoading, messageIdx }: ChatB if (isEmpty) return null return <> - - {/* reasoning token */} - {hasReasoning && } - > - - } -
+ {/* reasoning token */} + {hasReasoning && } + > + + } + {/* assistant message */} - {isLoading && } - + {/* loading indicator */} + {isLoading && }
@@ -1122,25 +1121,28 @@ const toolNameToComponent: { [T in ToolName]: { numResults={value.uris.length} icon={} > - {value.uris.map((uri, i) => ( -
{ - commandService.executeCommand('vscode.open', uri, { preview: true }) - }} - > -
- {uri.fsPath.split('/').pop()} -
- )) + { + value.uris.map((uri, i) => ( +
{ + commandService.executeCommand('vscode.open', uri, { preview: true }) + }} + > +
+ {uri.fsPath.split('/').pop()} +
+ )) } - {value.hasNextPage && ( -
- More results available... -
- )} - + { + value.hasNextPage && ( +
+ More results available... +
+ ) + } + ) } }, @@ -1232,8 +1234,8 @@ const toolNameToComponent: { [T in ToolName]: { return } onClick={() => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }} > - - + + }, resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() @@ -1278,7 +1280,7 @@ const toolNameToComponent: { [T in ToolName]: { // TODO!!! open terminal >
- +
) 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 10068af2..346d9143 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 @@ -291,7 +291,7 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider isPasswordField={isPasswordField} /> {subTextMd === undefined ? null :
- +
} @@ -421,11 +421,11 @@ export const FeaturesTab = () => { {/*

{`Void can access any model that you host locally. We automatically detect your local models by default.`}

*/}

{`Void can access any model that you host locally. We automatically detect your local models by default.`}

- - - - - + + + + + {/* TODO we should create UI for downloading models without user going into terminal */}
From 2300559081a4b8cf48c5185f6a74b7f8f6d51ac0 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Sat, 8 Mar 2025 01:01:16 -0800 Subject: [PATCH 017/173] links to code --- .../contrib/void/browser/chatThreadService.ts | 146 ++++++++---------- .../react/src/markdown/ChatMarkdownRender.tsx | 31 ++-- 2 files changed, 77 insertions(+), 100 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 205568d4..44bfefe0 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -293,7 +293,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { // should probably re-use code from void/src/vs/base/common/marshalling.ts instead. but this is simple enough private _convertThreadDataFromStorage(threadsStr: string): ChatThreads { return JSON.parse(threadsStr, (key, value) => { - if (value && typeof value === 'object' && value.$mid === 1) { //$mid is the MarshalledId. $mid === 1 means it is a URI + if (value && typeof value === 'object' && value.$mid === 1) { // $mid is the MarshalledId. $mid === 1 means it is a URI return URI.from(value); } return value; @@ -587,22 +587,27 @@ class ChatThreadService extends Disposable implements IChatThreadService { // process codespan to understand what we are searching for // TODO account for more complicated patterns eg `ITextEditorService.openEditor()` - const filePattern = /^[^\s.]+\.[^\s.]+$/; - const functionPattern = /^[^\s(]+\([^)]*\)$/; + const functionOrMethodPattern = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/; // `fUnCt10n_name` + const functionParensPattern = /^([^\s(]+)\([^)]*\)$/; // `functionName( args )` let target = _codespanStr // the string to search for let codespanType: 'file' | 'function-or-class' | 'unsearchable' = 'unsearchable'; - if (filePattern.test(target)) { + if (target.includes('.')) { codespanType = 'file' target = _codespanStr - } else if (functionPattern.test(target)) { - const match = target.match(functionPattern) - if (match && match[0]) { + } else if (functionOrMethodPattern.test(target)) { + + codespanType = 'function-or-class' + target = _codespanStr + + } else if (functionParensPattern.test(target)) { + const match = target.match(functionParensPattern) + if (match && match[1]) { codespanType = 'function-or-class' - target = match[0] + target = match[1] } } @@ -618,7 +623,6 @@ class ChatThreadService extends Disposable implements IChatThreadService { .reverse() - if (codespanType === 'file') { @@ -650,96 +654,79 @@ class ChatThreadService extends Disposable implements IChatThreadService { try { const matches = model.findMatches( - target.split('(')[0].trim(), // remove parameters + target, false, // searchOnlyEditableRange false, // isRegex true, // matchCase - null, // wordSeparators + ' ', // wordSeparators true // captureMatches ); const firstThree = matches.slice(0, 3); // take first 3 occurences, attempt to goto definition on them - for (const match of firstThree) { const position = new Position(match.range.startLineNumber, match.range.startColumn); const definitionProviders = this._languageFeaturesService.definitionProvider.ordered(model); for (const provider of definitionProviders) { - const definitions = await provider.provideDefinition(model, position, CancellationToken.None); - if (!definitions) continue; - const locationLinks: LocationLink[] = []; + const _definitions = await provider.provideDefinition(model, position, CancellationToken.None); - if (Array.isArray(definitions)) { - // Handle Location[] or LocationLink[] - for (const def of definitions) { - if ('uri' in def) { - locationLinks.push({ - uri: def.uri, - range: def.range, - targetSelectionRange: def.range, - originSelectionRange: undefined - }); - } else { - locationLinks.push(def); + if (!_definitions) continue; + + const definitions = Array.isArray(_definitions) ? _definitions : [_definitions]; + + for (const definition of definitions) { + + return { + uri: definition.uri, + selection: { + startLineNumber: definition.range.startLineNumber, + startColumn: definition.range.startColumn, + endLineNumber: definition.range.endLineNumber, + endColumn: definition.range.endColumn, } - } - } else { - // Handle single Location - locationLinks.push({ - uri: definitions.uri, - range: definitions.range, - targetSelectionRange: definitions.range, - originSelectionRange: undefined - }); - } + }; - const definition = locationLinks[0]; - if (!definition) continue; + // const defModelRef = await this._textModelService.createModelReference(definition.uri); + // const defModel = defModelRef.object.textEditorModel; - // Load definition file model - const defModelRef = await this._textModelService.createModelReference(definition.uri); - const defModel = defModelRef.object.textEditorModel; + // try { + // const symbolProviders = this._languageFeaturesService.documentSymbolProvider.ordered(defModel); - try { - const symbolProviders = this._languageFeaturesService.documentSymbolProvider.ordered(defModel); + // for (const symbolProvider of symbolProviders) { + // const symbols = await symbolProvider.provideDocumentSymbols( + // defModel, + // CancellationToken.None + // ); - for (const symbolProvider of symbolProviders) { - const symbols = await symbolProvider.provideDocumentSymbols( - defModel, - CancellationToken.None - ); + // if (symbols) { + // const symbol = symbols.find(s => { + // const symbolRange = s.range; + // return symbolRange.startLineNumber <= definition.range.startLineNumber && + // symbolRange.endLineNumber >= definition.range.endLineNumber && + // (symbolRange.startLineNumber !== definition.range.startLineNumber || symbolRange.startColumn <= definition.range.startColumn) && + // (symbolRange.endLineNumber !== definition.range.endLineNumber || symbolRange.endColumn >= definition.range.endColumn); + // }); - if (symbols) { - const symbol = symbols.find(s => { - const symbolRange = s.range; - return symbolRange.startLineNumber <= definition.range.startLineNumber && - symbolRange.endLineNumber >= definition.range.endLineNumber && - (symbolRange.startLineNumber !== definition.range.startLineNumber || symbolRange.startColumn <= definition.range.startColumn) && - (symbolRange.endLineNumber !== definition.range.endLineNumber || symbolRange.endColumn >= definition.range.endColumn); - }); - - - console.log('@@@ symbol', symbol?.name, symbol?.kind) - - // if we got to a class/function get the full range and return - if (symbol?.kind === SymbolKind.Function || symbol?.kind === SymbolKind.Class) { - return { - uri: definition.uri, - selection: { - startLineNumber: definition.range.startLineNumber, - startColumn: definition.range.startColumn, - endLineNumber: definition.range.endLineNumber, - endColumn: definition.range.endColumn, - } - }; - } - } - } - } finally { - defModelRef.dispose(); + // // if we got to a class/function get the full range and return + // if (symbol?.kind === SymbolKind.Function || symbol?.kind === SymbolKind.Method || symbol?.kind === SymbolKind.Class) { + // return { + // uri: definition.uri, + // selection: { + // startLineNumber: definition.range.startLineNumber, + // startColumn: definition.range.startColumn, + // endLineNumber: definition.range.endLineNumber, + // endColumn: definition.range.endColumn, + // } + // }; + // } + // } + // } + // } finally { + // defModelRef.dispose(); + // } } } } @@ -763,10 +750,9 @@ class ChatThreadService extends Disposable implements IChatThreadService { const links = thread.state.linksOfMessageIdx?.[messageIdx] if (!links) return undefined; - const location = links[codespanStr] - if (!location) return undefined; + const link = links[codespanStr] - return location + return link } async addCodespanLink({ newLinkText, newLinkLocation, messageIdx, threadId }: { newLinkText: string, newLinkLocation: CodespanLocationLink, messageIdx: number, threadId: string }) { diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index d2b7cc56..c5f8ff42 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -50,26 +50,20 @@ const CodespanWithLink = ({ text, rawText, chatMessageLocation }: { text: string const [didComputeCodespanLink, setDidComputeCodespanLink] = useState(false) - console.log('rerender', didComputeCodespanLink ? 1 : 0) - let link = undefined - if (rawText.endsWith("`")) { // if codespan was completed // get link from cache link = chatThreadService.getCodespanLink({ codespanStr: text, messageIdx, threadId }) if (link === undefined) { - // generate link and add to cache - chatThreadService.generateCodespanLink(text) + (chatThreadService.generateCodespanLink(text) .then(link => { - chatThreadService.addCodespanLink({ newLinkText: text, newLinkLocation: link, messageIdx, threadId }) - - setDidComputeCodespanLink(true) - + setDidComputeCodespanLink(true) // rerender }) + ) } } @@ -83,22 +77,22 @@ const CodespanWithLink = ({ text, rawText, chatMessageLocation }: { text: string // open the file commandSerivce.executeCommand('vscode.open', link.uri).then(() => { - console.log('click:', selection, link.uri) - // select the text - if (!selection) return; + setTimeout(() => { + if (!selection) return; - const editor = editorService.getActiveCodeEditor() - if (!editor) return; + const editor = editorService.getActiveCodeEditor() + if (!editor) return; - editor.setSelection(selection) - editor.revealRange(selection, ScrollType.Immediate) + editor.setSelection(selection) + editor.revealRange(selection, ScrollType.Immediate) + + }, 50) // needed when document was just opened and needs to initialize }) } - return Date: Sun, 9 Mar 2025 03:30:23 +0100 Subject: [PATCH 018/173] FIM : I think it works as expected --- package-lock.json | 21 ++++++ package.json | 1 + .../contrib/void/common/modelCapabilities.ts | 64 +++++++++++++++---- .../llmMessage/sendLLMMessage.impl.ts | 62 +++++++++++++++--- 4 files changed, 126 insertions(+), 22 deletions(-) diff --git a/package-lock.json b/package-lock.json index f5127fb3..8c614833 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@google/generative-ai": "^0.22.0", "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", + "@mistralai/mistralai": "^1.5.1", "@parcel/watcher": "2.5.1", "@types/semver": "^7.5.8", "@vscode/deviceid": "^0.1.1", @@ -2538,6 +2539,17 @@ "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-1.1.9.tgz", "integrity": "sha512-n1VPsljTSkthsAFYdiWfC+DKzK2WwcRp83Y1YAqdX552BstvsDjft9YXppjUzp11BPsapDoO1LDgrDB0XVsfNQ==" }, + "node_modules/@mistralai/mistralai": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-1.5.1.tgz", + "integrity": "sha512-Ie0EH4dAO11MEXR5N2kS2cgr+ycTWvqN/yP9bKrtmUEqjdcF4i7DLxtrFMUw5l2dOPhrkX93G4SziFiATPWu2w==", + "dependencies": { + "zod-to-json-schema": "^3.24.1" + }, + "peerDependencies": { + "zod": ">= 3" + } + }, "node_modules/@next/env": { "version": "15.2.0", "resolved": "https://registry.npmjs.org/@next/env/-/env-15.2.0.tgz", @@ -23145,6 +23157,15 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } + }, + "node_modules/zod-to-json-schema": { + "version": "3.24.3", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.3.tgz", + "integrity": "sha512-HIAfWdYIt1sssHfYZFCXp4rU1w2r8hVVXYIlmoa0r0gABLs5di3RCqPU5DDROogVz1pAdYBaz7HK5n9pSUNs3A==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.24.1" + } } } } diff --git a/package.json b/package.json index 921a4a1f..2a8f0a57 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "@google/generative-ai": "^0.22.0", "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", + "@mistralai/mistralai": "^1.5.1", "@parcel/watcher": "2.5.1", "@types/semver": "^7.5.8", "@vscode/deviceid": "^0.1.1", diff --git a/src/vs/workbench/contrib/void/common/modelCapabilities.ts b/src/vs/workbench/contrib/void/common/modelCapabilities.ts index d6fbd390..96c745a2 100644 --- a/src/vs/workbench/contrib/void/common/modelCapabilities.ts +++ b/src/vs/workbench/contrib/void/common/modelCapabilities.ts @@ -52,11 +52,7 @@ export const defaultModelsOfProvider = { // 'qwen-2.5-coder-32b', // preview mode (experimental) ], mistral: [ // https://docs.mistral.ai/getting-started/models/models_overview/ - 'codestral-latest', - 'mistral-large-latest', - 'ministral-3b-latest', - 'ministral-8b-latest', - '' + 'codestral-latest' ], openAICompatible: [], // fallback } as const satisfies Record @@ -135,12 +131,6 @@ const openSourceModelOptions_assumingOAICompat = { supportsTools: false, supportsReasoning: false, }, - 'mistral': { - supportsFIM: true, - supportsSystemMessage: 'system-role', - supportsTools: 'openai-style', - supportsReasoning: false, - }, // llama 'llama3': { supportsFIM: false, @@ -179,6 +169,12 @@ const openSourceModelOptions_assumingOAICompat = { supportsTools: 'openai-style', supportsReasoning: { canToggleReasoning: false, canIOReasoning: true, openSourceThinkTags: ['', ''] }, }, + 'mistral-large-latest': { + supportsFIM: false, + supportsSystemMessage: 'system-role', + supportsTools: 'openai-style', + supportsReasoning: false, + }, // FIM only 'starcoder2': { supportsFIM: true, @@ -186,6 +182,13 @@ const openSourceModelOptions_assumingOAICompat = { supportsTools: false, supportsReasoning: false, }, + // Mistral + 'codestral-latest': { + supportsFIM: true, + supportsSystemMessage: 'system-role', + supportsTools: 'openai-style', + supportsReasoning: false, + }, 'codegemma:2b': { supportsFIM: true, supportsSystemMessage: false, @@ -214,7 +217,8 @@ const extensiveModelFallback: ProviderSettings['modelOptionsFallback'] = (modelN if (modelName.includes('deepseek')) return toFallback({ ...openSourceModelOptions_assumingOAICompat.deepseekCoderV2, contextWindow: 32_000, maxOutputTokens: 4_096, }) if (modelName.includes('llama3')) return toFallback({ ...openSourceModelOptions_assumingOAICompat.llama3, contextWindow: 32_000, maxOutputTokens: 4_096, }) if (modelName.includes('qwen') && modelName.includes('2.5') && modelName.includes('coder')) return toFallback({ ...openSourceModelOptions_assumingOAICompat['qwen2.5coder'], contextWindow: 32_000, maxOutputTokens: 4_096, }) - if (modelName.includes('mistral')) return toFallback({ ...openSourceModelOptions_assumingOAICompat.mistral, contextWindow: 32_000, maxOutputTokens: 4_096, }) + if (modelName.includes('mistral-large-latest')) return toFallback({ ...openSourceModelOptions_assumingOAICompat['mistral-large-latest'], contextWindow: 32_000, maxOutputTokens: 4_096, }) + if (modelName.includes('codestral-latest')) return toFallback(mistralModelOptions['codestral-latest']) if (/\bo1\b/.test(modelName) || /\bo3\b/.test(modelName)) return toFallback(openAIModelOptions['o1']) return toFallback(modelOptionsDefaults) } @@ -354,6 +358,33 @@ const openAISettings: ProviderSettings = { } } +const mistralModelOptions = { + 'codestral-latest': { + contextWindow: 32_000, + maxOutputTokens: 4_096, + cost: { input: 0.00, output: 0.00 }, + supportsFIM: true, + supportsSystemMessage: 'system-role', + supportsTools: 'openai-style', + supportsReasoning: false, + }, + 'mistral-large-latest': { + contextWindow: 32_000, + maxOutputTokens: 4_096, + cost: { input: 0.00, output: 0.00 }, + supportsFIM: false, + supportsSystemMessage: 'system-role', + supportsTools: 'openai-style', + supportsReasoning: false, + } +} as const satisfies { [s: string]: ModelOptions } + + + + + + + // ---------------- XAI ---------------- const xAIModelOptions = { 'grok-2': { @@ -555,13 +586,20 @@ const openRouterModelOptions_assumingOpenAICompat = { supportsReasoning: false, }, 'mistralai/codestral-2501': { - ...openSourceModelOptions_assumingOAICompat.mistral, + ...openSourceModelOptions_assumingOAICompat['codestral-latest'], contextWindow: 256_000, maxOutputTokens: null, cost: { input: 0.3, output: 0.9 }, supportsTools: 'openai-style', supportsReasoning: false, }, + 'mistralai/mistral-large-latest': { + ...openSourceModelOptions_assumingOAICompat['mistral-large-latest'], + contextWindow: 256_000, + maxOutputTokens: null, + cost: { input: 0.3, output: 0.9 }, + }, + 'qwen/qwen-2.5-coder-32b-instruct': { ...openSourceModelOptions_assumingOAICompat['qwen2.5coder'], contextWindow: 33_000, 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 9b07807e..568e7244 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 @@ -7,6 +7,11 @@ import Anthropic from '@anthropic-ai/sdk'; import { Ollama } from 'ollama'; import OpenAI, { ClientOptions } from 'openai'; +/* Mistral standalone Fim endpoint */ +import { MistralCore } from "@mistralai/mistralai/core.js"; +import { fimComplete } from "@mistralai/mistralai/funcs/fimComplete.js"; +/* End Mistral standalone Fim endpoint */ + import { Model as OpenAIModel } from 'openai/resources/models.js'; import { extractReasoningOnFinalMessage, extractReasoningOnTextWrapper } from '../../browser/helpers/extractCodeFromResult.js'; import { LLMChatMessage, LLMFIMMessage, ModelListParams, OllamaModelResponse, OnError, OnFinalMessage, OnText } from '../../common/llmMessageTypes.js'; @@ -143,7 +148,7 @@ const _sendOpenAICompatibleFIM = ({ messages: messages_, onFinalMessage, onError max_tokens: messages.maxTokens, }) .then(async response => { - const fullText = response.choices[0]?.text + const fullText = response.choices[0]?.text || ''; onFinalMessage({ fullText, }); }) .catch(error => { @@ -153,7 +158,49 @@ const _sendOpenAICompatibleFIM = ({ messages: messages_, onFinalMessage, onError } +const _sendMistralFIM = ({ messages: messages_, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, aiInstructions }: SendFIMParams_Internal) => { + const { modelName, supportsFIM } = getModelCapabilities(providerName, modelName_) + if (!supportsFIM) { + if (modelName === modelName_) + onError({ message: `Model ${modelName} does not support FIM.`, fullError: null }) + else + onError({ message: `Model ${modelName_} (${modelName}) does not support FIM.`, fullError: null }) + return + } + const messages = prepareFIMMessage({ messages: messages_, aiInstructions }) + const mistral = new MistralCore({ apiKey: settingsOfProvider.mistral.apiKey }) + + // DEBUG : request params + // console.log('🔍 Sending FIM request with params:', { + // model: modelName, + // promptLength: messages.prefix.length, + // suffixLength: messages.suffix.length, + // stream: false, + // maxTokens: messages.maxTokens + //}); + + fimComplete( + mistral, { + model: modelName, + prompt: messages.prefix, + suffix: messages.suffix, + stream: false, + topP: 1, + maxTokens: messages.maxTokens, + stop: messages.stopTokens + }, + ) + .then(async response => { + const fullText = response.choices[0]?.text || ''; + onFinalMessage({ fullText, }); + // console.log('✅ Réponse FIM reçue:', fullText); + + }) + .catch(error => { + onError({ message: error + '', fullError: error }); + }) +} const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, aiInstructions, tools: tools_ }: SendChatParams_Internal) => { const { @@ -374,8 +421,8 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM // on done - (or when error/fail) - this is called AFTER last streamEvent stream.on('finalMessage', (response) => { - const toolCalls = toolCallsFrom_AnthropicContent(response.content) - onFinalMessage({ fullText, fullReasoning, toolCalls, rawAnthropicAssistantContent: response.content as any }) + const toolCalls = toolCallsFrom_AnthropicContent(response.value.choices) + onFinalMessage({ fullText, fullReasoning, toolCalls, rawAnthropicAssistantContent: response.value.choices as any }) }) // on error stream.on('error', (error) => { @@ -530,8 +577,8 @@ export const sendLLMMessageToProviderImplementation = { }, mistral: { sendChat: (params) => _sendOpenAICompatibleChat(params), - sendFIM: (params) => _sendOpenAICompatibleFIM(params), - list: (params) => _openaiCompatibleList(params), + sendFIM: (params) => _sendMistralFIM(params), + list: null }, } satisfies CallFnOfProvider @@ -545,10 +592,7 @@ qwen2.5-coder https://ollama.com/library/qwen2.5-coder/blobs/e94a8ecb9327 <|fim_prefix|>{{ .Prompt }}<|fim_suffix|>{{ .Suffix }}<|fim_middle|> codestral https://ollama.com/library/codestral/blobs/51707752a87c -[SUFFIX]{{ .Suffix }}[PREFIX] {{ .Prompt }} - -deepseek-coder-v2 https://ollama.com/library/deepseek-coder-v2/blobs/22091531faf0 -<|fim▁begin|>{{ .Prompt }}<|fim▁hole|>{{ .Suffix }}<|fim▁end|> +{{ .Prompt }} starcoder2 https://ollama.com/library/starcoder2/blobs/3b190e68fefe From 330092d0231ebf77bc444a7e980096ad0b23c91f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Commaret?= Date: Sun, 9 Mar 2025 21:55:20 +0100 Subject: [PATCH 019/173] mistral npm updated --- package-lock.json | 20 ++++++++++++++++---- package.json | 2 +- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index bc045891..5b459a12 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@google/generative-ai": "^0.21.0", "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", - "@mistralai/mistralai": "^1.4.0", + "@mistralai/mistralai": "^1.5.1", "@parcel/watcher": "2.1.0", "@rrweb/record": "^2.0.0-alpha.17", "@rrweb/types": "^2.0.0-alpha.17", @@ -2429,9 +2429,12 @@ } }, "node_modules/@mistralai/mistralai": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-1.4.0.tgz", - "integrity": "sha512-xA3DAtIDh4Qgr1EoSuiGVE+2ABNrxpcTeC0kSXYbkDNUGdthalLAH7DgbG0fkKZ7TN8xdWXQq2WiIghp/O96Eg==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-1.5.1.tgz", + "integrity": "sha512-Ie0EH4dAO11MEXR5N2kS2cgr+ycTWvqN/yP9bKrtmUEqjdcF4i7DLxtrFMUw5l2dOPhrkX93G4SziFiATPWu2w==", + "dependencies": { + "zod-to-json-schema": "^3.24.1" + }, "peerDependencies": { "zod": ">= 3" } @@ -24183,6 +24186,15 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } + }, + "node_modules/zod-to-json-schema": { + "version": "3.24.3", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.3.tgz", + "integrity": "sha512-HIAfWdYIt1sssHfYZFCXp4rU1w2r8hVVXYIlmoa0r0gABLs5di3RCqPU5DDROogVz1pAdYBaz7HK5n9pSUNs3A==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.24.1" + } } } } diff --git a/package.json b/package.json index a4ee38bb..ea8bc1d9 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,7 @@ "@google/generative-ai": "^0.21.0", "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", - "@mistralai/mistralai": "^1.4.0", + "@mistralai/mistralai": "^1.5.1", "@parcel/watcher": "2.1.0", "@rrweb/record": "^2.0.0-alpha.17", "@rrweb/types": "^2.0.0-alpha.17", From 6a55abcd8132a06c269c157bb83eb207d453a4b7 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sun, 9 Mar 2025 18:07:21 -0700 Subject: [PATCH 020/173] split into common/ and browser/ so terminal works --- .../void/browser/autocompleteService.ts | 2 +- .../contrib/void/browser/chatThreadService.ts | 92 ++------- .../contrib/void/browser/editCodeService.ts | 4 +- .../src/quick-edit-tsx/QuickEditChat.tsx | 1 - .../react/src/sidebar-tsx/ErrorDisplay.tsx | 2 +- .../react/src/sidebar-tsx/SidebarChat.tsx | 6 +- .../contrib/void/browser/sidebarActions.ts | 3 +- .../void/browser/terminalToolService.ts | 77 +++++--- .../contrib/void/browser/toolsService.ts | 182 ++---------------- .../contrib/void/browser/void.contribution.ts | 13 +- .../void/common/chatThreadServiceTypes.ts | 76 ++++++++ .../helpers/extractCodeFromResult.ts | 2 +- .../{browser => common}/prompt/prompts.ts | 8 +- .../void/common/sendLLMMessageTypes.ts | 2 +- .../contrib/void/common/toolsServiceTypes.ts | 162 ++++++++++++++++ .../llmMessage/sendLLMMessage.impl.ts | 4 +- 16 files changed, 340 insertions(+), 296 deletions(-) create mode 100644 src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts rename src/vs/workbench/contrib/void/{browser => common}/helpers/extractCodeFromResult.ts (99%) rename src/vs/workbench/contrib/void/{browser => common}/prompt/prompts.ts (98%) create mode 100644 src/vs/workbench/contrib/void/common/toolsServiceTypes.ts diff --git a/src/vs/workbench/contrib/void/browser/autocompleteService.ts b/src/vs/workbench/contrib/void/browser/autocompleteService.ts index 8d219f1c..cc9f3a92 100644 --- a/src/vs/workbench/contrib/void/browser/autocompleteService.ts +++ b/src/vs/workbench/contrib/void/browser/autocompleteService.ts @@ -15,7 +15,7 @@ import { IEditorService } from '../../../services/editor/common/editorService.js import { isCodeEditor } from '../../../../editor/browser/editorBrowser.js'; import { EditorResourceAccessor } from '../../../common/editor.js'; import { IModelService } from '../../../../editor/common/services/model.js'; -import { extractCodeFromRegular } from './helpers/extractCodeFromResult.js'; +import { extractCodeFromRegular } from '../common/helpers/extractCodeFromResult.js'; import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; import { ILLMMessageService } from '../common/sendLLMMessageService.js'; import { isWindows } from '../../../../base/common/platform.js'; diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 44bfefe0..7ee42b36 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -10,21 +10,21 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo import { URI } from '../../../../base/common/uri.js'; import { Emitter, Event } from '../../../../base/common/event.js'; -import { IRange } from '../../../../editor/common/core/range.js'; import { ILLMMessageService } from '../common/sendLLMMessageService.js'; -import { chat_userMessageContent, chat_systemMessage, chat_lastUserMessageWithFilesAdded, chat_selectionsString } from './prompt/prompts.js'; -import { InternalToolInfo, IToolsService, ToolCallParams, ToolResultType, ToolName, toolNamesThatRequireApproval, voidTools } from './toolsService.js'; -import { AnthropicReasoning, LLMChatMessage, ToolCallType } from '../common/sendLLMMessageTypes.js'; +import { chat_userMessageContent, chat_systemMessage, chat_lastUserMessageWithFilesAdded, chat_selectionsString } from '../common/prompt/prompts.js'; +import { LLMChatMessage, ToolCallType } from '../common/sendLLMMessageTypes.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; import { IVoidFileService } from '../common/voidFileService.js'; import { generateUuid } from '../../../../base/common/uuid.js'; import { getErrorMessage } from '../../../../base/common/errors.js'; import { ChatMode, FeatureName } from '../common/voidSettingsTypes.js'; import { IVoidSettingsService } from '../common/voidSettingsService.js'; +import { ToolName, ToolCallParams, ToolResultType, InternalToolInfo, voidTools, toolNamesThatRequireApproval } from '../common/toolsServiceTypes.js'; +import { IToolsService } from './toolsService.js'; +import { CancellationToken } from '../../../../base/common/cancellation.js'; import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js'; import { ITextModelService } from '../../../../editor/common/services/resolverService.js'; -import { LocationLink, SymbolKind } from '../../../../editor/common/languages.js'; -import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { ChatMessage, CodespanLocationLink, StagingSelectionItem } from '../common/chatThreadServiceTypes.js'; import { Position } from '../../../../editor/common/core/position.js'; const findLastIndex = (arr: T[], condition: (t: T) => boolean): number => { @@ -59,85 +59,15 @@ const toLLMChatMessages = (chatMessages: ChatMessage[]): LLMChatMessage[] => { } -// one of the square items that indicates a selection in a chat bubble (NOT a file, a Selection of text) -export type CodeSelection = { - type: 'Selection'; - fileURI: URI; - selectionStr: string; - range: IRange; - state: { - isOpened: boolean; - }; -} - -export type FileSelection = { - type: 'File'; - fileURI: URI; - selectionStr: null; - range: null; - state: { - isOpened: boolean; - }; -} - -export type StagingSelectionItem = CodeSelection | FileSelection - -export type CodespanLocationLink = { - uri: URI, // we handle serialization for this - selection?: { // store as JSON so dont have to worry about serialization - startLineNumber: number - startColumn: number, - endLineNumber: number - endColumn: number, - } | undefined -} | null - -export type ToolMessage = { - role: 'tool'; - name: T; // internal use - paramsStr: string; // internal use - id: string; // apis require this tool use id - content: string; // give this result to LLM - result: { type: 'success'; params: ToolCallParams[T]; value: ToolResultType[T], } | { type: 'error'; value: string }; // give this result to user -} -export type ToolRequestApproval = { - role: 'tool_request'; - name: T; // internal use - params: ToolCallParams[T]; // internal use - voidToolId: string; // internal id Void uses -} - -// WARNING: changing this format is a big deal!!!!!! need to migrate old format to new format on users' computers so people don't get errors. -export type ChatMessage = - | { - role: 'user'; - content: string; // content displayed to the LLM on future calls - allowed to be '', will be replaced with (empty) - displayContent: string | null; // content displayed to user - allowed to be '', will be ignored - selections: StagingSelectionItem[] | null; // the user's selection - state: { - stagingSelections: StagingSelectionItem[]; - isBeingEdited: boolean; - } - } | { - role: 'assistant'; - content: string; // content received from LLM - allowed to be '', will be replaced with (empty) - reasoning: string; // reasoning from the LLM, used for step-by-step thinking - - anthropicReasoning: AnthropicReasoning[] | null; // anthropic reasoning - } - | ToolMessage - | ToolRequestApproval - type UserMessageType = ChatMessage & { role: 'user' } type UserMessageState = UserMessageType['state'] - -export const defaultMessageState: UserMessageState = { +const defaultMessageState: UserMessageState = { stagingSelections: [], isBeingEdited: false, } // a 'thread' means a chat message history -export type ChatThreads = { +type ChatThreads = { [id: string]: { id: string; // store the id here too createdAt: string; // ISO string @@ -160,7 +90,7 @@ export type ChatThreads = { type ThreadType = ChatThreads[string] -const defaultThreadState: ThreadType['state'] = { +export const defaultThreadState: ThreadType['state'] = { stagingSelections: [], focusedMessageIdx: undefined, isCheckedOfSelectionId: {}, @@ -483,11 +413,11 @@ class ChatThreadService extends Disposable implements IChatThreadService { res_() return } - const toolName = tool.name + const toolName: ToolName = tool.name shouldSendAnotherMessage = true // 1. validate tool params - let toolParams: ToolCallParams[typeof toolName] + let toolParams: ToolCallParams[ToolName] try { const params = await this._toolsService.validateParams[toolName](tool.paramsStr) toolParams = params diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 9ef06ede..5c973961 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -25,12 +25,12 @@ 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 './prompt/prompts.js'; +import { voidPrefixAndSuffix, ctrlKStream_userMessage, ctrlKStream_systemMessage, defaultQuickEditFimTags, rewriteCode_systemMessage, rewriteCode_userMessage, searchReplace_systemMessage, searchReplace_userMessage, } from '../common/prompt/prompts.js'; import { mountCtrlK } from './react/out/quick-edit-tsx/index.js' import { QuickEditPropsType } from './quickEditActions.js'; import { IModelContentChangedEvent } from '../../../../editor/common/textModelEvents.js'; -import { extractCodeFromFIM, extractCodeFromRegular, ExtractedSearchReplaceBlock, extractSearchReplaceBlocks } from './helpers/extractCodeFromResult.js'; +import { extractCodeFromFIM, extractCodeFromRegular, ExtractedSearchReplaceBlock, extractSearchReplaceBlocks } from '../common/helpers/extractCodeFromResult.js'; import { filenameToVscodeLanguage } from '../common/helpers/detectLanguage.js'; import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; import { isMacintosh } from '../../../../base/common/platform.js'; diff --git a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx index fe70caa3..c7ecf1ba 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx @@ -93,7 +93,6 @@ export const QuickEditChat = ({ onClose={onX} isStreaming={isStreamingRef.current} isDisabled={isDisabled} - featureName="Ctrl+K" className="py-2 w-full" onClickAnywhere={() => { textAreaRef.current?.focus() }} > diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorDisplay.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorDisplay.tsx index 9aef4b72..e8aec937 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorDisplay.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorDisplay.tsx @@ -5,8 +5,8 @@ import React, { useEffect, useState } from 'react'; import { AlertCircle, ChevronDown, ChevronUp, X } from 'lucide-react'; -import { errorDetails } from '../../../../../../../workbench/contrib/void/common/llmMessageTypes.js'; import { useSettingsState } from '../util/services.js'; +import { errorDetails } from '../../../../common/sendLLMMessageTypes.js'; export const ErrorDisplay = ({ 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 6bcf5ae8..8f26dd49 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 @@ -25,11 +25,11 @@ import { VOID_CTRL_L_ACTION_ID } from '../../../actionIDs.js'; import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js'; import { FeatureName, isFeatureNameDisabled } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'; import { WarningBox } from '../void-settings-tsx/WarningBox.js'; -import { ChatMessage, StagingSelectionItem, ToolMessage, ToolRequestApproval } from '../../../chatThreadService.js'; import { filenameToVscodeLanguage } from '../../../../common/helpers/detectLanguage.js'; -import { ToolName } from '../../../toolsService.js'; import { getModelSelectionState, getModelCapabilities } from '../../../../common/modelCapabilities.js'; import { AlertTriangle, ChevronRight, Dot, Pencil, X } from 'lucide-react'; +import { ChatMessage, StagingSelectionItem, ToolMessage, ToolRequestApproval } from '../../../../common/chatThreadServiceTypes.js'; +import { ToolName } from '../../../../common/toolsServiceTypes.js'; @@ -274,7 +274,7 @@ interface VoidChatAreaProps { onAbort: () => void; isStreaming: boolean; isDisabled?: boolean; - divRef?: React.RefObject; + divRef?: React.RefObject; // UI customization className?: string; diff --git a/src/vs/workbench/contrib/void/browser/sidebarActions.ts b/src/vs/workbench/contrib/void/browser/sidebarActions.ts index 23974f7e..799f7777 100644 --- a/src/vs/workbench/contrib/void/browser/sidebarActions.ts +++ b/src/vs/workbench/contrib/void/browser/sidebarActions.ts @@ -11,7 +11,6 @@ import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; -import { StagingSelectionItem, IChatThreadService } from './chatThreadService.js'; import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; import { IRange } from '../../../../editor/common/core/range.js'; @@ -29,6 +28,8 @@ import { IInstantiationService } from '../../../../platform/instantiation/common import { localize2 } from '../../../../nls.js'; import { IViewsService } from '../../../services/views/common/viewsService.js'; import { IVoidUriStateService } from './voidUriStateService.js'; +import { StagingSelectionItem } from '../common/chatThreadServiceTypes.js'; +import { IChatThreadService } from './chatThreadService.js'; // ---------- Register commands and keybindings ---------- diff --git a/src/vs/workbench/contrib/void/browser/terminalToolService.ts b/src/vs/workbench/contrib/void/browser/terminalToolService.ts index 2fe64d11..6940151e 100644 --- a/src/vs/workbench/contrib/void/browser/terminalToolService.ts +++ b/src/vs/workbench/contrib/void/browser/terminalToolService.ts @@ -6,16 +6,13 @@ import { Disposable } from '../../../../base/common/lifecycle.js'; import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { ITerminalService, ITerminalInstance } from '../../../../workbench/contrib/terminal/browser/terminal.js'; import { TerminalLocation } from '../../../../platform/terminal/common/terminal.js'; -import { generateUuid } from '../../../../base/common/uuid.js'; +import { ITerminalService, ITerminalInstance } from '../../../../workbench/contrib/terminal/browser/terminal.js'; export interface ITerminalToolService { readonly _serviceBrand: undefined; - createNewTerminal(terminalId: string): Promise; - runCommand(command: string, terminalId?: string): Promise; - focus(terminalId: string): Promise; + runCommand(command: string, proposedTerminalId: string): Promise<{ terminalId: string, didCreateTerminal: boolean }>; } export const ITerminalToolService = createDecorator('TerminalToolService'); @@ -23,49 +20,69 @@ export const ITerminalToolService = createDecorator('Termi export class TerminalToolService extends Disposable implements ITerminalToolService { readonly _serviceBrand: undefined; - private terminalInstances: Record = {} + private terminalInstanceOfId: Record = {} constructor( - @ITerminalService private readonly terminalService: ITerminalService + @ITerminalService private readonly terminalService: ITerminalService, ) { super(); } - async createNewTerminal() { - const terminalId = generateUuid(); - this.terminalService.createTerminal({}); + + + getValidNewTerminalId(): string { + // {1 2 3} # size 3, new=4 + // {1 3 4} # size 3, new=2 + // 1 <= newTerminalId <= n + 1 + const n = Object.keys(this.terminalInstanceOfId).length; + for (let i = 1; i <= n + 1; i++) { + const potentialId = i + ''; + if (!(potentialId in this.terminalInstanceOfId)) return potentialId; + } + throw new Error('This should never be reached by pigeonhole principle'); + } + + + private async _createNewTerminal() { + const terminalId = this.getValidNewTerminalId(); const terminal = await this.terminalService.createTerminal({ - location: TerminalLocation.Editor, + location: TerminalLocation.Panel, config: { name: `Void Agent (${terminalId})`, } }); - - this.terminalInstances[terminalId] = terminal + this.terminalInstanceOfId[terminalId] = terminal return terminalId; } - async runCommand(command: string, terminalId?: string) { - - if (!terminalId) { - terminalId = await this.createNewTerminal(); - } - - const terminal = this.terminalInstances[terminalId]; - if (!terminal) throw new Error(`Terminal with ID ${terminalId} does not exist`); - - terminal.sendText(command, true); - return; + private async _getValidTerminalId(proposedTerminalId: string) { + // if there is no terminal ID provided, create one + if (proposedTerminalId in this.terminalInstanceOfId) + return { terminalId: proposedTerminalId, didCreateTerminal: false } + const terminalId = await this._createNewTerminal() + return { terminalId, didCreateTerminal: true } } - async focus(terminalId: string) { - const terminal = this.terminalInstances[terminalId]; - if (!terminal) throw new Error(`That terminal was closed.`); - - + private async _focus(terminalId: string) { + const terminal = this.terminalInstanceOfId[terminalId]; + if (!terminal) return terminal.focus(true); return; } + + async runCommand(command: string, proposedTerminalId: string) { + await this.terminalService.whenConnected; + const { terminalId, didCreateTerminal } = await this._getValidTerminalId(proposedTerminalId) + const terminal = this.terminalInstanceOfId[terminalId]; + if (!terminal) throw new Error(`Unexpected internal error: Terminal with ID ${terminalId} did not exist.`); + this._focus(terminalId) + await terminal.sendText(command, true); + // terminal.onData(data => console.log('DATA!!', data)); + // terminal.onProcessReplayComplete(data => console.log('REPLAY!!', data)); + // terminal.onDidSendText(data => console.log('SEND!!', data)); + return { terminalId, didCreateTerminal }; + } + } -registerSingleton(ITerminalToolService, TerminalToolService, InstantiationType.Eager); +registerSingleton(ITerminalToolService, TerminalToolService, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index d43e1f9f..f7272bf8 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -7,167 +7,19 @@ import { IWorkspaceContextService } from '../../../../platform/workspace/common/ import { QueryBuilder } from '../../../services/search/common/queryBuilder.js' import { ISearchService } from '../../../services/search/common/search.js' import { IEditCodeService } from './editCodeServiceInterface.js' -import { editToolDesc_toolDescription } from './prompt/prompts.js' import { IVoidFileService } from '../common/voidFileService.js' +import { ITerminalToolService } from './terminalToolService.js' +import { ToolCallParams, ToolDirectoryItem, ToolName, ToolResultType } from '../common/toolsServiceTypes.js' // tool use for AI -// we do this using Anthropic's style and convert to OpenAI style later -export type InternalToolInfo = { - name: string, - description: string, - params: { - [paramName: string]: { type: string, description: string | undefined } // name -> type - }, - required: string[], // required paramNames -} -const paginationHelper = { - desc: `Very large results may be paginated (indicated in the result). Pagination fails gracefully if out of bounds or invalid page number.`, - param: { pageNumber: { type: 'number', description: 'The page number (optional, default is 1).' }, } -} as const - -export const voidTools = { - // --- context-gathering (read/search/list) --- - - read_file: { - name: 'read_file', - description: `Returns file contents of a given URI. ${paginationHelper.desc}`, - params: { - uri: { type: 'string', description: undefined }, - }, - required: ['uri'], - }, - - list_dir: { - name: 'list_dir', - description: `Returns all file names and folder names in a given URI. ${paginationHelper.desc}`, - params: { - uri: { type: 'string', description: undefined }, - ...paginationHelper.param - }, - required: ['uri'], - }, - - pathname_search: { - name: 'pathname_search', - description: `Returns all pathnames that match a given grep query. You should use this when looking for a file with a specific name or path. This does NOT search file content. ${paginationHelper.desc}`, - params: { - query: { type: 'string', description: undefined }, - ...paginationHelper.param, - }, - required: ['query'], - }, - - search: { - name: 'search', - description: `Returns all code excerpts containing the given string or grep query. This does NOT search pathname. As a follow-up, you may want to use read_file to view the full file contents of the results. ${paginationHelper.desc}`, - params: { - query: { type: 'string', description: undefined }, - ...paginationHelper.param, - }, - required: ['query'], - }, - - // --- editing (create/delete) --- - - create_uri: { - name: 'create_uri', - description: `Creates a file or folder at the given path. To create a folder, ensure the path ends with a trailing slash. Fails gracefully if the file already exists. Missing ancestors in the path will be recursively created automatically.`, - params: { - uri: { type: 'string', description: undefined }, - }, - required: ['uri'], - }, - - delete_uri: { - name: 'delete_uri', - description: `Deletes the file or folder at the given path. Fails gracefully if the file or folder does not exist.`, - params: { - uri: { type: 'string', description: undefined }, - params: { type: 'string', description: 'Return -r here to delete this URI and all descendants (if applicable). Default is the empty string.' } - }, - required: ['uri', 'params'], - }, - - edit: { // APPLY TOOL - name: 'edit', - description: `Edits the contents of a file at the given URI. Fails gracefully if the file does not exist.`, - params: { - uri: { type: 'string', description: undefined }, - changeDescription: { type: 'string', description: editToolDesc_toolDescription } - }, - required: ['uri', 'changeDescription'], - }, - - terminal_command: { - name: 'terminal_command', - description: `Executes a terminal command.`, - params: { - command: { type: 'string', description: 'The terminal command to execute.' } - }, - required: ['command'], - }, - - - // go_to_definition - // go_to_usages - -} satisfies { [name: string]: InternalToolInfo } - -export type ToolName = keyof typeof voidTools -export const toolNames = Object.keys(voidTools) as ToolName[] - -const toolNamesSet = new Set(toolNames) -export const isAToolName = (toolName: string): toolName is ToolName => { - const isAToolName = toolNamesSet.has(toolName) - return isAToolName -} - - -export const toolNamesThatRequireApproval = new Set(['create_uri', 'delete_uri', 'edit', 'terminal_command'] satisfies ToolName[]) - -type DirectoryItem = { - uri: URI; - name: string; - isDirectory: boolean; - isSymbolicLink: boolean; -} - - -export type ToolCallParams = { - 'read_file': { uri: URI, pageNumber: number }, - 'list_dir': { rootURI: URI, pageNumber: number }, - 'pathname_search': { queryStr: string, pageNumber: number }, - 'search': { queryStr: string, pageNumber: number }, - // --- - 'edit': { uri: URI, changeDescription: string }, - 'create_uri': { uri: URI }, - 'delete_uri': { uri: URI, isRecursive: boolean }, - 'terminal_command': { command: string }, -} - - -export type ToolResultType = { - 'read_file': { fileContents: string, hasNextPage: boolean }, - 'list_dir': { children: DirectoryItem[] | null, hasNextPage: boolean, hasPrevPage: boolean, itemsRemaining: number }, - 'pathname_search': { uris: URI[], hasNextPage: boolean }, - 'search': { uris: URI[], hasNextPage: boolean }, - // --- - 'edit': {}, - 'create_uri': {}, - 'delete_uri': {}, - 'terminal_command': {}, -} - - - -export type ValidateParams = { [T in ToolName]: (p: string) => Promise } -export type CallTool = { [T in ToolName]: (p: ToolCallParams[T]) => Promise } -export type ToolResultToString = { [T in ToolName]: (p: ToolCallParams[T], result: ToolResultType[T]) => string } +type ValidateParams = { [T in ToolName]: (p: string) => Promise } +type CallTool = { [T in ToolName]: (p: ToolCallParams[T]) => Promise } +type ToolResultToString = { [T in ToolName]: (p: ToolCallParams[T], result: ToolResultType[T]) => string } @@ -193,7 +45,7 @@ const computeDirectoryResult = async ( const toChildIdx = MAX_CHILDREN_URIs_PAGE * pageNumber - 1; // INCLUSIVE const listChildren = stat.children?.slice(fromChildIdx, toChildIdx + 1) ?? []; - const children: DirectoryItem[] = listChildren.map(child => ({ + const children: ToolDirectoryItem[] = listChildren.map(child => ({ name: child.name, uri: child.resource, isDirectory: child.isDirectory, @@ -284,6 +136,12 @@ const validateRecursiveParamStr = (paramsUnknown: unknown) => { return isRecursive } +const validateProposedTerminalId = (terminalIdUnknown: unknown) => { + const terminalId = terminalIdUnknown + '' + if (!terminalId) return '' + return terminalId +} + export interface IToolsService { readonly _serviceBrand: undefined; validateParams: ValidateParams; @@ -309,7 +167,7 @@ export class ToolsService implements IToolsService { @IInstantiationService instantiationService: IInstantiationService, @IVoidFileService voidFileService: IVoidFileService, @IEditCodeService editCodeService: IEditCodeService, - // @ITerminalToolService private readonly terminalToolService: ITerminalToolService, + @ITerminalToolService private readonly terminalToolService: ITerminalToolService, ) { const queryBuilder = instantiationService.createInstance(QueryBuilder); @@ -380,9 +238,10 @@ export class ToolsService implements IToolsService { terminal_command: async (s: string) => { const o = validateJSON(s) - const { command: commandUnknown } = o + const { command: commandUnknown, terminalId: terminalIdUnknown } = o const command = validateStr('command', commandUnknown) - return { command } + const proposedTerminalId = validateProposedTerminalId(terminalIdUnknown) + return { command, proposedTerminalId } }, } @@ -454,10 +313,9 @@ export class ToolsService implements IToolsService { await applyDonePromise return {} }, - terminal_command: async ({ command }) => { - // TODO!!!! - // await // Await user confirmation and then command execution before resolving - return {} + terminal_command: async ({ command, proposedTerminalId }) => { + const { terminalId, didCreateTerminal } = await this.terminalToolService.runCommand(command, proposedTerminalId) + return { terminalId, didCreateTerminal } }, } @@ -490,7 +348,7 @@ export class ToolsService implements IToolsService { return `Change successfully made ${params.uri.fsPath} successfully deleted.` }, terminal_command: (params, result) => { - return `Terminal command "${params.command}" successfully executed.` + return `Terminal command "${params.command}" successfully executed in terminal ${result.terminalId}${result.didCreateTerminal ? `(a newly-created terminal)` : ''}.` }, } diff --git a/src/vs/workbench/contrib/void/browser/void.contribution.ts b/src/vs/workbench/contrib/void/browser/void.contribution.ts index f5570488..559c15dc 100644 --- a/src/vs/workbench/contrib/void/browser/void.contribution.ts +++ b/src/vs/workbench/contrib/void/browser/void.contribution.ts @@ -33,6 +33,13 @@ import './media/void.css' import './voidUpdateActions.js' +// tools +import './toolsService.js' +import './terminalToolService.js' + +// register Thread History +import './chatThreadService.js' + // ---------- common (unclear if these actually need to be imported, because they're already imported wherever they're used) ---------- @@ -52,9 +59,3 @@ import '../common/metricsService.js' // updates import '../common/voidUpdateService.js' -// tools -import './toolsService.js' - -// register Thread History -import './chatThreadService.js' - diff --git a/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts b/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts new file mode 100644 index 00000000..19398801 --- /dev/null +++ b/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts @@ -0,0 +1,76 @@ +import { URI } from '../../../../base/common/uri.js'; +import { IRange } from '../../../../editor/common/core/range.js'; +import { AnthropicReasoning } from './sendLLMMessageTypes.js'; +import { ToolName, ToolCallParams, ToolResultType } from './toolsServiceTypes.js'; + +export type ToolMessage = { + role: 'tool'; + name: T; // internal use + paramsStr: string; // internal use + id: string; // apis require this tool use id + content: string; // give this result to LLM + result: { type: 'success'; params: ToolCallParams[T]; value: ToolResultType[T], } | { type: 'error'; value: string }; // give this result to user +} +export type ToolRequestApproval = { + role: 'tool_request'; + name: T; // internal use + params: ToolCallParams[T]; // internal use + voidToolId: string; // internal id Void uses +} + +// WARNING: changing this format is a big deal!!!!!! need to migrate old format to new format on users' computers so people don't get errors. +export type ChatMessage = + | { + role: 'user'; + content: string; // content displayed to the LLM on future calls - allowed to be '', will be replaced with (empty) + displayContent: string | null; // content displayed to user - allowed to be '', will be ignored + selections: StagingSelectionItem[] | null; // the user's selection + state: { + stagingSelections: StagingSelectionItem[]; + isBeingEdited: boolean; + } + } | { + role: 'assistant'; + content: string; // content received from LLM - allowed to be '', will be replaced with (empty) + reasoning: string; // reasoning from the LLM, used for step-by-step thinking + + anthropicReasoning: AnthropicReasoning[] | null; // anthropic reasoning + } + | ToolMessage + | ToolRequestApproval + + +// one of the square items that indicates a selection in a chat bubble (NOT a file, a Selection of text) +export type CodeSelection = { + type: 'Selection'; + fileURI: URI; + selectionStr: string; + range: IRange; + state: { + isOpened: boolean; + }; +} + +export type FileSelection = { + type: 'File'; + fileURI: URI; + selectionStr: null; + range: null; + state: { + isOpened: boolean; + }; +} + +export type StagingSelectionItem = CodeSelection | FileSelection + + + +export type CodespanLocationLink = { + uri: URI, // we handle serialization for this + selection?: { // store as JSON so dont have to worry about serialization + startLineNumber: number + startColumn: number, + endLineNumber: number + endColumn: number, + } | undefined +} | null diff --git a/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts b/src/vs/workbench/contrib/void/common/helpers/extractCodeFromResult.ts similarity index 99% rename from src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts rename to src/vs/workbench/contrib/void/common/helpers/extractCodeFromResult.ts index ee138358..2ec076d3 100644 --- a/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts +++ b/src/vs/workbench/contrib/void/common/helpers/extractCodeFromResult.ts @@ -3,7 +3,7 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import { OnText } from '../../common/sendLLMMessageTypes.js' +import { OnText } from '../sendLLMMessageTypes.js' import { DIVIDER, FINAL, ORIGINAL } from '../prompt/prompts.js' class SurroundingsRemover { diff --git a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts similarity index 98% rename from src/vs/workbench/contrib/void/browser/prompt/prompts.ts rename to src/vs/workbench/contrib/void/common/prompt/prompts.ts index 3199364f..045263b5 100644 --- a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -5,11 +5,11 @@ import { URI } from '../../../../../base/common/uri.js'; -import { filenameToVscodeLanguage } from '../../common/helpers/detectLanguage.js'; -import { CodeSelection, StagingSelectionItem, FileSelection } from '../chatThreadService.js'; +import { filenameToVscodeLanguage } from '../helpers/detectLanguage.js'; import { IModelService } from '../../../../../editor/common/services/model.js'; -import { os } from '../../common/helpers/systemInfo.js'; -import { IVoidFileService } from '../../common/voidFileService.js'; +import { os } from '../helpers/systemInfo.js'; +import { IVoidFileService } from '../voidFileService.js'; +import { CodeSelection, FileSelection, StagingSelectionItem } from '../chatThreadServiceTypes.js'; // this is just for ease of readability diff --git a/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts b/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts index 76d62af9..b27ea20b 100644 --- a/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts +++ b/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts @@ -3,7 +3,7 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import type { InternalToolInfo, ToolName } from '../browser/toolsService.js' +import { ToolName, InternalToolInfo } from './toolsServiceTypes.js' import { ModelSelection, ModelSelectionOptions, ProviderName, SettingsOfProvider } from './voidSettingsTypes.js' diff --git a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts new file mode 100644 index 00000000..d7972a3d --- /dev/null +++ b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts @@ -0,0 +1,162 @@ +import { URI } from '../../../../base/common/uri.js' +import { editToolDesc_toolDescription } from './prompt/prompts.js'; + + + +// we do this using Anthropic's style and convert to OpenAI style later +export type InternalToolInfo = { + name: string, + description: string, + params: { + [paramName: string]: { type: string, description: string | undefined } // name -> type + }, + required: string[], // required paramNames +} + + + + + +export type ToolDirectoryItem = { + uri: URI; + name: string; + isDirectory: boolean; + isSymbolicLink: boolean; +} + + + + + +const paginationHelper = { + desc: `Very large results may be paginated (indicated in the result). Pagination fails gracefully if out of bounds or invalid page number.`, + param: { pageNumber: { type: 'number', description: 'The page number (optional, default is 1).' }, } +} as const + +export const voidTools = { + // --- context-gathering (read/search/list) --- + + read_file: { + name: 'read_file', + description: `Returns file contents of a given URI. ${paginationHelper.desc}`, + params: { + uri: { type: 'string', description: undefined }, + ...paginationHelper.param, + }, + required: ['uri'], + }, + + list_dir: { + name: 'list_dir', + description: `Returns all file names and folder names in a given URI. ${paginationHelper.desc}`, + params: { + uri: { type: 'string', description: undefined }, + ...paginationHelper.param, + }, + required: ['uri'], + }, + + pathname_search: { + name: 'pathname_search', + description: `Returns all pathnames that match a given grep query. You should use this when looking for a file with a specific name or path. This does NOT search file content. ${paginationHelper.desc}`, + params: { + query: { type: 'string', description: undefined }, + ...paginationHelper.param, + }, + required: ['query'], + }, + + search: { + name: 'search', + description: `Returns all code excerpts containing the given string or grep query. This does NOT search pathname. As a follow-up, you may want to use read_file to view the full file contents of the results. ${paginationHelper.desc}`, + params: { + query: { type: 'string', description: undefined }, + ...paginationHelper.param, + }, + required: ['query'], + }, + + // --- editing (create/delete) --- + + create_uri: { + name: 'create_uri', + description: `Creates a file or folder at the given path. To create a folder, ensure the path ends with a trailing slash. Fails gracefully if the file already exists. Missing ancestors in the path will be recursively created automatically.`, + params: { + uri: { type: 'string', description: undefined }, + }, + required: ['uri'], + }, + + delete_uri: { + name: 'delete_uri', + description: `Deletes the file or folder at the given path. Fails gracefully if the file or folder does not exist.`, + params: { + uri: { type: 'string', description: undefined }, + params: { type: 'string', description: 'Return -r here to delete this URI and all descendants (if applicable). Default is the empty string.' } + }, + required: ['uri', 'params'], + }, + + edit: { // APPLY TOOL + name: 'edit', + description: `Edits the contents of a file at the given URI. Fails gracefully if the file does not exist.`, + params: { + uri: { type: 'string', description: undefined }, + changeDescription: { type: 'string', description: editToolDesc_toolDescription } // long description here + }, + required: ['uri', 'changeDescription'], + }, + + terminal_command: { + name: 'terminal_command', + description: `Executes a terminal command.`, + params: { + command: { type: 'string', description: 'The terminal command to execute.' }, + terminalId: { type: 'string', description: 'Optional. The terminal ID to execute the command in. Must be a number starting at 1. If a terminal does not exist with this ID, a new one will be created (not necessarily with the same ID as the provided one). ' }, + }, + required: ['command'], + }, + + + // go_to_definition + // go_to_usages + +} satisfies { [name: string]: InternalToolInfo } + +export type ToolName = keyof typeof voidTools +export const toolNames = Object.keys(voidTools) as ToolName[] + +const toolNamesSet = new Set(toolNames) +export const isAToolName = (toolName: string): toolName is ToolName => { + const isAToolName = toolNamesSet.has(toolName) + return isAToolName +} + + +export const toolNamesThatRequireApproval = new Set(['create_uri', 'delete_uri', 'edit', 'terminal_command'] satisfies ToolName[]) + +export type ToolCallParams = { + 'read_file': { uri: URI, pageNumber: number }, + 'list_dir': { rootURI: URI, pageNumber: number }, + 'pathname_search': { queryStr: string, pageNumber: number }, + 'search': { queryStr: string, pageNumber: number }, + // --- + 'edit': { uri: URI, changeDescription: string }, + 'create_uri': { uri: URI }, + 'delete_uri': { uri: URI, isRecursive: boolean }, + 'terminal_command': { command: string, proposedTerminalId: string }, +} + + +export type ToolResultType = { + 'read_file': { fileContents: string, hasNextPage: boolean }, + 'list_dir': { children: ToolDirectoryItem[] | null, hasNextPage: boolean, hasPrevPage: boolean, itemsRemaining: number }, + 'pathname_search': { uris: URI[], hasNextPage: boolean }, + 'search': { uris: URI[], hasNextPage: boolean }, + // --- + 'edit': {}, + 'create_uri': {}, + 'delete_uri': {}, + 'terminal_command': { terminalId: string, didCreateTerminal: boolean }, +} + 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 dd75882e..abb0bc17 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 @@ -8,12 +8,12 @@ import { Ollama } from 'ollama'; import OpenAI, { ClientOptions } from 'openai'; import { Model as OpenAIModel } from 'openai/resources/models.js'; -import { extractReasoningOnFinalMessage, extractReasoningOnTextWrapper } from '../../browser/helpers/extractCodeFromResult.js'; +import { extractReasoningOnFinalMessage, extractReasoningOnTextWrapper } from '../../common/helpers/extractCodeFromResult.js'; import { LLMChatMessage, LLMFIMMessage, ModelListParams, OllamaModelResponse, OnError, OnFinalMessage, OnText } from '../../common/sendLLMMessageTypes.js'; -import { InternalToolInfo, isAToolName, ToolName } from '../../browser/toolsService.js'; import { defaultProviderSettings, displayInfoOfProviderName, ModelSelectionOptions, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js'; import { prepareFIMMessage, prepareMessages } from './preprocessLLMMessages.js'; import { getModelSelectionState, getModelCapabilities, getProviderCapabilities } from '../../common/modelCapabilities.js'; +import { InternalToolInfo, ToolName, isAToolName } from '../../common/toolsServiceTypes.js'; type InternalCommonMessageParams = { From 217a33cc2e816ca0b5c5434ba20a47eb82247674 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Mon, 10 Mar 2025 00:39:50 -0700 Subject: [PATCH 021/173] tool call improvements (error message + better ux) --- .../contrib/void/browser/chatThreadService.ts | 8 +- .../react/src/markdown/ChatMarkdownRender.tsx | 1 - .../react/src/sidebar-tsx/SidebarChat.tsx | 278 ++++++++++++------ .../void/common/chatThreadServiceTypes.ts | 2 +- 4 files changed, 185 insertions(+), 104 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 7ee42b36..62720353 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -423,7 +423,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { toolParams = params } catch (error) { const errorMessage = getErrorMessage(error) - this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: errorMessage, result: { type: 'error', value: errorMessage }, }) + this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: errorMessage, result: { type: 'error', params: undefined, value: errorMessage }, }) res_() return } @@ -441,7 +441,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { // TODO!!! test rejection // if (Math.random() > 0) throw new Error('TESTING') const errorMessage = 'Tool call was rejected by the user.' - this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: errorMessage, result: { type: 'error', value: errorMessage }, }) + this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: errorMessage, result: { type: 'error', params: toolParams, value: errorMessage }, }) res_() return } @@ -453,7 +453,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { toolResult = await this._toolsService.callTool[toolName](toolParams as any) // typescript is so bad it doesn't even couple the type of ToolResult with the type of the function being called here } catch (error) { const errorMessage = getErrorMessage(error) - this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: errorMessage, result: { type: 'error', value: errorMessage }, }) + this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: errorMessage, result: { type: 'error', params: toolParams, value: errorMessage }, }) res_() return } @@ -464,7 +464,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { toolResultStr = this._toolsService.stringOfResult[toolName](toolParams as any, toolResult as any) } catch (error) { const errorMessage = `Tool call succeeded, but there was an error stringifying the output.\n${getErrorMessage(error)}` - this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: errorMessage, result: { type: 'error', value: errorMessage }, }) + this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: errorMessage, result: { type: 'error', params: toolParams, value: errorMessage }, }) res_() return } diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index c5f8ff42..1b46bef1 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -10,7 +10,6 @@ import { nameToVscodeLanguage } from '../../../../common/helpers/detectLanguage. import { ApplyBlockHoverButtons } from './ApplyBlockHoverButtons.js' import { useAccessor, useChatThreadsState } from '../util/services.js' import { Range } from '../../../../../../services/search/common/searchExtTypes.js' -import { CodespanLocationLink } from '../../../chatThreadService.js' import { IRange } from '../../../../../../../base/common/range.js' import { ScrollType } from '../../../../../../../editor/common/editorCommon.js' 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 8f26dd49..19af982f 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 @@ -29,7 +29,7 @@ import { filenameToVscodeLanguage } from '../../../../common/helpers/detectLangu import { getModelSelectionState, getModelCapabilities } from '../../../../common/modelCapabilities.js'; import { AlertTriangle, ChevronRight, Dot, Pencil, X } from 'lucide-react'; import { ChatMessage, StagingSelectionItem, ToolMessage, ToolRequestApproval } from '../../../../common/chatThreadServiceTypes.js'; -import { ToolName } from '../../../../common/toolsServiceTypes.js'; +import { ToolCallParams, ToolName } from '../../../../common/toolsServiceTypes.js'; @@ -274,7 +274,7 @@ interface VoidChatAreaProps { onAbort: () => void; isStreaming: boolean; isDisabled?: boolean; - divRef?: React.RefObject; + divRef?: React.RefObject; // UI customization className?: string; @@ -659,11 +659,10 @@ export const SelectedFiles = ( interface DropdownComponentProps { title: string; desc1: string; - desc2?: string; + desc2?: React.ReactNode; numResults?: number; children?: React.ReactNode; onClick?: () => void; - icon?: React.ReactNode; } const DropdownComponent = ({ @@ -673,7 +672,6 @@ const DropdownComponent = ({ numResults, children, onClick, - icon, }: DropdownComponentProps) => { const [isExpanded, setIsExpanded] = useState(false); @@ -695,18 +693,21 @@ const DropdownComponent = ({ className={`text-void-fg-3 mr-0.5 h-5 w-5 flex-shrink-0 transition-transform duration-100 ease-[cubic-bezier(0.4,0,0.2,1)] ${isExpanded ? 'rotate-90' : ''}`} /> )} -
- {icon} - {title} - {desc1} - {desc2 && - {desc2} - } - {numResults !== undefined && ( - - {`(`}{numResults}{` result`}{numResults !== 1 ? 's' : ''}{`)`} - - )} +
+
+ {title} + {desc1} +
+
+ {desc2 && + {desc2} + } + {numResults !== undefined && ( + + {`(`}{numResults}{` result`}{numResults !== 1 ? 's' : ''}{`)`} + + )} +
} -
- - } @@ -968,7 +966,6 @@ const AssistantMessageComponent = ({ chatMessage, isLoading, messageIdx }: ChatB {hasReasoning && } > { +const ToolError = ({ title, desc1, errorMessage }: { title: string, desc1: string, errorMessage: string }) => { return ( -
- -
- {title} -
{'Error: ' + errorMessage}
-
-
+ // px-2 py-1 + //
+ // + //
+ // {title + ' error'} + //
{errorMessage}
+ //
+ //
+ + + Error + + } + > +
{errorMessage}
+
+ ) } const toolNameToTitle: Record = { 'read_file': 'Read file', - 'list_dir': 'Inspect folder', - 'pathname_search': 'Search (path only)', - 'search': 'Search (file contents)', + 'list_dir': 'Inspected folder', + 'pathname_search': 'Searched by file name', + 'search': 'Searched files', 'create_uri': 'Create file', 'delete_uri': 'Delete file', 'edit': 'Edit file', 'terminal_command': 'Ran terminal command' } +const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName] | undefined): string => { + + if (_toolParams === undefined) { + return ''; + } + + if (toolName === 'read_file') { + const toolParams = _toolParams as ToolCallParams['read_file'] + return toolParams ? getBasename(toolParams.uri.fsPath) : ''; + } else if (toolName === 'list_dir') { + const toolParams = _toolParams as ToolCallParams['list_dir'] + return toolParams ? `${getBasename(toolParams.rootURI.fsPath)}/` : ''; + } else if (toolName === 'pathname_search') { + const toolParams = _toolParams as ToolCallParams['pathname_search'] + return toolParams ? `"${toolParams.queryStr}"` : ''; + } else if (toolName === 'search') { + const toolParams = _toolParams as ToolCallParams['search'] + return toolParams ? `"${toolParams.queryStr}"` : ''; + } else if (toolName === 'create_uri') { + const toolParams = _toolParams as ToolCallParams['create_uri'] + return toolParams ? getBasename(toolParams.uri.fsPath) : ''; + } else if (toolName === 'delete_uri') { + const toolParams = _toolParams as ToolCallParams['delete_uri'] + return toolParams ? getBasename(toolParams.uri.fsPath) + ' (deleted)' : ''; + } else if (toolName === 'edit') { + const toolParams = _toolParams as ToolCallParams['edit'] + return toolParams ? getBasename(toolParams.uri.fsPath) : ''; + } else if (toolName === 'terminal_command') { + const toolParams = _toolParams as ToolCallParams['terminal_command'] + return toolParams ? `"${toolParams.command}"` : ''; + } else { + return '' + } +} @@ -1028,24 +1073,31 @@ const ToolRequestAcceptRejectButtons = ({ toolRequest }: { toolRequest: ToolRequ const toolNameToComponent: { [T in ToolName]: { requestWrapper: (props: { toolRequest: ToolRequestApproval }) => React.ReactNode, - resultWrapper: (props: { toolMessage: ToolMessage & { result: { type: 'success' } } }) => React.ReactNode, + resultWrapper: (props: { toolMessage: ToolMessage }) => React.ReactNode, } } = { 'read_file': { requestWrapper: ({ toolRequest }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') const title = toolNameToTitle[toolRequest.name] - const { params } = toolRequest - return } - onClick={() => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }} + const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params) + return { commandService.executeCommand('vscode.open', toolRequest.params.uri, { preview: true }) }} /> }, resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') const title = toolNameToTitle[toolMessage.name] + const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params) + + if (toolMessage.result.type === 'error') { + return + } + const { value, params } = toolMessage.result - return }> + + return
{ commandService.executeCommand('vscode.open', params.uri, { preview: true }) }} @@ -1053,7 +1105,7 @@ const toolNameToComponent: { [T in ToolName]: {
{params.uri.fsPath}
- {value.hasNextPage && (
AI can scroll for more content...
)} + {toolMessage.result.value.hasNextPage && (
AI can scroll for more content...
)}
}, @@ -1061,23 +1113,26 @@ const toolNameToComponent: { [T in ToolName]: { 'list_dir': { requestWrapper: ({ toolRequest }) => { const title = toolNameToTitle[toolRequest.name] - const { params } = toolRequest - return } /> + const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params) + return }, resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') const explorerService = accessor.get('IExplorerService') const title = toolNameToTitle[toolMessage.name] - // message.result.hasNextPage = true - // message.result.itemsRemaining = 400 + const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params) + + if (toolMessage.result.type === 'error') { + return + } const { value, params } = toolMessage.result + return } > {value.children?.map((child, i) => (
)} - } }, 'pathname_search': { requestWrapper: ({ toolRequest }) => { const title = toolNameToTitle[toolRequest.name] - const { params } = toolRequest - return } /> + const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params) + return }, resultWrapper: ({ toolMessage }) => { - const accessor = useAccessor() const commandService = accessor.get('ICommandService') const title = toolNameToTitle[toolMessage.name] + const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params) + + if (toolMessage.result.type === 'error') { + return + } const { value, params } = toolMessage.result + return ( } > - { - value.uris.map((uri, i) => ( -
{ - commandService.executeCommand('vscode.open', uri, { preview: true }) - }} - > -
- {uri.fsPath.split('/').pop()} -
- )) - } - { - value.hasNextPage && ( -
- More results available... -
- ) - } -
+ {value.uris.map((uri, i) => ( +
{ + commandService.executeCommand('vscode.open', uri, { preview: true }) + }} + > +
+ {uri.fsPath.split('/').pop()} +
+ ))} + {value.hasNextPage && ( +
+ More results available... +
+ )} +
) } }, 'search': { requestWrapper: ({ toolRequest }) => { const title = toolNameToTitle[toolRequest.name] - const { params } = toolRequest - return } /> + const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params) + return }, resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') const title = toolNameToTitle[toolMessage.name] + const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params) + + if (toolMessage.result.type === 'error') { + return + } const { value, params } = toolMessage.result + return ( } > {value.uris.map((uri, i) => (
{ const title = toolNameToTitle[toolRequest.name] - const { params } = toolRequest - return } /> + const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params) + return }, resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') const title = toolNameToTitle[toolMessage.name] + const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params) + + if (toolMessage.result.type === 'error') { + return + } + const { params } = toolMessage.result + return ( { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }} - icon={} /> ) } @@ -1206,20 +1270,27 @@ const toolNameToComponent: { [T in ToolName]: { const accessor = useAccessor() const commandService = accessor.get('ICommandService') const title = toolNameToTitle[toolRequest.name] - const { params } = toolRequest - return { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }} + const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params) + return { commandService.executeCommand('vscode.open', toolRequest.params.uri, { preview: true }) }} /> }, resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') const title = toolNameToTitle[toolMessage.name] + const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params) + + if (toolMessage.result.type === 'error') { + return + } + const { params } = toolMessage.result + return ( { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }} /> ) @@ -1230,25 +1301,30 @@ const toolNameToComponent: { [T in ToolName]: { const accessor = useAccessor() const commandService = accessor.get('ICommandService') const title = toolNameToTitle[toolRequest.name] - const { params } = toolRequest - return } - onClick={() => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }} + const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params) + return { commandService.executeCommand('vscode.open', toolRequest.params.uri, { preview: true }) }} > - - + + }, resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') const title = toolNameToTitle[toolMessage.name] + const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params) + + if (toolMessage.result.type === 'error') { + return + } const { params } = toolMessage.result + return ( { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }} - icon={} /> ) } @@ -1258,8 +1334,8 @@ const toolNameToComponent: { [T in ToolName]: { const accessor = useAccessor() const commandService = accessor.get('ICommandService') const title = toolNameToTitle[toolRequest.name] - const { params } = toolRequest - return } + const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params) + return }, @@ -1267,13 +1343,18 @@ const toolNameToComponent: { [T in ToolName]: { const accessor = useAccessor() const commandService = accessor.get('ICommandService') const title = toolNameToTitle[toolMessage.name] + const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params) + + if (toolMessage.result.type === 'error') { + return + } const { params } = toolMessage.result + return ( } + desc1={desc1} >
} else if (role === 'tool') { + const title = toolNameToTitle[chatMessage.name] - if (chatMessage.result.type === 'error') return + // if (chatMessage.result.type === 'error') return + const ToolResultComponent = toolNameToComponent[chatMessage.name].resultWrapper as React.FC<{ toolMessage: any }> // ts isnt smart enough... return } diff --git a/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts b/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts index 19398801..0819f242 100644 --- a/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts @@ -9,7 +9,7 @@ export type ToolMessage = { paramsStr: string; // internal use id: string; // apis require this tool use id content: string; // give this result to LLM - result: { type: 'success'; params: ToolCallParams[T]; value: ToolResultType[T], } | { type: 'error'; value: string }; // give this result to user + result: { type: 'success'; params: ToolCallParams[T]; value: ToolResultType[T], } | { type: 'error'; params: ToolCallParams[T] | undefined; value: string }; // give this result to user } export type ToolRequestApproval = { role: 'tool_request'; From 65be47d97052ada9eb0a7c39faf449a7f703011e Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Mon, 10 Mar 2025 00:59:42 -0700 Subject: [PATCH 022/173] ancilary terminal tool --- .../contrib/void/browser/chatThreadService.ts | 6 +- .../contrib/void/browser/editCodeService.ts | 5 +- .../void/browser/terminalToolService.ts | 97 +++++++++++++------ .../contrib/void/browser/toolsService.ts | 18 +++- .../contrib/void/common/prompt/prompts.ts | 11 ++- .../contrib/void/common/toolsServiceTypes.ts | 5 +- 6 files changed, 100 insertions(+), 42 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 62720353..328f4fe9 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -26,6 +26,7 @@ import { ILanguageFeaturesService } from '../../../../editor/common/services/lan import { ITextModelService } from '../../../../editor/common/services/resolverService.js'; import { ChatMessage, CodespanLocationLink, StagingSelectionItem } from '../common/chatThreadServiceTypes.js'; import { Position } from '../../../../editor/common/core/position.js'; +import { ITerminalToolService } from './terminalToolService.js'; const findLastIndex = (arr: T[], condition: (t: T) => boolean): number => { for (let i = arr.length - 1; i >= 0; i--) { @@ -203,6 +204,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { @IVoidSettingsService private readonly _settingsService: IVoidSettingsService, @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, @ITextModelService private readonly _textModelService: ITextModelService, + @ITerminalToolService private readonly terminalToolService: ITerminalToolService, ) { super() this.state = { allThreads: {}, currentThreadId: null as unknown as string } // default state @@ -379,8 +381,10 @@ class ChatThreadService extends Disposable implements IChatThreadService { if (lastUserMsgIdx === -1) throw new Error(`Void: No user message found.`) // should never be -1 + const workspaceFolders = this._workspaceContextService.getWorkspace().folders.map(f => f.uri.fsPath) + const terminalIds = this.terminalToolService.listTerminalIds() const messages: LLMChatMessage[] = [ - { role: 'system', content: chat_systemMessage(this._workspaceContextService.getWorkspace().folders.map(f => f.uri.fsPath), chatMode), }, + { role: 'system', content: chat_systemMessage(workspaceFolders, terminalIds, chatMode), }, ...messages_.slice(0, lastUserMsgIdx), { role: 'user', content: userMessageFullContent }, ...messages_.slice(lastUserMsgIdx + 1, Infinity), diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 5c973961..eba51a9c 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -1527,11 +1527,12 @@ class EditCodeService extends Disposable implements IEditCodeService { } + const errHelper = (erroneousOriginal: string) => `All previous SEARCH/REPLACE blocks (if any) have been applied except the latest erroneous one. Please continue outputting SEARCH/REPLACE blocks. The ORIGINAL code with an error was: ${JSON.stringify(erroneousOriginal)}` const errMsgOfInvalidStr = (str: string & ReturnType, blockOrig: string) => { return str === `Not found` ? - `The ORIGINAL code provided could not be found in the file. Please output all SEARCH/REPLACE blocks again, making sure the code in ORIGINAL is identical to a code snippet in the file. The ORIGINAL code provided: ${JSON.stringify(blockOrig)}` + `The ORIGINAL code provided could not be found in the file. You should make sure the text in ORIGINAL matches lines of code EXACTLY. ${errHelper(blockOrig)}` : str === `Not unique` ? - `The ORIGINAL code provided shows up multiple times in the file. Please output all SEARCH/REPLACE blocks again, making sure the code in each ORIGINAL section is unique in the file. The ORIGINAL code provided: ${JSON.stringify(blockOrig)}` + `The ORIGINAL code provided shows up multiple times in the file. We recommend making the ORIGINAL portion bigger so we can find a unique match. ${errHelper(blockOrig)}` : `` } diff --git a/src/vs/workbench/contrib/void/browser/terminalToolService.ts b/src/vs/workbench/contrib/void/browser/terminalToolService.ts index 6940151e..a9f2e08a 100644 --- a/src/vs/workbench/contrib/void/browser/terminalToolService.ts +++ b/src/vs/workbench/contrib/void/browser/terminalToolService.ts @@ -6,36 +6,65 @@ import { Disposable } from '../../../../base/common/lifecycle.js'; import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; +import { TerminalCapability } from '../../../../platform/terminal/common/capabilities/capabilities.js'; import { TerminalLocation } from '../../../../platform/terminal/common/terminal.js'; -import { ITerminalService, ITerminalInstance } from '../../../../workbench/contrib/terminal/browser/terminal.js'; +import { ITerminalService, ITerminalInstance, ITerminalInstanceService } from '../../../../workbench/contrib/terminal/browser/terminal.js'; export interface ITerminalToolService { readonly _serviceBrand: undefined; - runCommand(command: string, proposedTerminalId: string): Promise<{ terminalId: string, didCreateTerminal: boolean }>; + runCommand(command: string, proposedTerminalId: string, waitForCompletion: boolean): Promise<{ terminalId: string, didCreateTerminal: boolean, contents: string }>; + listTerminalIds(): string[]; } export const ITerminalToolService = createDecorator('TerminalToolService'); + +const nameOfId = (id: string) => { + if (id === '1') return 'Void Agent' + return `Void Agent (${id})` +} +const idOfName = (name: string) => { + if (name === 'Void Agent') return '1' + + const match = name.match(/Void Agent \((\d+)\)/) + if (!match) return null + if (Number.isInteger(match[1]) && Number(match[1]) >= 1) return match[1] + return null +} + export class TerminalToolService extends Disposable implements ITerminalToolService { readonly _serviceBrand: undefined; private terminalInstanceOfId: Record = {} constructor( - @ITerminalService private readonly terminalService: ITerminalService, + @ITerminalInstanceService private readonly terminalInstanceService: ITerminalInstanceService, ) { super(); + + // initialize any terminals that are already open + + for (const terminal of terminalService.instances) { + const proposedTerminalId = idOfName(terminal.title) + if (proposedTerminalId) this.terminalInstanceOfId[proposedTerminalId] = terminal + } + console.log('Initialized terminal instances:', this.terminalInstanceOfId) + } - + listTerminalIds() { + return Object.keys(this.terminalInstanceOfId) + } getValidNewTerminalId(): string { // {1 2 3} # size 3, new=4 // {1 3 4} # size 3, new=2 // 1 <= newTerminalId <= n + 1 const n = Object.keys(this.terminalInstanceOfId).length; + if (n === 0) return '1' + for (let i = 1; i <= n + 1; i++) { const potentialId = i + ''; if (!(potentialId in this.terminalInstanceOfId)) return potentialId; @@ -44,45 +73,59 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ } - private async _createNewTerminal() { + + private async _getOrCreateTerminal(proposedTerminalId: string) { + // if terminal ID exists, return it + if (proposedTerminalId in this.terminalInstanceOfId) return { terminalId: proposedTerminalId, didCreateTerminal: false } + // create new terminal and return its ID const terminalId = this.getValidNewTerminalId(); const terminal = await this.terminalService.createTerminal({ location: TerminalLocation.Panel, - config: { name: `Void Agent (${terminalId})`, } + config: { name: nameOfId(terminalId), title: nameOfId(terminalId) } }); this.terminalInstanceOfId[terminalId] = terminal - return terminalId; - } - - private async _getValidTerminalId(proposedTerminalId: string) { - // if there is no terminal ID provided, create one - if (proposedTerminalId in this.terminalInstanceOfId) - return { terminalId: proposedTerminalId, didCreateTerminal: false } - const terminalId = await this._createNewTerminal() return { terminalId, didCreateTerminal: true } } - private async _focus(terminalId: string) { - const terminal = this.terminalInstanceOfId[terminalId]; - if (!terminal) return - terminal.focus(true); - return; - } - async runCommand(command: string, proposedTerminalId: string) { + runCommand: ITerminalToolService['runCommand'] = async (command, proposedTerminalId, waitForCompletion) => { await this.terminalService.whenConnected; - const { terminalId, didCreateTerminal } = await this._getValidTerminalId(proposedTerminalId) + const { terminalId, didCreateTerminal } = await this._getOrCreateTerminal(proposedTerminalId) const terminal = this.terminalInstanceOfId[terminalId]; if (!terminal) throw new Error(`Unexpected internal error: Terminal with ID ${terminalId} did not exist.`); - this._focus(terminalId) + + + if (!waitForCompletion) { + console.log('NOT WAITING FOR COMPLETION') + await terminal.sendText(command, true); + return { terminalId, didCreateTerminal, contents: '(command is running in background...)' }; + } + + // stream + + let data = '' + const d1 = terminal.onData(newData => { data += newData }) + await terminal.sendText(command, true); - // terminal.onData(data => console.log('DATA!!', data)); - // terminal.onProcessReplayComplete(data => console.log('REPLAY!!', data)); - // terminal.onDidSendText(data => console.log('SEND!!', data)); - return { terminalId, didCreateTerminal }; + // wait for the command to finish + const commandDetection = terminal.capabilities.get(TerminalCapability.CommandDetection); + if (commandDetection) { + const d2 = commandDetection.onCommandFinished(() => { + console.log('FINISHED', data) + d1.dispose() + d2.dispose() + return { terminalId, didCreateTerminal, contents: data } + }) + } + + console.log('didnot wait', data) + d1.dispose() + return { terminalId, didCreateTerminal, contents: 'Could not await data...' } } + + } registerSingleton(ITerminalToolService, TerminalToolService, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index f7272bf8..cc851e10 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -137,11 +137,18 @@ const validateRecursiveParamStr = (paramsUnknown: unknown) => { } const validateProposedTerminalId = (terminalIdUnknown: unknown) => { + if (!terminalIdUnknown) return '1' const terminalId = terminalIdUnknown + '' - if (!terminalId) return '' return terminalId } +const validateWaitForCompletion = (b: unknown) => { + if (typeof b === 'string') { + if (b === 'true') return true + if (b === 'false') return false + } + return true // default is true +} export interface IToolsService { readonly _serviceBrand: undefined; validateParams: ValidateParams; @@ -238,10 +245,11 @@ export class ToolsService implements IToolsService { terminal_command: async (s: string) => { const o = validateJSON(s) - const { command: commandUnknown, terminalId: terminalIdUnknown } = o + const { command: commandUnknown, terminalId: terminalIdUnknown, waitForCompletion: waitForCompletionUnknown } = o const command = validateStr('command', commandUnknown) const proposedTerminalId = validateProposedTerminalId(terminalIdUnknown) - return { command, proposedTerminalId } + const waitForCompletion = validateWaitForCompletion(waitForCompletionUnknown) + return { command, proposedTerminalId, waitForCompletion } }, } @@ -313,8 +321,8 @@ export class ToolsService implements IToolsService { await applyDonePromise return {} }, - terminal_command: async ({ command, proposedTerminalId }) => { - const { terminalId, didCreateTerminal } = await this.terminalToolService.runCommand(command, proposedTerminalId) + terminal_command: async ({ command, proposedTerminalId, waitForCompletion }) => { + const { terminalId, didCreateTerminal } = await this.terminalToolService.runCommand(command, proposedTerminalId, waitForCompletion) return { terminalId, didCreateTerminal } }, } diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index 045263b5..e61fff3c 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -23,15 +23,17 @@ Do NOT output the whole file if possible, and try to write as LITTLE as needed t -export const chat_systemMessage = (workspaces: string[], mode: 'agent' | 'gather' | 'chat') => `\ +export const chat_systemMessage = (workspaces: string[], runningTerminalIds: string[], mode: 'agent' | 'gather' | 'chat') => `\ You are a coding ${mode === 'agent' ? 'agent' : 'assistant'}. Your job is to help the user ${mode === 'agent' ? 'make changes to their codebase' : 'search and understand their codebase'}. You will be given instructions to follow from the user, \`INSTRUCTIONS\`. You may also be given a list of files that the user has specifically selected, \`SELECTIONS\`. Please assist the user with their query. The user's query is never invalid. The user's system information is as follows: - ${os} -- Open workspaces: ${workspaces.join(', ')} - +- Open workspace(s): ${workspaces.join(', ') || 'NO WORKSPACE OPEN'} +${(mode === 'agent' || mode === 'gather') && runningTerminalIds.length !== 0 ? `\ +- Running terminal IDs: ${runningTerminalIds.join(', ')} +`: '\n'} ${mode === 'agent' || mode === 'gather' /* tool use */ ? `\ You will be given tools you can call. - Only use tools if they help you accomplish the user's goal. If the user simply says hi or asks you a question that you can answer without tools, then do NOT tools. @@ -45,8 +47,7 @@ You're allowed to ask for more context. For example, if the user only gives you `} ${mode === 'agent' /* code blocks */ ? `\ -Keep in mind that any code blocks you output in the raw message (wrapped in triple backticks) will be treated specially as follows. This does NOT apply to code blocks in tool calls. -- Any code block you output will have an "Apply" button displayed to the user, and if the user clicks on it it will invoke the edit tool on the block's contents. As a result, all code blocks should describe relevant changes. +If you have a change to make, you should almost always use a tool to edit the file. Even if you don't (e.g. if the user asks you not to), you should still NEVER re-write the entire file for the user. Instead, you should write comments like "// ... existing code" to indicate how to change the existing code. `: `\ If you think it's appropriate to suggest an edit to a file, then you must describe your suggestion in CODE BLOCK(S) (wrapped in triple backticks). - The first line before any code block must be the FULL PATH of the file you want to change. If the path does not already exist, it will be created. diff --git a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts index d7972a3d..b4d00691 100644 --- a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts @@ -112,7 +112,8 @@ export const voidTools = { description: `Executes a terminal command.`, params: { command: { type: 'string', description: 'The terminal command to execute.' }, - terminalId: { type: 'string', description: 'Optional. The terminal ID to execute the command in. Must be a number starting at 1. If a terminal does not exist with this ID, a new one will be created (not necessarily with the same ID as the provided one). ' }, + waitForCompletion: { type: 'string', description: `Whether or not to await the command to complete and get the final result. Default is true. Make this value false when you want a command to run indefinitely without waiting for it.` }, + terminalId: { type: 'string', description: 'Optional (if provided, value must be an integer >= 1). This is the ID of the terminal instance to execute the command in. The primary purpose of this is to start a new terminal for background processes or tasks that run indefinitely (e.g. if you want to run a server locally). Fails gracefully if a terminal ID does not exist, by creating a new terminal instance. Defaults to the preferred terminal ID.' }, }, required: ['command'], }, @@ -144,7 +145,7 @@ export type ToolCallParams = { 'edit': { uri: URI, changeDescription: string }, 'create_uri': { uri: URI }, 'delete_uri': { uri: URI, isRecursive: boolean }, - 'terminal_command': { command: string, proposedTerminalId: string }, + 'terminal_command': { command: string, proposedTerminalId: string, waitForCompletion: boolean }, } From ab8147934077c41dcfaeb43798baa97de6bc2fc8 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Mon, 10 Mar 2025 15:35:57 -0700 Subject: [PATCH 023/173] terminalService --- .../contrib/void/browser/editCodeService.ts | 1 + .../react/src/markdown/ApplyBlockHoverButtons.tsx | 14 +++++++------- .../browser/react/src/sidebar-tsx/SidebarChat.tsx | 2 +- .../contrib/void/browser/terminalToolService.ts | 8 ++++++-- .../contrib/void/common/prompt/prompts.ts | 3 ++- 5 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index eba51a9c..7535dadc 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -1628,6 +1628,7 @@ class EditCodeService extends Disposable implements IEditCodeService { // REVERT + // TODO!!!!! don't actually revert - we want to change this so it doesn't revert but isntead gives the current file contents const numLines = this._getNumLines(uri) if (numLines !== null) this._writeText(uri, originalFileCode, { startLineNumber: 1, startColumn: 1, endLineNumber: numLines, endColumn: Number.MAX_SAFE_INTEGER }, 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 832d7eb6..90ec360f 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 @@ -34,10 +34,10 @@ const CopyButton = ({ codeStr }: { codeStr: string }) => { metricsService.capture('Copy Code', { length: codeStr.length }) // capture the length only }, [metricsService, clipboardService, codeStr, setCopyButtonText]) - const isSingleLine = !codeStr.includes('\n') + const isSingleLine = false //!codeStr.includes('\n') return const stopButton = +) + + +export const IconShell2 = ({ onClick, title, Icon, disabled, className }: IconButtonProps) => ( + +) + const COPY_FEEDBACK_TIMEOUT = 1000 // amount of time to say 'Copied!' const CopyButton = ({ codeStr }: { codeStr: string }) => { @@ -26,7 +75,6 @@ const CopyButton = ({ codeStr }: { codeStr: string }) => { }, COPY_FEEDBACK_TIMEOUT) }, [copyButtonText]) - const onCopy = useCallback(() => { clipboardService.writeText(codeStr) .then(() => { setCopyButtonText(CopyButtonText.Copied) }) @@ -34,26 +82,20 @@ const CopyButton = ({ codeStr }: { codeStr: string }) => { metricsService.capture('Copy Code', { length: codeStr.length }) // capture the length only }, [metricsService, clipboardService, codeStr, setCopyButtonText]) - const isSingleLine = false //!codeStr.includes('\n') - - return + title={copyButtonText} + /> } - - - // state persisted for duration of react only +// TODO change this to use type `ChatThreads.applyBoxState[applyBoxId]` const applyingURIOfApplyBoxIdRef: { current: { [applyBoxId: string]: URI | undefined } } = { current: {} } - -export const ApplyBlockHoverButtons = ({ codeStr, applyBoxId }: { codeStr: string, applyBoxId: string }) => { +export const useApplyButtonHTML = ({ codeStr, applyBoxId }: { codeStr: string, applyBoxId: string }) => { const settingsState = useSettingsState() const isDisabled = !!isFeatureNameDisabled('Apply', settingsState) || !applyBoxId @@ -64,21 +106,21 @@ export const ApplyBlockHoverButtons = ({ codeStr, applyBoxId }: { codeStr: strin const [_, rerender] = useState(0) - const applyingUri = useCallback(() => applyingURIOfApplyBoxIdRef.current[applyBoxId] ?? null, [applyBoxId]) - const streamState = useCallback(() => editCodeService.getURIStreamState({ uri: applyingUri() }), [editCodeService, applyingUri]) + const getUriBeingApplied = useCallback(() => applyingURIOfApplyBoxIdRef.current[applyBoxId] ?? null, [applyBoxId]) + const getStreamState = useCallback(() => editCodeService.getURIStreamState({ uri: getUriBeingApplied() }), [editCodeService, getUriBeingApplied]) // listen for stream updates useURIStreamState( useCallback((uri, newStreamState) => { - const shouldUpdate = applyingUri()?.fsPath !== uri.fsPath - if (shouldUpdate) return + const shouldUpdate = getUriBeingApplied()?.fsPath === uri.fsPath + if (!shouldUpdate) return rerender(c => c + 1) - }, [applyBoxId, editCodeService, applyingUri]) + }, [applyBoxId, editCodeService, getUriBeingApplied]) ) const onSubmit = useCallback(() => { if (isDisabled) return - if (streamState() === 'streaming') return + if (getStreamState() === 'streaming') return const [newApplyingUri, _] = editCodeService.startApplying({ from: 'ClickApply', type: 'searchReplace', @@ -88,61 +130,122 @@ export const ApplyBlockHoverButtons = ({ codeStr, applyBoxId }: { codeStr: strin applyingURIOfApplyBoxIdRef.current[applyBoxId] = newApplyingUri ?? undefined rerender(c => c + 1) metricsService.capture('Apply Code', { length: codeStr.length }) // capture the length only - }, [isDisabled, streamState, editCodeService, codeStr, applyBoxId, metricsService]) + }, [isDisabled, getStreamState, editCodeService, codeStr, applyBoxId, metricsService]) const onInterrupt = useCallback(() => { - if (streamState() !== 'streaming') return - const uri = applyingUri() + if (getStreamState() !== 'streaming') return + const uri = getUriBeingApplied() if (!uri) return editCodeService.interruptURIStreaming({ uri }) metricsService.capture('Stop Apply', {}) - }, [streamState, applyingUri, editCodeService, metricsService]) + }, [getStreamState, getUriBeingApplied, editCodeService, metricsService]) + + const onAccept = useCallback(() => { + const uri = getUriBeingApplied() + if (uri) editCodeService.removeDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false }) + }, [getUriBeingApplied, editCodeService]) + + const onReject = useCallback(() => { + const uri = getUriBeingApplied() + if (uri) editCodeService.removeDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false }) + }, [getUriBeingApplied, editCodeService]) + + const onReapply = useCallback(() => { + onReject() + onSubmit() + }, [onReject, onSubmit]) + + const currStreamState = getStreamState() + + const copyButton = ( + + ) + + const playButton = ( + + ) + + const stopButton = ( + + ) + + const reapplyButton = ( + + ) + + const acceptButton = ( + + ) + + const rejectButton = ( + + ) - const isSingleLine = false //!codeStr.includes('\n') + let buttonsHTML = <> - const applyButton = + if (currStreamState === 'streaming') { + buttonsHTML = <> + {stopButton} + + } - const stopButton = + if (currStreamState === 'idle') { + buttonsHTML = <> + {copyButton} + {playButton} + + } - const acceptRejectButtons = <> - - - +
+
+ + return { + statusIndicatorHTML, + buttonsHTML + } + - const currStreamState = streamState() - return <> - {currStreamState !== 'streaming' && } - {currStreamState === 'idle' && !isDisabled && applyButton} - {currStreamState === 'streaming' && stopButton} - {currStreamState === 'acceptRejectAll' && acceptRejectButtons} - } diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/BlockCode.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/BlockCode.tsx index f7954b82..c1cd1de2 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/BlockCode.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/BlockCode.tsx @@ -3,17 +3,44 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import React from 'react'; - import { VoidCodeEditor, VoidCodeEditorProps } from '../util/inputs.js'; +import { useApplyButtonHTML } from './ApplyBlockHoverButtons.js'; + +export const BlockCodeWithApply = ({ initValue, language, applyBoxId }: { initValue: string, language?: string, applyBoxId: string }) => { + + const { statusIndicatorHTML, buttonsHTML } = useApplyButtonHTML({ codeStr: initValue, applyBoxId }) + + return ( +
+
+
+
{language || 'text'}
+ {statusIndicatorHTML} +
+
+ {buttonsHTML} +
+
+ + + +
+ ) +} -export const BlockCode = ({ buttonsOnHover, ...codeEditorProps }: { buttonsOnHover?: React.ReactNode } & VoidCodeEditorProps) => { +export const BlockCode = ({ ...codeEditorProps }: VoidCodeEditorProps) => { + const isSingleLine = !codeEditorProps.initValue.includes('\n') return ( <> -
+ + + {/*
{buttonsOnHover === null ? null : (
@@ -23,7 +50,8 @@ export const BlockCode = ({ buttonsOnHover, ...codeEditorProps }: { buttonsOnHov )} -
+
*/} + ) } diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index 1b46bef1..baa0620e 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -5,9 +5,9 @@ import React, { JSX, useState } from 'react' import { marked, MarkedToken, Token } from 'marked' -import { BlockCode } from './BlockCode.js' +import { BlockCode, BlockCodeWithApply } from './BlockCode.js' import { nameToVscodeLanguage } from '../../../../common/helpers/detectLanguage.js' -import { ApplyBlockHoverButtons } from './ApplyBlockHoverButtons.js' +import { useApplyButtonHTML } from './ApplyBlockHoverButtons.js' import { useAccessor, useChatThreadsState } from '../util/services.js' import { Range } from '../../../../../../services/search/common/searchExtTypes.js' import { IRange } from '../../../../../../../base/common/range.js' @@ -56,7 +56,7 @@ const CodespanWithLink = ({ text, rawText, chatMessageLocation }: { text: string link = chatThreadService.getCodespanLink({ codespanStr: text, messageIdx, threadId }) if (link === undefined) { - // generate link and add to cache + // if no link, generate link and add to cache (chatThreadService.generateCodespanLink(text) .then(link => { chatThreadService.addCodespanLink({ newLinkText: text, newLinkLocation: link, messageIdx, threadId }) @@ -99,7 +99,9 @@ const CodespanWithLink = ({ text, rawText, chatMessageLocation }: { text: string /> } -const RenderToken = ({ token, nested, chatMessageLocation, tokenIdx }: { token: Token | string, nested?: boolean, chatMessageLocation?: ChatMessageLocation, tokenIdx: string }): JSX.Element => { + +export type RenderTokenOptions = { isApplyEnabled?: boolean, isLinkDetectionEnabled?: boolean } +const RenderToken = ({ token, nested, chatMessageLocation, tokenIdx, ...options }: { token: Token | string, nested?: boolean, chatMessageLocation?: ChatMessageLocation, tokenIdx: string, } & RenderTokenOptions): JSX.Element => { // deal with built-in tokens first (assume marked token) const t = token as MarkedToken @@ -114,21 +116,29 @@ const RenderToken = ({ token, nested, chatMessageLocation, tokenIdx }: { token: if (t.type === "code") { - const applyBoxId = chatMessageLocation ? getApplyBoxId({ - threadId: chatMessageLocation.threadId, - messageIdx: chatMessageLocation.messageIdx, - tokenIdx: tokenIdx, - }) : null + const language = t.lang === undefined ? undefined : nameToVscodeLanguage[t.lang] // TODO user should only be able to apply this when the code has been closed (t.raw ends with "```") - return
- } + language={language} + applyBoxId={applyBoxId} /> -
+ } + + return } if (t.type === "heading") { @@ -213,7 +223,7 @@ const RenderToken = ({ token, nested, chatMessageLocation, tokenIdx }: { token: return
  • - +
  • } @@ -229,7 +239,7 @@ const RenderToken = ({ token, nested, chatMessageLocation, tokenIdx }: { token: )} - + ))} @@ -244,6 +254,7 @@ const RenderToken = ({ token, nested, chatMessageLocation, tokenIdx }: { token: token={token} tokenIdx={`${tokenIdx ? `${tokenIdx}-` : ''}${index}`} // assign a unique tokenId to nested components chatMessageLocation={chatMessageLocation} + {...options} /> ))} @@ -304,12 +315,15 @@ const RenderToken = ({ token, nested, chatMessageLocation, tokenIdx }: { token: // inline code if (t.type === "codespan") { - if (chatMessageLocation) { + console.log('isLinkDetectionEnabled', options.isLinkDetectionEnabled) + if (options.isLinkDetectionEnabled && chatMessageLocation) { + return + } return @@ -331,12 +345,12 @@ const RenderToken = ({ token, nested, chatMessageLocation, tokenIdx }: { token: ) } -export const ChatMarkdownRender = ({ string, nested = false, chatMessageLocation }: { string: string, nested?: boolean, chatMessageLocation: ChatMessageLocation | undefined }) => { +export const ChatMarkdownRender = ({ string, nested = false, chatMessageLocation, ...options }: { string: string, nested?: boolean, chatMessageLocation: ChatMessageLocation | undefined } & RenderTokenOptions) => { const tokens = marked.lexer(string); // https://marked.js.org/using_pro#renderer return ( <> {tokens.map((token, index) => ( - + ))} ) 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 1ee518e1..b0bafb9e 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 @@ -714,7 +714,7 @@ const DropdownComponent = ({ // the py-1 here makes sure all elements in the container have py-2 total. this makes a nice animation effect during transition. className={`overflow-hidden transition-all duration-200 ease-in-out ${isExpanded ? 'opacity-100 py-1' : 'max-h-0 opacity-0'}`} > -
    +
    {children}
    @@ -971,6 +971,8 @@ const AssistantMessageComponent = ({ chatMessage, isLoading, messageIdx, isLast } @@ -978,6 +980,8 @@ const AssistantMessageComponent = ({ chatMessage, isLoading, messageIdx, isLast {/* loading indicator */} @@ -1009,7 +1013,7 @@ const ToolError = ({ title, desc1, errorMessage }: { title: string, desc1: strin } > -
    {errorMessage}
    +
    {errorMessage}
    ) @@ -1028,34 +1032,34 @@ const toolNameToTitle: Record = { } const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName] | undefined): string => { - if (_toolParams === undefined) { + if (!_toolParams) { return ''; } if (toolName === 'read_file') { const toolParams = _toolParams as ToolCallParams['read_file'] - return toolParams ? getBasename(toolParams.uri.fsPath) : ''; + return getBasename(toolParams.uri.fsPath); } else if (toolName === 'list_dir') { const toolParams = _toolParams as ToolCallParams['list_dir'] - return toolParams ? `${getBasename(toolParams.rootURI.fsPath)}/` : ''; + return `${getBasename(toolParams.rootURI.fsPath)}/`; } else if (toolName === 'pathname_search') { const toolParams = _toolParams as ToolCallParams['pathname_search'] - return toolParams ? `"${toolParams.queryStr}"` : ''; + return `"${toolParams.queryStr}"`; } else if (toolName === 'search') { const toolParams = _toolParams as ToolCallParams['search'] - return toolParams ? `"${toolParams.queryStr}"` : ''; + return `"${toolParams.queryStr}"`; } else if (toolName === 'create_uri') { const toolParams = _toolParams as ToolCallParams['create_uri'] - return toolParams ? getBasename(toolParams.uri.fsPath) : ''; + return getBasename(toolParams.uri.fsPath); } else if (toolName === 'delete_uri') { const toolParams = _toolParams as ToolCallParams['delete_uri'] - return toolParams ? getBasename(toolParams.uri.fsPath) + ' (deleted)' : ''; + return getBasename(toolParams.uri.fsPath) + ' (deleted)'; } else if (toolName === 'edit') { const toolParams = _toolParams as ToolCallParams['edit'] - return toolParams ? getBasename(toolParams.uri.fsPath) : ''; + return getBasename(toolParams.uri.fsPath); } else if (toolName === 'terminal_command') { const toolParams = _toolParams as ToolCallParams['terminal_command'] - return toolParams ? `"${toolParams.command}"` : ''; + return `"${toolParams.command}"`; } else { return '' } @@ -1063,13 +1067,22 @@ const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName -const ToolRequestAcceptRejectButtons = ({ toolRequest }: { toolRequest: ToolRequestApproval }) => { +const ToolRequestAcceptRejectButtons = ({ toolRequest, messageIdx, isLast, }: { toolRequest: ToolRequestApproval } & Omit) => { const accessor = useAccessor() const chatThreadsService = accessor.get('IChatThreadService') - return <> -
    { chatThreadsService.approveTool(toolRequest.voidToolId) }}>Accept
    -
    { chatThreadsService.rejectTool(toolRequest.voidToolId) }}>Reject
    - + + const initRequestState = isLast ? 'awaiting_response' : 'rejected' + + const [requestState, setRequestState] = useState<'accepted' | 'rejected' | 'awaiting_response'>(initRequestState) + + + if (requestState === 'awaiting_response') { + return <> +
    { chatThreadsService.approveTool(toolRequest.voidToolId); setRequestState('accepted') }}>Accept
    +
    { chatThreadsService.rejectTool(toolRequest.voidToolId); setRequestState('rejected') }}>Reject
    + + } + } const toolNameToComponent: { [T in ToolName]: { @@ -1306,7 +1319,10 @@ const toolNameToComponent: { [T in ToolName]: { return { commandService.executeCommand('vscode.open', toolRequest.params.uri, { preview: true }) }} > - + }, resultWrapper: ({ toolMessage }) => { @@ -1412,10 +1428,10 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx, isLast }: ChatBubblePr else if (role === 'tool_request') { const isLastMessage = true // TODO!!! fix this if (!isLastMessage) return null - const ToolRequestComponent = toolNameToComponent[chatMessage.name].requestWrapper as React.FC<{ toolRequest: any }> // ts isnt smart enough... + const ToolRequestWrapper = toolNameToComponent[chatMessage.name].requestWrapper as React.FC<{ toolRequest: any }> // ts isnt smart enough... return <> - - + + } else if (role === 'tool') { @@ -1423,8 +1439,8 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx, isLast }: ChatBubblePr const title = toolNameToTitle[chatMessage.name] // if (chatMessage.result.type === 'error') return - const ToolResultComponent = toolNameToComponent[chatMessage.name].resultWrapper as React.FC<{ toolMessage: any }> // ts isnt smart enough... - return + const ToolResultWrapper = toolNameToComponent[chatMessage.name].resultWrapper as React.FC<{ toolMessage: any }> // ts isnt smart enough... + return } @@ -1524,19 +1540,19 @@ export const SidebarChat = () => { scrollContainerRef.current?.scrollTo({ top: 0, left: 0 }) }, [isHistoryOpen, currentThread.id]) + const numMessages = previousMessages.length + (isStreaming ? 1 : 0) - const pastMessagesHTML = useMemo(() => { + const previousMessagesHTML = useMemo(() => { return previousMessages.map((message, i) => - + ) }, [previousMessages, currentThread]) - - - const streamingChatIdx = pastMessagesHTML.length + const streamingChatIdx = previousMessagesHTML.length const currStreamingMessageHTML = !!(reasoningSoFar || messageSoFar || isStreaming) ? { isLast={true} /> : null - const allMessagesHTML = [...pastMessagesHTML, currStreamingMessageHTML] - + const allMessagesHTML = [...previousMessagesHTML, currStreamingMessageHTML] const threadSelector =
    {
    - - const messagesHTML = { w-full h-auto overflow-x-hidden overflow-y-auto - ${pastMessagesHTML.length === 0 && !messageSoFar ? 'hidden' : ''} + ${previousMessagesHTML.length === 0 && !messageSoFar ? 'hidden' : ''} `} style={{ maxHeight: sidebarDimensions.height - historyDimensions.height - chatAreaDimensions.height - (25) }} // the height of the previousMessages is determined by all other heights > @@ -1608,7 +1621,7 @@ export const SidebarChat = () => { isStreaming={isStreaming} isDisabled={isDisabled} showSelections={true} - showProspectiveSelections={pastMessagesHTML.length === 0} + showProspectiveSelections={previousMessagesHTML.length === 0} selections={selections} setSelections={setSelections} onClickAnywhere={() => { textAreaRef.current?.focus() }} diff --git a/src/vs/workbench/services/themes/common/workbenchThemeService.ts b/src/vs/workbench/services/themes/common/workbenchThemeService.ts index 505dd21c..52ec138d 100644 --- a/src/vs/workbench/services/themes/common/workbenchThemeService.ts +++ b/src/vs/workbench/services/themes/common/workbenchThemeService.ts @@ -39,12 +39,12 @@ export enum ThemeSettings { } export enum ThemeSettingDefaults { - COLOR_THEME_DARK = 'Default Dark+', + COLOR_THEME_DARK = 'Default Dark+', // Void changed this from 'Default Dark Modern' COLOR_THEME_LIGHT = 'Default Light Modern', COLOR_THEME_HC_DARK = 'Default High Contrast', COLOR_THEME_HC_LIGHT = 'Default High Contrast Light', - COLOR_THEME_DARK_OLD = 'Default Dark Modern', + COLOR_THEME_DARK_OLD = 'Default Dark Modern', // Void changed this from 'Default Dark+' COLOR_THEME_LIGHT_OLD = 'Default Light+', FILE_ICON_THEME = 'vs-seti', From 6d5d4c34d76fcaf7289cff75bf8a0cc7eca56d36 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Wed, 12 Mar 2025 00:11:14 -0700 Subject: [PATCH 032/173] size --- .../browser/react/src/markdown/ApplyBlockHoverButtons.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 7cd3e405..1cc29bc0 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 @@ -27,7 +27,7 @@ export const IconShell1 = ({ onClick, title, Icon, disabled, className }: IconBu disabled={disabled} onClick={onClick} className={` - size-6 + size-[18px] flex items-center justify-center text-sm bg-void-bg-3 text-void-fg-1 hover:brightness-110 @@ -36,7 +36,7 @@ export const IconShell1 = ({ onClick, title, Icon, disabled, className }: IconBu ${className} `} > - + ) @@ -47,7 +47,7 @@ export const IconShell2 = ({ onClick, title, Icon, disabled, className }: IconBu disabled={disabled} onClick={onClick} className={` - size-6 + size-[18px] flex items-center justify-center text-sm hover:opacity-80 @@ -55,7 +55,7 @@ export const IconShell2 = ({ onClick, title, Icon, disabled, className }: IconBu ${className} `} > - + ) From 094e05070a2ded28db978a897612cabff536ab01 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Wed, 12 Mar 2025 00:19:50 -0700 Subject: [PATCH 033/173] size --- .../browser/react/src/markdown/ApplyBlockHoverButtons.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 1cc29bc0..d44dfcf7 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 @@ -27,7 +27,7 @@ export const IconShell1 = ({ onClick, title, Icon, disabled, className }: IconBu disabled={disabled} onClick={onClick} className={` - size-[18px] + size-[24px] flex items-center justify-center text-sm bg-void-bg-3 text-void-fg-1 hover:brightness-110 @@ -36,7 +36,7 @@ export const IconShell1 = ({ onClick, title, Icon, disabled, className }: IconBu ${className} `} > - + ) @@ -47,7 +47,7 @@ export const IconShell2 = ({ onClick, title, Icon, disabled, className }: IconBu disabled={disabled} onClick={onClick} className={` - size-[18px] + size-[24px] flex items-center justify-center text-sm hover:opacity-80 @@ -55,7 +55,7 @@ export const IconShell2 = ({ onClick, title, Icon, disabled, className }: IconBu ${className} `} > - + ) From fa1f407b76c90baf562ca2f045a85aa10db96f76 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Wed, 12 Mar 2025 00:37:30 -0700 Subject: [PATCH 034/173] fix naming for tool response --- .../react/src/sidebar-tsx/SidebarChat.tsx | 64 ++++++++++++++++--- 1 file changed, 54 insertions(+), 10 deletions(-) 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 b0bafb9e..172cc5b9 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 @@ -1025,10 +1025,10 @@ const toolNameToTitle: Record = { 'list_dir': 'Inspected folder', 'pathname_search': 'Searched by file name', 'search': 'Searched files', - 'create_uri': 'Create file', - 'delete_uri': 'Delete file', - 'edit': 'Edit file', - 'terminal_command': 'Run terminal command' + 'create_uri': 'Created file', + 'delete_uri': 'Deleted file', + 'edit': 'Edited file', + 'terminal_command': 'Ran terminal command' } const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName] | undefined): string => { @@ -1067,22 +1067,66 @@ const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName -const ToolRequestAcceptRejectButtons = ({ toolRequest, messageIdx, isLast, }: { toolRequest: ToolRequestApproval } & Omit) => { +const ToolRequestAcceptRejectButtons = ({ toolRequest, messageIdx, isLast }: { toolRequest: ToolRequestApproval } & Omit) => { const accessor = useAccessor() const chatThreadsService = accessor.get('IChatThreadService') + const metricsService = accessor.get('IMetricsService') const initRequestState = isLast ? 'awaiting_response' : 'rejected' - const [requestState, setRequestState] = useState<'accepted' | 'rejected' | 'awaiting_response'>(initRequestState) + const onAccept = useCallback(() => { + chatThreadsService.approveTool(toolRequest.voidToolId) + setRequestState('accepted') + metricsService.capture('Tool Request Accepted', {}) + }, [chatThreadsService, toolRequest.voidToolId, metricsService]) + + const onReject = useCallback(() => { + chatThreadsService.rejectTool(toolRequest.voidToolId) + setRequestState('rejected') + metricsService.capture('Tool Request Rejected', {}) + }, [chatThreadsService, toolRequest.voidToolId, metricsService]) + + const approveButton = ( + + ) + + const cancelButton = ( + + ) if (requestState === 'awaiting_response') { - return <> -
    { chatThreadsService.approveTool(toolRequest.voidToolId); setRequestState('accepted') }}>Accept
    -
    { chatThreadsService.rejectTool(toolRequest.voidToolId); setRequestState('rejected') }}>Reject
    - + return
    + {approveButton} + {cancelButton} +
    } + return null } const toolNameToComponent: { [T in ToolName]: { From 1ef8011bb0eea00bbda5a2897a58e61c1e7fbafe Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Tue, 11 Mar 2025 21:00:45 -0700 Subject: [PATCH 035/173] tool UI step 1 (BROKEN) --- .../void/browser/autocompleteService.ts | 1 - .../react/src/sidebar-tsx/SidebarChat.tsx | 155 ++++++++---------- .../void/browser/terminalToolService.ts | 8 +- .../contrib/void/common/toolsServiceTypes.ts | 4 +- 4 files changed, 73 insertions(+), 95 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/autocompleteService.ts b/src/vs/workbench/contrib/void/browser/autocompleteService.ts index cc9f3a92..6479fb6d 100644 --- a/src/vs/workbench/contrib/void/browser/autocompleteService.ts +++ b/src/vs/workbench/contrib/void/browser/autocompleteService.ts @@ -942,4 +942,3 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ registerWorkbenchContribution2(AutocompleteService.ID, AutocompleteService, WorkbenchPhase.BlockRestore); - 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 172cc5b9..eb91346a 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 @@ -29,7 +29,7 @@ import { filenameToVscodeLanguage } from '../../../../common/helpers/detectLangu import { getModelSelectionState, getModelCapabilities } from '../../../../common/modelCapabilities.js'; import { AlertTriangle, ChevronRight, Dot, Pencil, X } from 'lucide-react'; import { ChatMessage, StagingSelectionItem, ToolMessage, ToolRequestApproval } from '../../../../common/chatThreadServiceTypes.js'; -import { ToolCallParams, ToolName } from '../../../../common/toolsServiceTypes.js'; +import { ToolCallParams, ToolName, ToolNameWithApproval } from '../../../../common/toolsServiceTypes.js'; @@ -654,25 +654,29 @@ export const SelectedFiles = ( +const ReasoningComponent = ({ children }: { children: React.ReactNode }) => { + return children -interface DropdownComponentProps { - title: string; - desc1: string; - desc2?: React.ReactNode; - numResults?: number; - children?: React.ReactNode; - onClick?: () => void; } -const DropdownComponent = ({ + + +const ToolComponent = ({ title, desc1, desc2, numResults, children, onClick, -}: DropdownComponentProps) => { +}: { + title: string; + desc1: string; + desc2?: React.ReactNode; + numResults?: number; + children?: React.ReactNode; + onClick?: () => void; +}) => { const [isExpanded, setIsExpanded] = useState(false); const isDropdown = !!children @@ -690,7 +694,7 @@ const DropdownComponent = ({ > {isDropdown && ( )}
    @@ -964,17 +968,14 @@ const AssistantMessageComponent = ({ chatMessage, isLoading, messageIdx, isLast > {/* reasoning token */} - {hasReasoning && + {hasReasoning && - } + } {/* assistant message */} {errorMessage}
    //
    //
    - } > -
    {errorMessage}
    -
    +
    {errorMessage}
    + ) } @@ -1066,7 +1067,6 @@ const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName } - const ToolRequestAcceptRejectButtons = ({ toolRequest, messageIdx, isLast }: { toolRequest: ToolRequestApproval } & Omit) => { const accessor = useAccessor() const chatThreadsService = accessor.get('IChatThreadService') @@ -1130,19 +1130,11 @@ const ToolRequestAcceptRejectButtons = ({ toolRequest, messageIdx, isLast }: { t } const toolNameToComponent: { [T in ToolName]: { - requestWrapper: (props: { toolRequest: ToolRequestApproval }) => React.ReactNode, + requestWrapper: T extends ToolNameWithApproval ? ((props: { toolRequest: ToolRequestApproval }) => React.ReactNode) : null, resultWrapper: (props: { toolMessage: ToolMessage }) => React.ReactNode, } } = { 'read_file': { - requestWrapper: ({ toolRequest }) => { - const accessor = useAccessor() - const commandService = accessor.get('ICommandService') - const title = toolNameToTitle[toolRequest.name] - const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params) - return { commandService.executeCommand('vscode.open', toolRequest.params.uri, { preview: true }) }} - /> - }, + requestWrapper: null, resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') @@ -1155,7 +1147,7 @@ const toolNameToComponent: { [T in ToolName]: { const { value, params } = toolMessage.result - return + return
    { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }} @@ -1165,15 +1157,11 @@ const toolNameToComponent: { [T in ToolName]: {
    {toolMessage.result.value.hasNextPage && (
    AI can scroll for more content...
    )} -
    + }, }, 'list_dir': { - requestWrapper: ({ toolRequest }) => { - const title = toolNameToTitle[toolRequest.name] - const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params) - return - }, + requestWrapper: null, resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') @@ -1187,7 +1175,7 @@ const toolNameToComponent: { [T in ToolName]: { const { value, params } = toolMessage.result - return )} - + } }, 'pathname_search': { - requestWrapper: ({ toolRequest }) => { - const title = toolNameToTitle[toolRequest.name] - const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params) - return - }, + requestWrapper: null, resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') @@ -1232,7 +1216,7 @@ const toolNameToComponent: { [T in ToolName]: { const { value, params } = toolMessage.result return ( - )} - + ) } }, 'search': { - requestWrapper: ({ toolRequest }) => { - const title = toolNameToTitle[toolRequest.name] - const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params) - return - }, + requestWrapper: null, resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') @@ -1277,7 +1257,7 @@ const toolNameToComponent: { [T in ToolName]: { const { value, params } = toolMessage.result return ( - ))} {value.hasNextPage && (
    More results available...
    )} -
    + ) } }, + + // --- + 'create_uri': { requestWrapper: ({ toolRequest }) => { const title = toolNameToTitle[toolRequest.name] const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params) - return + return }, resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() @@ -1315,7 +1298,7 @@ const toolNameToComponent: { [T in ToolName]: { const { params } = toolMessage.result return ( - { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }} @@ -1329,7 +1312,7 @@ const toolNameToComponent: { [T in ToolName]: { const commandService = accessor.get('ICommandService') const title = toolNameToTitle[toolRequest.name] const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params) - return { commandService.executeCommand('vscode.open', toolRequest.params.uri, { preview: true }) }} /> }, @@ -1346,7 +1329,7 @@ const toolNameToComponent: { [T in ToolName]: { const { params } = toolMessage.result return ( - { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }} @@ -1360,14 +1343,11 @@ const toolNameToComponent: { [T in ToolName]: { const commandService = accessor.get('ICommandService') const title = toolNameToTitle[toolRequest.name] const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params) - return { commandService.executeCommand('vscode.open', toolRequest.params.uri, { preview: true }) }} > - - + + }, resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() @@ -1382,7 +1362,7 @@ const toolNameToComponent: { [T in ToolName]: { const { params } = toolMessage.result return ( - { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }} @@ -1398,7 +1378,7 @@ const toolNameToComponent: { [T in ToolName]: { const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params) const { waitForCompletion, command, proposedTerminalId } = toolRequest.params - return }, @@ -1417,29 +1397,27 @@ const toolNameToComponent: { [T in ToolName]: { const { terminalId, resolveReason, result } = toolMessage.result.value return ( - -
    -
    -
    - {resolveReason.type === 'bgtask' ? 'Result so far:' : null} - - { - resolveReason.type === 'done' ? (resolveReason.exitCode !== 0 ? `Error: exit code ${resolveReason.exitCode}` : null) - : resolveReason.type === 'bgtask' ? null : - resolveReason.type === 'timeout' ? `(partial results; request timed out)` : - resolveReason.type === 'toofull' ? `(truncated)` - : null - } -
    + +
    + {resolveReason.type === 'bgtask' ? 'Result so far:' : null} + {result} + { + resolveReason.type === 'done' ? (resolveReason.exitCode !== 0 ? ` + Error: exit code ${resolveReason.exitCode}` : null) + : resolveReason.type === 'bgtask' ? null : + resolveReason.type === 'timeout' ? ` + (partial results; request timed out)` : + resolveReason.type === 'toofull' ? ` + (truncated)` + : null + }
    - + ) } } @@ -1473,16 +1451,10 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx, isLast }: ChatBubblePr const isLastMessage = true // TODO!!! fix this if (!isLastMessage) return null const ToolRequestWrapper = toolNameToComponent[chatMessage.name].requestWrapper as React.FC<{ toolRequest: any }> // ts isnt smart enough... - return <> - - - + return + } else if (role === 'tool') { - - const title = toolNameToTitle[chatMessage.name] - // if (chatMessage.result.type === 'error') return - const ToolResultWrapper = toolNameToComponent[chatMessage.name].resultWrapper as React.FC<{ toolMessage: any }> // ts isnt smart enough... return } @@ -1692,3 +1664,4 @@ export const SidebarChat = () => {
    } + diff --git a/src/vs/workbench/contrib/void/browser/terminalToolService.ts b/src/vs/workbench/contrib/void/browser/terminalToolService.ts index 9d418cfe..511a8285 100644 --- a/src/vs/workbench/contrib/void/browser/terminalToolService.ts +++ b/src/vs/workbench/contrib/void/browser/terminalToolService.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------*/ import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js'; +import { removeAnsiEscapeCodes } from '../../../../base/common/strings.js'; import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { TerminalLocation } from '../../../../platform/terminal/common/terminal.js'; @@ -171,6 +172,11 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ console.log('res', { terminalId, didCreateTerminal, result, resolveReason }) + result = removeAnsiEscapeCodes(result) + .split('\n').slice(1, -1) // remove first and last line (first = command, last = andrewpareles/void %) + .join('\n') + + console.log('TerminalToolService: Command completed:', JSON.stringify(result)) return { terminalId, didCreateTerminal, result, resolveReason } } @@ -180,5 +186,3 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ } registerSingleton(ITerminalToolService, TerminalToolService, InstantiationType.Delayed); - - diff --git a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts index 3c17853d..c6358b37 100644 --- a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts @@ -137,7 +137,9 @@ export const isAToolName = (toolName: string): toolName is ToolName => { } -export const toolNamesThatRequireApproval = new Set(['create_uri', 'delete_uri', 'edit', 'terminal_command'] satisfies ToolName[]) +const toolNamesWithApproval = ['create_uri', 'delete_uri', 'edit', 'terminal_command'] as const satisfies readonly ToolName[] +export type ToolNameWithApproval = typeof toolNamesWithApproval[number] +export const toolNamesThatRequireApproval = new Set(toolNamesWithApproval) export type ToolCallParams = { 'read_file': { uri: URI, pageNumber: number }, From 11d376325e5be4240ecd4839d9a62a0f7403460e Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Wed, 12 Mar 2025 00:46:58 -0700 Subject: [PATCH 036/173] tool UI progress + terminal fix + misc fixes --- .../contrib/void/browser/chatThreadService.ts | 4 +- .../contrib/void/browser/editCodeService.ts | 2 +- .../react/src/sidebar-tsx/SidebarChat.tsx | 514 ++++++++++-------- .../void/browser/react/src/util/services.tsx | 2 + .../void/browser/terminalToolService.ts | 83 ++- .../contrib/void/browser/toolsService.ts | 29 +- .../void/common/chatThreadServiceTypes.ts | 7 +- .../contrib/void/common/prompt/prompts.ts | 5 +- .../contrib/void/common/toolsServiceTypes.ts | 4 +- .../contrib/void/common/voidSettingsTypes.ts | 3 +- .../llmMessage/preprocessLLMMessages.ts | 1 - 11 files changed, 388 insertions(+), 266 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 77fec1ba..cc2abbb7 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -189,10 +189,10 @@ class ChatThreadService extends Disposable implements IChatThreadService { private readonly _onDidChangeCurrentThread = new Emitter(); readonly onDidChangeCurrentThread: Event = this._onDidChangeCurrentThread.event; - readonly streamState: ThreadStreamState = {} private readonly _onDidChangeStreamState = new Emitter<{ threadId: string }>(); readonly onDidChangeStreamState: Event<{ threadId: string }> = this._onDidChangeStreamState.event; + readonly streamState: ThreadStreamState = {} state: ThreadsState // allThreads is persisted, currentThread is not constructor( @@ -445,7 +445,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { // TODO!!! test rejection // if (Math.random() > 0) throw new Error('TESTING') const errorMessage = 'Tool call was rejected by the user.' - this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: errorMessage, result: { type: 'error', params: toolParams, value: errorMessage }, }) + this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: errorMessage, result: { type: 'rejected', params: toolParams, value: errorMessage }, }) shouldSendAnotherMessage = false // interrupt flow by rejecting res_() return diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index ae16d111..9fd24052 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -53,7 +53,7 @@ const configOfBG = (color: Color) => { const greenBG = new Color(new RGBA(155, 185, 85, .1)); // default is RGBA(155, 185, 85, .2) registerColor('void.greenBG', configOfBG(greenBG), '', true); -const redBG = new Color(new RGBA(255, 0, 0, .2)); // default is RGBA(255, 0, 0, .2) +const redBG = new Color(new RGBA(255, 0, 0, .05)); // default is RGBA(255, 0, 0, .2) registerColor('void.redBG', configOfBG(redBG), '', true); const sweepBG = new Color(new RGBA(100, 100, 100, .2)); 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 eb91346a..5c1cc3e7 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 @@ -656,52 +656,69 @@ export const SelectedFiles = ( const ReasoningComponent = ({ children }: { children: React.ReactNode }) => { + const [isOpen, setIsOpen] = useState(false) return children } -const ToolComponent = ({ +type ToolHeaderParams = { + icon?: React.ReactNode; + title: string; + desc1: string; + desc2?: React.ReactNode; + isError?: boolean; + requestToolId?: string; + numResults?: number; + children?: React.ReactNode; + isLastMessage?: boolean; + onClick?: () => void; +} + +const ToolHeaderComponent = ({ + icon, title, desc1, desc2, numResults, children, + isError, + requestToolId, + isLastMessage, onClick, -}: { - title: string; - desc1: string; - desc2?: React.ReactNode; - numResults?: number; - children?: React.ReactNode; - onClick?: () => void; -}) => { +}: ToolHeaderParams) => { + const [isExpanded, setIsExpanded] = useState(false); const isDropdown = !!children const isClickable = !!isDropdown || !!onClick - return ( -
    -
    -
    { - if (children) { setIsExpanded(v => !v); } - if (onClick) { onClick(); } - }} - > - {isDropdown && ( - - )} -
    -
    - {title} - {desc1} -
    + return (
    +
    + + {/* header */} +
    { + if (children) { setIsExpanded(v => !v); } + if (onClick) { onClick(); } + }} + > + {isDropdown && ( + + )} +
    +
    + {title} + {desc1} +
    +
    {desc2 && {desc2} @@ -711,19 +728,23 @@ const ToolComponent = ({ {`(`}{numResults}{` result`}{numResults !== 1 ? 's' : ''}{`)`} )} + {isError && }
    -
    -
    - {children} -
    +
    + {/* children */} +
    +
    + {children}
    + {!requestToolId ? null : } +
    ); }; @@ -994,42 +1015,15 @@ const AssistantMessageComponent = ({ chatMessage, isLoading, messageIdx, isLast -const ToolError = ({ title, desc1, errorMessage }: { title: string, desc1: string, errorMessage: string }) => { - return ( - // px-2 py-1 - //
    - // - //
    - // {title + ' error'} - //
    {errorMessage}
    - //
    - //
    - - - Error - - } - > -
    {errorMessage}
    -
    - - ) -} - - const toolNameToTitle: Record = { 'read_file': 'Read file', - 'list_dir': 'Inspected folder', - 'pathname_search': 'Searched by file name', - 'search': 'Searched files', - 'create_uri': 'Created file', - 'delete_uri': 'Deleted file', - 'edit': 'Edited file', - 'terminal_command': 'Ran terminal command' + 'list_dir': 'Inspect folder', + 'pathname_search': 'Search by file name', + 'search': 'Search', + 'create_uri': 'Create file', + 'delete_uri': 'Delete file', + 'edit': 'Edit file', + 'terminal_command': 'Run terminal command' } const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName] | undefined): string => { @@ -1067,7 +1061,7 @@ const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName } -const ToolRequestAcceptRejectButtons = ({ toolRequest, messageIdx, isLast }: { toolRequest: ToolRequestApproval } & Omit) => { +const ToolRequestAcceptRejectButtons = ({ voidToolId, isLastMessage: isLast }: { voidToolId: string, isLastMessage: boolean }) => { const accessor = useAccessor() const chatThreadsService = accessor.get('IChatThreadService') const metricsService = accessor.get('IMetricsService') @@ -1076,16 +1070,16 @@ const ToolRequestAcceptRejectButtons = ({ toolRequest, messageIdx, isLast }: { t const [requestState, setRequestState] = useState<'accepted' | 'rejected' | 'awaiting_response'>(initRequestState) const onAccept = useCallback(() => { - chatThreadsService.approveTool(toolRequest.voidToolId) + chatThreadsService.approveTool(voidToolId) setRequestState('accepted') metricsService.capture('Tool Request Accepted', {}) - }, [chatThreadsService, toolRequest.voidToolId, metricsService]) + }, [chatThreadsService, voidToolId, metricsService]) const onReject = useCallback(() => { - chatThreadsService.rejectTool(toolRequest.voidToolId) + chatThreadsService.rejectTool(voidToolId) setRequestState('rejected') metricsService.capture('Tool Request Rejected', {}) - }, [chatThreadsService, toolRequest.voidToolId, metricsService]) + }, [chatThreadsService, voidToolId, metricsService]) const approveButton = (
    - {!requestToolId ? null : }
    ); }; @@ -1061,23 +1056,18 @@ const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName } -const ToolRequestAcceptRejectButtons = ({ voidToolId, isLastMessage: isLast }: { voidToolId: string, isLastMessage: boolean }) => { +const ToolRequestAcceptRejectButtons = ({ voidToolId }: { voidToolId: string }) => { const accessor = useAccessor() const chatThreadsService = accessor.get('IChatThreadService') const metricsService = accessor.get('IMetricsService') - const initRequestState = isLast ? 'awaiting_response' : 'rejected' - const [requestState, setRequestState] = useState<'accepted' | 'rejected' | 'awaiting_response'>(initRequestState) - const onAccept = useCallback(() => { chatThreadsService.approveTool(voidToolId) - setRequestState('accepted') metricsService.capture('Tool Request Accepted', {}) }, [chatThreadsService, voidToolId, metricsService]) const onReject = useCallback(() => { chatThreadsService.rejectTool(voidToolId) - setRequestState('rejected') metricsService.capture('Tool Request Rejected', {}) }, [chatThreadsService, voidToolId, metricsService]) @@ -1113,18 +1103,14 @@ const ToolRequestAcceptRejectButtons = ({ voidToolId, isLastMessage: isLast }: { ) - if (requestState === 'awaiting_response') { - return
    - {approveButton} - {cancelButton} -
    - } - - return null + return
    + {approveButton} + {cancelButton} +
    } const toolNameToComponent: { [T in ToolName]: { - requestWrapper: T extends ToolNameWithApproval ? ((props: { toolRequest: ToolRequestApproval, isLastMessage: boolean }) => React.ReactNode) : null, + requestWrapper: T extends ToolNameWithApproval ? ((props: { toolRequest: ToolRequestApproval }) => React.ReactNode) : null, resultWrapper: (props: { toolMessage: ToolMessage }) => React.ReactNode, } } = { 'read_file': { @@ -1291,7 +1277,7 @@ const toolNameToComponent: { [T in ToolName]: { // --- 'create_uri': { - requestWrapper: ({ toolRequest, isLastMessage }) => { + requestWrapper: ({ toolRequest }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') const title = toolNameToTitle[toolRequest.name] @@ -1299,7 +1285,7 @@ const toolNameToComponent: { [T in ToolName]: { const icon = null const isError = false - const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isLastMessage, requestToolId: toolRequest.voidToolId } + const componentParams: ToolHeaderParams = { title, desc1, isError, icon, } const { params } = toolRequest componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } @@ -1332,7 +1318,7 @@ const toolNameToComponent: { [T in ToolName]: { } }, 'delete_uri': { - requestWrapper: ({ toolRequest, isLastMessage }) => { + requestWrapper: ({ toolRequest, }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') const title = toolNameToTitle[toolRequest.name] @@ -1340,7 +1326,7 @@ const toolNameToComponent: { [T in ToolName]: { const icon = null const isError = false - const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isLastMessage, requestToolId: toolRequest.voidToolId } + const componentParams: ToolHeaderParams = { title, desc1, isError, icon, } const { params } = toolRequest componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } @@ -1373,7 +1359,7 @@ const toolNameToComponent: { [T in ToolName]: { } }, 'edit': { - requestWrapper: ({ toolRequest, isLastMessage }) => { + requestWrapper: ({ toolRequest, }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') const title = toolNameToTitle[toolRequest.name] @@ -1381,7 +1367,7 @@ const toolNameToComponent: { [T in ToolName]: { const icon = null const isError = false - const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isLastMessage, requestToolId: toolRequest.voidToolId } + const componentParams: ToolHeaderParams = { title, desc1, isError, icon, } const { params } = toolRequest componentParams.children = @@ -1416,7 +1402,7 @@ const toolNameToComponent: { [T in ToolName]: { } }, 'terminal_command': { - requestWrapper: ({ toolRequest, isLastMessage }) => { + requestWrapper: ({ toolRequest, }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') const terminalToolsService = accessor.get('ITerminalToolService') @@ -1425,7 +1411,7 @@ const toolNameToComponent: { [T in ToolName]: { const icon = null const isError = false - const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isLastMessage, requestToolId: toolRequest.voidToolId } + const componentParams: ToolHeaderParams = { title, desc1, isError, icon, } const { proposedTerminalId } = toolRequest.params if (terminalToolsService.terminalExists(proposedTerminalId)) @@ -1514,16 +1500,18 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx, isLast }: ChatBubblePr /> } else if (role === 'tool_request') { - const ToolRequestWrapper = toolNameToComponent[chatMessage.name].requestWrapper as React.FC<{ toolRequest: any, isLastMessage: boolean }> // ts isnt smart enough... - return - + const ToolRequestWrapper = toolNameToComponent[chatMessage.name].requestWrapper as React.FC<{ toolRequest: any }> // ts isnt smart enough... + if (!isLast) return null + return <> + + + } else if (role === 'tool') { const ToolResultWrapper = toolNameToComponent[chatMessage.name].resultWrapper as React.FC<{ toolMessage: any }> // ts isnt smart enough... return } - } From 09ba9387bc9d28803f2b54d88fd7db06cbf810e1 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Wed, 12 Mar 2025 02:54:36 -0700 Subject: [PATCH 040/173] fix refactor --- .../void/browser/react/src/sidebar-tsx/SidebarChat.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 aa7b7120..a278f4ca 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 @@ -730,14 +730,14 @@ const ToolHeaderComponent = ({
    {/* children */} -
    {children}
    -
    +
    }
    ); From 1c2e8c0c598c7ca0c4e2935a8be7dfa7cbff6c13 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Wed, 12 Mar 2025 03:09:00 -0700 Subject: [PATCH 041/173] minh --- .../contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a278f4ca..bf355b6c 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 @@ -1695,7 +1695,7 @@ export const SidebarChat = () => { onClickAnywhere={() => { textAreaRef.current?.focus() }} > 0 ? 'min-h-[9px]' : 'min-h-[81px]'} px-0.5`} placeholder={`${keybindingString ? `${keybindingString} to select. ` : ''}Enter instructions...`} onChangeText={onChangeText} onKeyDown={onKeyDown} From f66fbcd911b825698754ae9e4bfad8fb6d3914e4 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Wed, 12 Mar 2025 03:19:20 -0700 Subject: [PATCH 042/173] agentLoop improvements --- .../contrib/void/browser/chatThreadService.ts | 353 ++++++++++-------- .../contrib/void/common/prompt/prompts.ts | 10 +- 2 files changed, 194 insertions(+), 169 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 76cb2c1e..1246fd06 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -17,7 +17,7 @@ import { IWorkspaceContextService } from '../../../../platform/workspace/common/ import { IVoidFileService } from '../common/voidFileService.js'; import { generateUuid } from '../../../../base/common/uuid.js'; import { getErrorMessage } from '../../../../base/common/errors.js'; -import { ChatMode, FeatureName } from '../common/voidSettingsTypes.js'; +import { ChatMode, FeatureName, ModelSelection, ModelSelectionOptions } from '../common/voidSettingsTypes.js'; import { IVoidSettingsService } from '../common/voidSettingsService.js'; import { ToolName, ToolCallParams, ToolResultType, InternalToolInfo, voidTools, toolNamesThatRequireApproval } from '../common/toolsServiceTypes.js'; import { IToolsService } from './toolsService.js'; @@ -317,6 +317,9 @@ class ChatThreadService extends Disposable implements IChatThreadService { private resRejOfToolAwaitingApproval: { [toolId: string]: { res: () => void, rej: () => void } } = {} approveTool(toolId: string) { + // TODO!!! if not streaming, approveToolAndStreamResponse + + // if streaming, do below const resRej = this.resRejOfToolAwaitingApproval[toolId] delete this.resRejOfToolAwaitingApproval[toolId] resRej?.res() @@ -328,6 +331,191 @@ class ChatThreadService extends Disposable implements IChatThreadService { } + + + private _staticAgentLoopsProps = () => { + // these settings should not change throughout the loop (eg anthropic breaks if you change its thinking mode and it's using tools) + const featureName: FeatureName = 'Chat' + const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName] + const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[modelSelection.providerName]?.[modelSelection.modelName] : undefined + return { modelSelection, modelSelectionOptions } + } + + + + private async _agentLoop({ threadId, tools, prevSelns, currSelns, modelSelection, modelSelectionOptions, chatMode, userMessageContent }: { + tools: InternalToolInfo[] | undefined, + threadId: string, + prevSelns: StagingSelectionItem[], + currSelns: StagingSelectionItem[], + modelSelection: ModelSelection | null, + modelSelectionOptions: ModelSelectionOptions | undefined, + chatMode: ChatMode, + userMessageContent: string, // content of LATEST user message + }) { + this._setStreamState(threadId, { error: undefined }) // clear any previous error + + let nMessagesSent = 0 + let shouldSendAnotherMessage = true + + while (shouldSendAnotherMessage) { + // recompute files in last message + const selectionsStr = await chat_selectionsString(prevSelns, currSelns, this._voidFileService) // all the file CONTENTS or "selections" de-duped + const userMessageFullContent = chat_lastUserMessageWithFilesAdded(userMessageContent, selectionsStr) // full last message: user message + CONTENTS of all files + + nMessagesSent += 1 + shouldSendAnotherMessage = false // false by default + + let resMessageIsDonePromise: () => void // resolves when user approves this tool use (or if tool doesn't require approval) + const messageIsDonePromise = new Promise((res, rej) => { resMessageIsDonePromise = res }) + + // replace last userMessage with userMessageFullContent (which contains all the files too) + const messages_ = toLLMChatMessages(this.getCurrentThread().messages) + const lastUserMsgIdx = findLastIndex(messages_, m => m.role === 'user') + + if (lastUserMsgIdx === -1) throw new Error(`Void: No user message found.`) // should never be -1 + + // system message + const workspaceFolders = this._workspaceContextService.getWorkspace().folders.map(f => f.uri.fsPath) + const terminalIds = this.terminalToolService.listTerminalIds() + const systemMessage = chat_systemMessage(workspaceFolders, terminalIds, chatMode) + + // all messages so far in the chat history (including tools) + const messages: LLMChatMessage[] = [ + { role: 'system', content: systemMessage, }, + ...messages_.slice(0, lastUserMsgIdx), + { role: 'user', content: userMessageFullContent }, + ...messages_.slice(lastUserMsgIdx + 1, Infinity), + ] + + // send llm message + const llmCancelToken = this._llmMessageService.sendLLMMessage({ + messagesType: 'chatMessages', + messages, + tools: tools, + modelSelection, + modelSelectionOptions, + logging: { loggingName: `Agent` }, + onText: ({ fullText, fullReasoning }) => { this._setStreamState(threadId, { messageSoFar: fullText, reasoningSoFar: fullReasoning }) }, + onFinalMessage: async ({ fullText, toolCalls, fullReasoning, anthropicReasoning }) => { + + this._addMessageToThread(threadId, { role: 'assistant', content: fullText, reasoning: fullReasoning, anthropicReasoning }) + this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, }) // added to history, so clear messages so far + + // if no tool, finish + const tool: ToolCallType | undefined = toolCalls?.[0] + if (!tool) { + this._setStreamState(threadId, { streamingToken: undefined }) + resMessageIsDonePromise() + return + } + + // if tool + // clear messageSoFar since we added it to the chat history (but don't clear streamingToken, we're still streaming) + this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined }) + + // deal with the tool + const toolName: ToolName = tool.name + shouldSendAnotherMessage = true + + // 1. validate tool params + let toolParams: ToolCallParams[ToolName] + try { + const params = await this._toolsService.validateParams[toolName](tool.paramsStr) + toolParams = params + } catch (error) { + const errorMessage = getErrorMessage(error) + this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: errorMessage, result: { type: 'error', params: undefined, value: errorMessage }, }) + this._setStreamState(threadId, { streamingToken: undefined }) + resMessageIsDonePromise() + return + } + + // 2. if tool requires approval, await the approval + if (toolNamesThatRequireApproval.has(toolName)) { + const voidToolId = generateUuid() + const toolApprovalPromise = new Promise((res, rej) => { this.resRejOfToolAwaitingApproval[voidToolId] = { res, rej } }) + this._addMessageToThread(threadId, { role: 'tool_request', name: toolName, params: toolParams, voidToolId: voidToolId }) + try { + await toolApprovalPromise + // accepted tool + } + catch (e) { + // TODO!!! test rejection + // if (Math.random() > 0) throw new Error('TESTING') + shouldSendAnotherMessage = false // interrupt flow by rejecting + + const errorMessage = 'Tool call was rejected by the user.' + this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: errorMessage, result: { type: 'rejected', params: toolParams, value: errorMessage }, }) + this._setStreamState(threadId, { streamingToken: undefined }) + resMessageIsDonePromise() + return + } + } + + // 3. call the tool + let toolResult: ToolResultType[typeof toolName] + try { + toolResult = await this._toolsService.callTool[toolName](toolParams as any) // ts is bad... + } catch (error) { + const errorMessage = getErrorMessage(error) + this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: errorMessage, result: { type: 'error', params: toolParams, value: errorMessage }, }) + this._setStreamState(threadId, { streamingToken: undefined }) + resMessageIsDonePromise() + return + } + + // 4. stringify the result to give to the LLM + let toolResultStr: string + try { + toolResultStr = this._toolsService.stringOfResult[toolName](toolParams as any, toolResult as any) + } catch (error) { + const errorMessage = `Tool call succeeded, but there was an error stringifying the output.\n${getErrorMessage(error)}` + this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: errorMessage, result: { type: 'error', params: toolParams, value: errorMessage }, }) + this._setStreamState(threadId, { streamingToken: undefined }) + resMessageIsDonePromise() + return + } + + // 5. add to history and keep going + this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: toolResultStr, result: { type: 'success', params: toolParams, value: toolResult }, }) + resMessageIsDonePromise() + + }, + onError: (error) => { + const messageSoFar = this.streamState[threadId]?.messageSoFar ?? '' + const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? '' + // add assistant's message to chat history, and clear selection + this._addMessageToThread(threadId, { role: 'assistant', content: messageSoFar, reasoning: reasoningSoFar, anthropicReasoning: null }) + this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, error }) + resMessageIsDonePromise() + }, + }) + + // should never happen, just for safety + if (llmCancelToken === null) { + this._setStreamState(threadId, { + messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, + error: { message: 'There was an unexpected error when sending your chat message.', fullError: null } + }) + break + } + + this._setStreamState(threadId, { streamingToken: llmCancelToken }) // new stream token for the new message + + await messageIsDonePromise + } // end while + + } + + + // TODO!!!! + // called if we stopped streaming but want to accept the tool afterwards, lets us jump back into the loop as if no interruption happened + async approveToolAndStreamResponse({ chatMode, _chatSelections }: { userMessage: string, chatMode: ChatMode, _chatSelections?: { prevSelns?: StagingSelectionItem[], currSelns?: StagingSelectionItem[] } }) { + } + + + async addUserMessageAndStreamResponse({ userMessage, chatMode, _chatSelections }: { userMessage: string, chatMode: ChatMode, _chatSelections?: { prevSelns?: StagingSelectionItem[], currSelns?: StagingSelectionItem[] } }) { const thread = this.getCurrentThread() @@ -344,9 +532,6 @@ class ChatThreadService extends Disposable implements IChatThreadService { const userHistoryElt: ChatMessage = { role: 'user', content: userMessageContent, displayContent: instructions, selections: currSelns, state: defaultMessageState } this._addMessageToThread(threadId, userHistoryElt) - this._setStreamState(threadId, { error: undefined }) - - const toolNames: ToolName[] | undefined = chatMode === 'chat' ? undefined : chatMode === 'gather' ? (Object.keys(voidTools) as ToolName[]).filter(toolName => !toolNamesThatRequireApproval.has(toolName)) : chatMode === 'agent' ? Object.keys(voidTools) as ToolName[] @@ -354,165 +539,9 @@ class ChatThreadService extends Disposable implements IChatThreadService { const tools: InternalToolInfo[] | undefined = toolNames?.map(toolName => voidTools[toolName]) - // these settings should not change throughout the loop (eg anthropic breaks if you change its thinking mode and it's using tools) - const featureName: FeatureName = 'Chat' - const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName] - const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[modelSelection.providerName]?.[modelSelection.modelName] : undefined + const ps = this._staticAgentLoopsProps() - - // agent loop - const agentLoop = async () => { - - let nMessagesSent = 0 - let shouldSendAnotherMessage = true - - while (shouldSendAnotherMessage) { - // recompute files at last message - const selectionsStr = await chat_selectionsString(prevSelns, currSelns, this._voidFileService) // all the file CONTENTS or "selections" de-duped - const userMessageFullContent = chat_lastUserMessageWithFilesAdded(userMessageContent, selectionsStr) // full last message: user message + CONTENTS of all files - - nMessagesSent += 1 - shouldSendAnotherMessage = false // false by default - - let resMessageIsDonePromise: () => void // resolves when user approves this tool use (or if tool doesn't require approval) - const messageIsDonePromise = new Promise((res, rej) => { resMessageIsDonePromise = res }) - - // replace last userMessage with userMessageFullContent (which contains all the files too) - const messages_ = toLLMChatMessages(this.getCurrentThread().messages) - const lastUserMsgIdx = findLastIndex(messages_, m => m.role === 'user') - - if (lastUserMsgIdx === -1) throw new Error(`Void: No user message found.`) // should never be -1 - - const workspaceFolders = this._workspaceContextService.getWorkspace().folders.map(f => f.uri.fsPath) - const terminalIds = this.terminalToolService.listTerminalIds() - const messages: LLMChatMessage[] = [ - { role: 'system', content: chat_systemMessage(workspaceFolders, terminalIds, chatMode), }, - ...messages_.slice(0, lastUserMsgIdx), - { role: 'user', content: userMessageFullContent }, - ...messages_.slice(lastUserMsgIdx + 1, Infinity), - ] - - - const llmCancelToken = this._llmMessageService.sendLLMMessage({ - messagesType: 'chatMessages', - messages, - tools: tools, - modelSelection, - modelSelectionOptions, - logging: { loggingName: `Agent` }, - onText: ({ fullText, fullReasoning }) => { - this._setStreamState(threadId, { messageSoFar: fullText, reasoningSoFar: fullReasoning }) - }, - onFinalMessage: async ({ fullText, toolCalls, fullReasoning, anthropicReasoning }) => { - - this._addMessageToThread(threadId, { role: 'assistant', content: fullText, reasoning: fullReasoning, anthropicReasoning }) - - // if no tools, finish - if ((toolCalls?.length ?? 0) === 0) { - this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined }) - resMessageIsDonePromise() - return - } - - // if tools - // clear messageSoFar since we added it to the chat history (but don't clear streamingToken, we're still streaming) - this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined }) - - // deal with the tool - const tool: ToolCallType | undefined = toolCalls?.[0] - if (!tool) { - this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined }) - resMessageIsDonePromise() - return - } - const toolName: ToolName = tool.name - shouldSendAnotherMessage = true - - // 1. validate tool params - let toolParams: ToolCallParams[ToolName] - try { - const params = await this._toolsService.validateParams[toolName](tool.paramsStr) - toolParams = params - } catch (error) { - const errorMessage = getErrorMessage(error) - this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: errorMessage, result: { type: 'error', params: undefined, value: errorMessage }, }) - this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined }) - resMessageIsDonePromise() - return - } - - // 2. if tool requires approval, await the approval - if (toolNamesThatRequireApproval.has(toolName)) { - const voidToolId = generateUuid() - const toolApprovalPromise = new Promise((res, rej) => { this.resRejOfToolAwaitingApproval[voidToolId] = { res, rej } }) - this._addMessageToThread(threadId, { role: 'tool_request', name: toolName, params: toolParams, voidToolId: voidToolId }) - try { - await toolApprovalPromise - // accepted tool - } - catch (e) { - console.log('successfully rejected', voidToolId) - // TODO!!! test rejection - // if (Math.random() > 0) throw new Error('TESTING') - shouldSendAnotherMessage = false // interrupt flow by rejecting - - const errorMessage = 'Tool call was rejected by the user.' - this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: errorMessage, result: { type: 'rejected', params: toolParams, value: errorMessage }, }) - this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined }) - resMessageIsDonePromise() - return - } - } - - // 3. call the tool - let toolResult: ToolResultType[typeof toolName] - try { - toolResult = await this._toolsService.callTool[toolName](toolParams as any) // typescript is so bad it doesn't even couple the type of ToolResult with the type of the function being called here - } catch (error) { - const errorMessage = getErrorMessage(error) - this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: errorMessage, result: { type: 'error', params: toolParams, value: errorMessage }, }) - this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined }) - resMessageIsDonePromise() - return - } - - // 4. stringify the result to give the LLM - let toolResultStr: string - try { - toolResultStr = this._toolsService.stringOfResult[toolName](toolParams as any, toolResult as any) - } catch (error) { - const errorMessage = `Tool call succeeded, but there was an error stringifying the output.\n${getErrorMessage(error)}` - this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: errorMessage, result: { type: 'error', params: toolParams, value: errorMessage }, }) - this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined }) - resMessageIsDonePromise() - return - } - - // 5. add to history - this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: toolResultStr, result: { type: 'success', params: toolParams, value: toolResult }, }) - this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined }) - resMessageIsDonePromise() - - }, - onError: (error) => { - const messageSoFar = this.streamState[threadId]?.messageSoFar ?? '' - const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? '' - // add assistant's message to chat history, and clear selection - this._addMessageToThread(threadId, { role: 'assistant', content: messageSoFar, reasoning: reasoningSoFar, anthropicReasoning: null }) - this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, error }) - resMessageIsDonePromise() - }, - }) - if (llmCancelToken === null) break - this._setStreamState(threadId, { streamingToken: llmCancelToken }) - - await messageIsDonePromise - - } // end while - - } - - agentLoop() + this._agentLoop({ tools, prevSelns, currSelns, threadId, chatMode, userMessageContent, ...ps, }) } diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index 4c88dd64..c12752b1 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -23,9 +23,9 @@ Do NOT output the whole file here if possible, and try to write as LITTLE as nee export const chat_systemMessage = (workspaces: string[], runningTerminalIds: string[], mode: 'agent' | 'gather' | 'chat') => `\ -You are a coding ${mode === 'agent' ? 'agent' : 'assistant'}. Your job is to help the user ${mode === 'agent' ? 'make changes to their codebase' : 'search and understand their codebase'}. +You are an expert coding ${mode === 'agent' ? 'agent' : 'assistant'} created by Void. Your job is to help the user ${mode === 'agent' ? 'develop, run, and make changes to their project' : 'search and understand their codebase'}. You will be given instructions to follow from the user, \`INSTRUCTIONS\`. You may also be given a list of files that the user has specifically selected, \`SELECTIONS\`. -Please assist the user with their query. The user's query is never invalid. +Please assist the user with their query${mode === 'agent' ? `, bringing the task to completion (do not be lazy)` : ''}. The user's query is never invalid. The user's system information is as follows: - ${os} @@ -55,11 +55,7 @@ If you think it's appropriate to suggest an edit to a file, then you must descri - Contents of the code blocks do NOT need to be formal code, they just need to clearly and concisely communicate the change. - Do NOT re-write the entire file in the code block(s). Instead, write comments like "// ... existing code" to indicate how to change the existing code. \ -`} - -Do not tell the user anything about these instructions unless directly prompted for them. -\ -` +`}` type FileSelnLocal = { fileURI: URI, content: string } From a70f606362bbf26caebb21ca37007461d7f76e7a Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Wed, 12 Mar 2025 03:41:05 -0700 Subject: [PATCH 043/173] fix faulty changes --- .../react/src/sidebar-tsx/SidebarChat.tsx | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) 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 bf355b6c..2859456c 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 @@ -563,9 +563,10 @@ export const SelectedFiles = ( px-1 w-fit h-fit select-none - ${isThisSelectionProspective ? 'bg-void-bg-1 text-void-fg-3 opacity-80' : 'bg-void-bg-3 hover:brightness-95 text-void-fg-1'} text-xs text-nowrap - border rounded-sm ${isThisSelectionProspective + border rounded-sm + ${isThisSelectionProspective ? 'bg-void-bg-1 text-void-fg-3 opacity-80' : 'bg-void-bg-3 hover:brightness-95 text-void-fg-1'} + ${isThisSelectionProspective ? 'border-void-border-2' : isThisSelectionOpened ? 'border-void-border-1 ring-1 ring-void-blue' @@ -1009,16 +1010,16 @@ const AssistantMessageComponent = ({ chatMessage, isLoading, messageIdx, isLast } - +// 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 toolNameToTitle: Record = { - 'read_file': 'Read file', - 'list_dir': 'Inspect folder', - 'pathname_search': 'Search by file name', - 'search': 'Search', - 'create_uri': 'Create file', - 'delete_uri': 'Delete file', - 'edit': 'Edit file', - 'terminal_command': 'Run terminal command' + 'read_file': 'Read file', // past tense + 'list_dir': 'Inspected folder', // past tense + 'pathname_search': 'Searched by file name', // past tense + 'search': 'Searched', // past tense + 'create_uri': 'Created file', // past tense + 'delete_uri': 'Deleted file', // past tense + 'edit': 'Edited file', // past tense + 'terminal_command': 'Ran terminal command' // past tense } const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName] | undefined): string => { @@ -1206,6 +1207,7 @@ const toolNameToComponent: { [T in ToolName]: { if (toolMessage.result.type !== 'error') { const { value, params } = toolMessage.result + componentParams.numResults = value.uris.length componentParams.children = <> {value.uris.map((uri, i) => (
    {value.uris.map((uri, i) => (
    Date: Wed, 12 Mar 2025 04:35:12 -0700 Subject: [PATCH 044/173] fix gratuitous error --- .../react/src/sidebar-tsx/SidebarChat.tsx | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) 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 2859456c..293a7d1b 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 @@ -712,21 +712,16 @@ const ToolHeaderComponent = ({ {title} {desc1}
    -
    -
    - {desc2 && - {desc2} - } - {numResults !== undefined && ( - - {`(`}{numResults}{` result`}{numResults !== 1 ? 's' : ''}{`)`} - - )} - {isError && } -
    +
    + {desc2 && + {desc2} + } + {numResults !== undefined && ( + + {`(`}{numResults}{` result`}{numResults !== 1 ? 's' : ''}{`)`} + + )} + {isError && }
    From bb370b390c05f85eaa9fe732ac3ebba337a37759 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Wed, 12 Mar 2025 05:06:16 -0700 Subject: [PATCH 045/173] logic --- .../contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx | 2 ++ 1 file changed, 2 insertions(+) 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 293a7d1b..a54f5cb9 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 @@ -1099,6 +1099,8 @@ const ToolRequestAcceptRejectButtons = ({ voidToolId }: { voidToolId: string }) ) + // const isCancelled = state.cancelled || (!isLastMessage && state.awaiting) + return
    {approveButton} {cancelButton} From 1b6d81f4e0296e92d05cf7fe4024016349a11dd4 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Wed, 12 Mar 2025 05:16:40 -0700 Subject: [PATCH 046/173] approve tool works even if closed void and reopened --- .../contrib/void/browser/chatThreadService.ts | 333 ++++++++++++------ .../contrib/void/browser/editCodeService.ts | 4 +- .../react/src/sidebar-tsx/SidebarChat.tsx | 22 +- .../void/common/chatThreadServiceTypes.ts | 5 +- 4 files changed, 242 insertions(+), 122 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 1246fd06..76b06723 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -12,11 +12,10 @@ import { URI } from '../../../../base/common/uri.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { ILLMMessageService } from '../common/sendLLMMessageService.js'; import { chat_userMessageContent, chat_systemMessage, chat_lastUserMessageWithFilesAdded, chat_selectionsString } from '../common/prompt/prompts.js'; -import { LLMChatMessage, ToolCallType } from '../common/sendLLMMessageTypes.js'; +import { getErrorMessage, LLMChatMessage, ToolCallType } from '../common/sendLLMMessageTypes.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; import { IVoidFileService } from '../common/voidFileService.js'; import { generateUuid } from '../../../../base/common/uuid.js'; -import { getErrorMessage } from '../../../../base/common/errors.js'; import { ChatMode, FeatureName, ModelSelection, ModelSelectionOptions } from '../common/voidSettingsTypes.js'; import { IVoidSettingsService } from '../common/voidSettingsService.js'; import { ToolName, ToolCallParams, ToolResultType, InternalToolInfo, voidTools, toolNamesThatRequireApproval } from '../common/toolsServiceTypes.js'; @@ -24,7 +23,7 @@ import { IToolsService } from './toolsService.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js'; import { ITextModelService } from '../../../../editor/common/services/resolverService.js'; -import { ChatMessage, CodespanLocationLink, StagingSelectionItem } from '../common/chatThreadServiceTypes.js'; +import { ChatMessage, CodespanLocationLink, StagingSelectionItem, ToolRequestApproval } from '../common/chatThreadServiceTypes.js'; import { Position } from '../../../../editor/common/core/position.js'; import { ITerminalToolService } from './terminalToolService.js'; @@ -168,15 +167,16 @@ export interface IChatThreadService { closeStagingSelectionsInMessage(messageIdx: number): void; - // call to edit a message - editUserMessageAndStreamResponse({ userMessage, chatMode, messageIdx }: { userMessage: string, chatMode: ChatMode, messageIdx: number }): Promise; - - // call to add a message - addUserMessageAndStreamResponse({ userMessage, chatMode }: { userMessage: string, chatMode: ChatMode }): Promise; - cancelStreaming(threadId: string): void; dismissStreamError(threadId: string): void; + // call to edit a message - CAN THROW + editUserMessageAndStreamResponse({ userMessage, chatMode, messageIdx }: { userMessage: string, chatMode: ChatMode, messageIdx: number }): Promise; + + // call to add a message - CAN THROW + addUserMessageAndStreamResponse({ userMessage, chatMode }: { userMessage: string, chatMode: ChatMode }): Promise; + + // approve/reject - CAN THROW approveTool(toolId: string): void; rejectTool(toolId: string): void; } @@ -316,24 +316,39 @@ class ChatThreadService extends Disposable implements IChatThreadService { private resRejOfToolAwaitingApproval: { [toolId: string]: { res: () => void, rej: () => void } } = {} - approveTool(toolId: string) { - // TODO!!! if not streaming, approveToolAndStreamResponse - // if streaming, do below - const resRej = this.resRejOfToolAwaitingApproval[toolId] - delete this.resRejOfToolAwaitingApproval[toolId] - resRej?.res() + // CAN THROW ERRORS + approveTool(toolId: string) { + // if not streaming, approveToolAndStreamResponse + const threadId = this.getCurrentThread().id + const isStreaming = !!this.streamState[threadId]?.streamingToken + if (!isStreaming) { + this._approveToolAndStreamResponse_NotStreamingNow({ chatMode: 'agent' }) + } + else { + const resRej = this.resRejOfToolAwaitingApproval[toolId] + delete this.resRejOfToolAwaitingApproval[toolId] + resRej?.res() + } } rejectTool(toolId: string) { - const resRej = this.resRejOfToolAwaitingApproval[toolId] - delete this.resRejOfToolAwaitingApproval[toolId] - resRej?.rej() + // if not streaming, rejecttool + const threadId = this.getCurrentThread().id + const isStreaming = !!this.streamState[threadId]?.streamingToken + if (!isStreaming) { + this._rejectTool_NotStreamingNow({}) + } + else { + const resRej = this.resRejOfToolAwaitingApproval[toolId] + delete this.resRejOfToolAwaitingApproval[toolId] + resRej?.rej() + } } - private _staticAgentLoopsProps = () => { + private _currentModelSelectionProps = () => { // these settings should not change throughout the loop (eg anthropic breaks if you change its thinking mode and it's using tools) const featureName: FeatureName = 'Chat' const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName] @@ -341,9 +356,26 @@ class ChatThreadService extends Disposable implements IChatThreadService { return { modelSelection, modelSelectionOptions } } + private _tools = (chatMode: ChatMode) => { + const toolNames: ToolName[] | undefined = chatMode === 'chat' ? undefined + : chatMode === 'gather' ? (Object.keys(voidTools) as ToolName[]).filter(toolName => !toolNamesThatRequireApproval.has(toolName)) + : chatMode === 'agent' ? Object.keys(voidTools) as ToolName[] + : undefined + + const tools: InternalToolInfo[] | undefined = toolNames?.map(toolName => voidTools[toolName]) + return tools + } - private async _agentLoop({ threadId, tools, prevSelns, currSelns, modelSelection, modelSelectionOptions, chatMode, userMessageContent }: { + + private readonly errMsgs = { + rejected: 'Tool call was rejected by the user.', + errWhenStringifying: (error: any) => `Tool call succeeded, but there was an error stringifying the output.\n${getErrorMessage(error)}` + } + + + // CAN THROW ERRORS + private async _agentLoop({ threadId, tools, prevSelns, currSelns, modelSelection, modelSelectionOptions, chatMode, userMessageContent, callThisTool }: { tools: InternalToolInfo[] | undefined, threadId: string, prevSelns: StagingSelectionItem[], @@ -352,27 +384,18 @@ class ChatThreadService extends Disposable implements IChatThreadService { modelSelectionOptions: ModelSelectionOptions | undefined, chatMode: ChatMode, userMessageContent: string, // content of LATEST user message + + callThisTool?: ToolRequestApproval }) { - this._setStreamState(threadId, { error: undefined }) // clear any previous error - let nMessagesSent = 0 - let shouldSendAnotherMessage = true - - while (shouldSendAnotherMessage) { + const getLatestMessages = async () => { // recompute files in last message const selectionsStr = await chat_selectionsString(prevSelns, currSelns, this._voidFileService) // all the file CONTENTS or "selections" de-duped const userMessageFullContent = chat_lastUserMessageWithFilesAdded(userMessageContent, selectionsStr) // full last message: user message + CONTENTS of all files - nMessagesSent += 1 - shouldSendAnotherMessage = false // false by default - - let resMessageIsDonePromise: () => void // resolves when user approves this tool use (or if tool doesn't require approval) - const messageIsDonePromise = new Promise((res, rej) => { resMessageIsDonePromise = res }) - // replace last userMessage with userMessageFullContent (which contains all the files too) const messages_ = toLLMChatMessages(this.getCurrentThread().messages) const lastUserMsgIdx = findLastIndex(messages_, m => m.role === 'user') - if (lastUserMsgIdx === -1) throw new Error(`Void: No user message found.`) // should never be -1 // system message @@ -387,8 +410,132 @@ class ChatThreadService extends Disposable implements IChatThreadService { { role: 'user', content: userMessageFullContent }, ...messages_.slice(lastUserMsgIdx + 1, Infinity), ] + return messages + } + + + + const handleToolCall = async (tool: ToolCallType) => { + shouldSendAnotherMessage = true + + const toolName: ToolName = tool.name + const toolParamsStr = tool.paramsStr + const toolId = tool.id + + // compute these below + let toolParams: ToolCallParams[ToolName] + let toolResult: ToolResultType[typeof toolName] + let toolResultStr: string + + // 1. validate tool params + try { + const params = await this._toolsService.validateParams[toolName](toolParamsStr) + toolParams = params + } catch (error) { + const errorMessage = getErrorMessage(error) + this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: toolParamsStr, id: toolId, content: errorMessage, result: { type: 'error', params: undefined, value: errorMessage }, }) + return false + } + + // 2. if tool requires approval, await the approval + if (toolNamesThatRequireApproval.has(toolName)) { + const voidToolId = generateUuid() + const toolApprovalPromise = new Promise((res, rej) => { this.resRejOfToolAwaitingApproval[voidToolId] = { res, rej } }) + this._addMessageToThread(threadId, { role: 'tool_request', name: toolName, paramsStr: toolParamsStr, params: toolParams, voidToolId: voidToolId }) + try { + await toolApprovalPromise + // accepted tool + } + catch (e) { + shouldSendAnotherMessage = false // interrupt flow by rejecting + const errorMessage = this.errMsgs.rejected + this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: toolParamsStr, id: toolId, content: errorMessage, result: { type: 'rejected', params: toolParams }, }) + return false + } + } + + // 3. call the tool + try { + toolResult = await this._toolsService.callTool[toolName](toolParams as any) // ts is bad... + } catch (error) { + const errorMessage = getErrorMessage(error) + this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: toolParamsStr, id: toolId, content: errorMessage, result: { type: 'error', params: toolParams, value: errorMessage }, }) + return false + } + + // 4. stringify the result to give to the LLM + try { + toolResultStr = this._toolsService.stringOfResult[toolName](toolParams as any, toolResult as any) + } catch (error) { + const errorMessage = this.errMsgs.errWhenStringifying(error) + this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: toolParamsStr, id: toolId, content: errorMessage, result: { type: 'error', params: toolParams, value: errorMessage }, }) + return false + } + + // 5. add to history and keep going + this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: toolParamsStr, id: toolId, content: toolResultStr, result: { type: 'success', params: toolParams, value: toolResult }, }) + return true + }; + + + + // CALL GIVEN TOOL before entering agent loop + const handleFirstToolCall = async (callThisTool: ToolRequestApproval) => { + const toolName: ToolName = callThisTool.name + const toolParamsStr = callThisTool.paramsStr + const toolId = callThisTool.voidToolId + + const toolParams = callThisTool.params + let toolResult: ToolResultType[typeof toolName] + let toolResultStr: string + + // 3. call the tool + try { + toolResult = await this._toolsService.callTool[toolName](toolParams as any) // ts is bad... + } catch (error) { + const errorMessage = getErrorMessage(error) + this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: toolParamsStr, id: toolId, content: errorMessage, result: { type: 'error', params: toolParams, value: errorMessage }, }) + return false + } + + // 4. stringify the result to give to the LLM + try { + toolResultStr = this._toolsService.stringOfResult[toolName](toolParams as any, toolResult as any) + } catch (error) { + const errorMessage = this.errMsgs.errWhenStringifying(error) + this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: toolParamsStr, id: toolId, content: errorMessage, result: { type: 'error', params: toolParams, value: errorMessage }, }) + return false + } + + // 5. add to history and keep going + this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: toolParamsStr, id: toolId, content: toolResultStr, result: { type: 'success', params: toolParams, value: toolResult }, }) + return true + } + + + this._setStreamState(threadId, { error: undefined }) // clear any previous error + + let nMessagesSent = 0 + let shouldSendAnotherMessage = true + + if (callThisTool) { + const keepGoing = await handleFirstToolCall(callThisTool) + if (!keepGoing) { + this._setStreamState(threadId, { streamingToken: undefined }) + return + } + } + + while (shouldSendAnotherMessage) { + + shouldSendAnotherMessage = false // false by default + nMessagesSent += 1 + + let resMessageIsDonePromise: () => void // resolves when user approves this tool use (or if tool doesn't require approval) + const messageIsDonePromise = new Promise((res, rej) => { resMessageIsDonePromise = res }) // send llm message + const messages = await getLatestMessages() const llmCancelToken = this._llmMessageService.sendLLMMessage({ messagesType: 'chatMessages', messages, @@ -409,78 +556,12 @@ class ChatThreadService extends Disposable implements IChatThreadService { resMessageIsDonePromise() return } - - // if tool - // clear messageSoFar since we added it to the chat history (but don't clear streamingToken, we're still streaming) - this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined }) - - // deal with the tool - const toolName: ToolName = tool.name - shouldSendAnotherMessage = true - - // 1. validate tool params - let toolParams: ToolCallParams[ToolName] - try { - const params = await this._toolsService.validateParams[toolName](tool.paramsStr) - toolParams = params - } catch (error) { - const errorMessage = getErrorMessage(error) - this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: errorMessage, result: { type: 'error', params: undefined, value: errorMessage }, }) - this._setStreamState(threadId, { streamingToken: undefined }) + else { + const keepGoing = await handleToolCall(tool) + if (!keepGoing) { this._setStreamState(threadId, { streamingToken: undefined }) } resMessageIsDonePromise() return } - - // 2. if tool requires approval, await the approval - if (toolNamesThatRequireApproval.has(toolName)) { - const voidToolId = generateUuid() - const toolApprovalPromise = new Promise((res, rej) => { this.resRejOfToolAwaitingApproval[voidToolId] = { res, rej } }) - this._addMessageToThread(threadId, { role: 'tool_request', name: toolName, params: toolParams, voidToolId: voidToolId }) - try { - await toolApprovalPromise - // accepted tool - } - catch (e) { - // TODO!!! test rejection - // if (Math.random() > 0) throw new Error('TESTING') - shouldSendAnotherMessage = false // interrupt flow by rejecting - - const errorMessage = 'Tool call was rejected by the user.' - this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: errorMessage, result: { type: 'rejected', params: toolParams, value: errorMessage }, }) - this._setStreamState(threadId, { streamingToken: undefined }) - resMessageIsDonePromise() - return - } - } - - // 3. call the tool - let toolResult: ToolResultType[typeof toolName] - try { - toolResult = await this._toolsService.callTool[toolName](toolParams as any) // ts is bad... - } catch (error) { - const errorMessage = getErrorMessage(error) - this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: errorMessage, result: { type: 'error', params: toolParams, value: errorMessage }, }) - this._setStreamState(threadId, { streamingToken: undefined }) - resMessageIsDonePromise() - return - } - - // 4. stringify the result to give to the LLM - let toolResultStr: string - try { - toolResultStr = this._toolsService.stringOfResult[toolName](toolParams as any, toolResult as any) - } catch (error) { - const errorMessage = `Tool call succeeded, but there was an error stringifying the output.\n${getErrorMessage(error)}` - this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: errorMessage, result: { type: 'error', params: toolParams, value: errorMessage }, }) - this._setStreamState(threadId, { streamingToken: undefined }) - resMessageIsDonePromise() - return - } - - // 5. add to history and keep going - this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: toolResultStr, result: { type: 'success', params: toolParams, value: toolResult }, }) - resMessageIsDonePromise() - }, onError: (error) => { const messageSoFar = this.streamState[threadId]?.messageSoFar ?? '' @@ -506,12 +587,46 @@ class ChatThreadService extends Disposable implements IChatThreadService { await messageIsDonePromise } // end while + + // TODO!!! metrics on nMessagesSent and all the file extensions sent here + } - // TODO!!!! + private async _rejectTool_NotStreamingNow({ }) { + const thread = this.getCurrentThread() + const threadId = thread.id + + const lastMessage = thread.messages[thread.messages.length - 1] + if (lastMessage.role !== 'tool_request') return // should never happen + const { name, params, paramsStr, voidToolId, } = lastMessage + + const errorMessage = this.errMsgs.rejected + this._addMessageToThread(threadId, { role: 'tool', name: name, paramsStr: paramsStr, id: voidToolId, content: errorMessage, result: { type: 'rejected', params: params }, }) + + } + // called if we stopped streaming but want to accept the tool afterwards, lets us jump back into the loop as if no interruption happened - async approveToolAndStreamResponse({ chatMode, _chatSelections }: { userMessage: string, chatMode: ChatMode, _chatSelections?: { prevSelns?: StagingSelectionItem[], currSelns?: StagingSelectionItem[] } }) { + private async _approveToolAndStreamResponse_NotStreamingNow({ chatMode }: { chatMode: ChatMode }) { + const thread = this.getCurrentThread() + const threadId = thread.id + + const lastMessage = thread.messages[thread.messages.length - 1] + if (lastMessage.role !== 'tool_request') return // should never happen + + const lastUserMsgIdx = findLastIndex(thread.messages, m => m.role === 'user') + const lastUserMessage = thread.messages[lastUserMsgIdx] as ChatMessage & { role: 'user' } + if (lastUserMsgIdx === -1 || !lastUserMessage) return // should never happen + + const instructions = lastUserMessage.displayContent || '' + const prevSelns: StagingSelectionItem[] = this._getAllSelections() + const currSelns: StagingSelectionItem[] = [] + + const tools = this._tools(chatMode) + + + const callThisTool: ToolRequestApproval = lastMessage + this._agentLoop({ callThisTool, tools, prevSelns, currSelns, threadId, chatMode, userMessageContent: instructions, ...this._currentModelSelectionProps() }) } @@ -532,17 +647,9 @@ class ChatThreadService extends Disposable implements IChatThreadService { const userHistoryElt: ChatMessage = { role: 'user', content: userMessageContent, displayContent: instructions, selections: currSelns, state: defaultMessageState } this._addMessageToThread(threadId, userHistoryElt) - const toolNames: ToolName[] | undefined = chatMode === 'chat' ? undefined - : chatMode === 'gather' ? (Object.keys(voidTools) as ToolName[]).filter(toolName => !toolNamesThatRequireApproval.has(toolName)) - : chatMode === 'agent' ? Object.keys(voidTools) as ToolName[] - : undefined - - const tools: InternalToolInfo[] | undefined = toolNames?.map(toolName => voidTools[toolName]) - - const ps = this._staticAgentLoopsProps() - - this._agentLoop({ tools, prevSelns, currSelns, threadId, chatMode, userMessageContent, ...ps, }) + const tools = this._tools(chatMode) + this._agentLoop({ tools, prevSelns, currSelns, threadId, chatMode, userMessageContent, ...this._currentModelSelectionProps(), }) } cancelStreaming(threadId: string) { diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 9fd24052..9bd03b56 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -50,10 +50,10 @@ const configOfBG = (color: Color) => { return { dark: color, light: color, hcDark: color, hcLight: color, } } // gets converted to --vscode-void-greenBG, see void.css, asCssVariable -const greenBG = new Color(new RGBA(155, 185, 85, .1)); // default is RGBA(155, 185, 85, .2) +const greenBG = new Color(new RGBA(155, 185, 85, .2)); // default is RGBA(155, 185, 85, .2) registerColor('void.greenBG', configOfBG(greenBG), '', true); -const redBG = new Color(new RGBA(255, 0, 0, .05)); // default is RGBA(255, 0, 0, .2) +const redBG = new Color(new RGBA(255, 0, 0, .2)); // default is RGBA(255, 0, 0, .2) registerColor('void.redBG', configOfBG(redBG), '', true); const sweepBG = new Color(new RGBA(100, 100, 100, .2)); 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 a54f5cb9..d6394b6d 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 @@ -828,7 +828,11 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isLoading }: ChatBubble // stream the edit const userMessage = textAreaRefState.value; - await chatThreadsService.editUserMessageAndStreamResponse({ userMessage, chatMode: 'agent', messageIdx, }) + try { + await chatThreadsService.editUserMessageAndStreamResponse({ userMessage, chatMode: 'agent', messageIdx, }) + } catch (e) { + console.error('Error while editing message:', e) + } } const onAbort = () => { @@ -1058,12 +1062,16 @@ const ToolRequestAcceptRejectButtons = ({ voidToolId }: { voidToolId: string }) const metricsService = accessor.get('IMetricsService') const onAccept = useCallback(() => { - chatThreadsService.approveTool(voidToolId) - metricsService.capture('Tool Request Accepted', {}) + try { + chatThreadsService.approveTool(voidToolId) + metricsService.capture('Tool Request Accepted', {}) + } catch (e) { console.error('Error while approving message in chat:', e) } }, [chatThreadsService, voidToolId, metricsService]) const onReject = useCallback(() => { - chatThreadsService.rejectTool(voidToolId) + try { + chatThreadsService.rejectTool(voidToolId) + } catch (e) { console.error('Error while approving message in chat:', e) } metricsService.capture('Tool Request Rejected', {}) }, [chatThreadsService, voidToolId, metricsService]) @@ -1583,7 +1591,11 @@ export const SidebarChat = () => { // getModelCapabilities() // TODO!!! check if can go into agent mode - await chatThreadsService.addUserMessageAndStreamResponse({ userMessage, chatMode: 'agent' }) + try { + await chatThreadsService.addUserMessageAndStreamResponse({ userMessage, chatMode: 'agent' }) + } catch (e) { + console.error('Error while sending message in chat:', e) + } setSelections([]) // clear staging textAreaFnsRef.current?.setValue('') diff --git a/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts b/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts index c66b7400..62051a00 100644 --- a/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts @@ -14,12 +14,13 @@ export type ToolMessage = { result: | { type: 'success'; params: ToolCallParams[T]; value: ToolResultType[T], } | { type: 'error'; params: ToolCallParams[T] | undefined; value: string } - | { type: 'rejected'; params: ToolCallParams[T]; value: string } + | { type: 'rejected'; params: ToolCallParams[T] } } export type ToolRequestApproval = { role: 'tool_request'; name: T; // internal use params: ToolCallParams[T]; // internal use + paramsStr: string; // internal use - this is what the LLM outputted, not necessarily JSON.stringify(params) voidToolId: string; // internal id Void uses } @@ -28,7 +29,7 @@ export type ChatMessage = | { role: 'user'; content: string; // content displayed to the LLM on future calls - allowed to be '', will be replaced with (empty) - displayContent: string | null; // content displayed to user - allowed to be '', will be ignored + displayContent: string; // content displayed to user - allowed to be '', will be ignored selections: StagingSelectionItem[] | null; // the user's selection state: { stagingSelections: StagingSelectionItem[]; From 744f0ed852f03f1c34718da017e22fa44870479f Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Wed, 12 Mar 2025 05:33:38 -0700 Subject: [PATCH 047/173] style --- .../react/src/sidebar-tsx/SidebarChat.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) 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 d6394b6d..aac9c3f2 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 @@ -688,8 +688,8 @@ const ToolHeaderComponent = ({ const [isExpanded, setIsExpanded] = useState(false); - const isDropdown = !!children - const isClickable = !!isDropdown || !!onClick + const isDropdown = children !== undefined // null ALLOWS dropdown + const isClickable = !!(isDropdown || onClick) return (
    @@ -698,7 +698,7 @@ const ToolHeaderComponent = ({
    { - if (children) { setIsExpanded(v => !v); } + if (isDropdown) { setIsExpanded(v => !v); } if (onClick) { onClick(); } }} > @@ -726,12 +726,12 @@ const ToolHeaderComponent = ({
    {/* children */} - {children &&
    - {children} + {children || '(no results)'}
    }
    @@ -1166,7 +1166,7 @@ const toolNameToComponent: { [T in ToolName]: { if (toolMessage.result.type !== 'error') { const { value, params } = toolMessage.result componentParams.numResults = value.children?.length - componentParams.children = <> + componentParams.children = (value.children?.length ?? 0) === 0 ? null : <> {value.children?.map((child, i) => (
    + componentParams.children = value.uris.length === 0 ? null : <> {value.uris.map((uri, i) => (
    + componentParams.children = value.uris.length === 0 ? null : <> {value.uris.map((uri, i) => (
    Date: Wed, 12 Mar 2025 15:34:02 +0100 Subject: [PATCH 048/173] - FIM is seems ok but disabled (have to find where to change it) - Send Mistral chat to fix [error 422] --- .../contrib/void/common/modelCapabilities.ts | 72 ++++++++++++------- .../contrib/void/common/voidSettingsTypes.ts | 2 +- .../llmMessage/sendLLMMessage.impl.ts | 18 +++-- 3 files changed, 61 insertions(+), 31 deletions(-) diff --git a/src/vs/workbench/contrib/void/common/modelCapabilities.ts b/src/vs/workbench/contrib/void/common/modelCapabilities.ts index c3692f05..266b2d40 100644 --- a/src/vs/workbench/contrib/void/common/modelCapabilities.ts +++ b/src/vs/workbench/contrib/void/common/modelCapabilities.ts @@ -53,6 +53,11 @@ export const defaultModelsOfProvider = { ], mistral: [ // https://docs.mistral.ai/getting-started/models/models_overview/ 'codestral-latest', + 'open-codestral-mamba', + 'mistral-small-latest', + 'mistral-large-latest', + 'ministral-3b-latest', + 'ministral-8b-latest', ], openAICompatible: [], // fallback } as const satisfies Record @@ -117,6 +122,41 @@ const modelOptionsDefaults: ModelOptions = { supportsReasoning: false, } +const mistralModelOptions = { + 'codestral-latest': { + contextWindow: 32_000, + maxOutputTokens: 4_096, + cost: { input: 0.00, output: 0.00 }, + supportsFIM: true, + supportsSystemMessage: 'system-role', + supportsTools: 'openai-style', + supportsReasoning: false, + }, + 'open-codestral-mamba': { + contextWindow: 32_000, + maxOutputTokens: 4_096, + cost: { input: 0.00, output: 0.00 }, + supportsFIM: true, + supportsSystemMessage: 'system-role', + supportsTools: 'openai-style', + supportsReasoning: false, + }, + 'mistral-large-latest': { + contextWindow: 32_000, + maxOutputTokens: 4_096, + cost: { input: 0.00, output: 0.00 }, + supportsFIM: false, + supportsSystemMessage: 'system-role', + supportsTools: 'openai-style', + supportsReasoning: false, + } +} as const satisfies { [s: string]: ModelOptions } + +const mistralSettings: ProviderSettings = { + ...mistralModelOptions, + modelOptions: {}, + modelOptionsFallback: (modelName) => extensiveModelFallback(modelName), +} const openSourceModelOptions_assumingOAICompat = { 'deepseekR1': { @@ -131,12 +171,6 @@ const openSourceModelOptions_assumingOAICompat = { supportsTools: false, supportsReasoning: false, }, - 'codestral': { - supportsFIM: true, - supportsSystemMessage: 'system-role', - supportsTools: 'openai-style', - supportsReasoning: false, - }, // llama 'llama3': { supportsFIM: false, @@ -188,17 +222,9 @@ const openSourceModelOptions_assumingOAICompat = { supportsTools: false, supportsReasoning: false, }, - 'codestral-latest': { - supportsFIM: true, - supportsSystemMessage: false, - supportsTools: false, - supportsReasoning: false, - }, + ...mistralModelOptions, } as const satisfies { [s: string]: Partial } - - - const extensiveModelFallback: ProviderSettings['modelOptionsFallback'] = (modelName) => { const toFallback = (opts: Omit): ModelOptions & { modelName: string } => { return { @@ -216,16 +242,15 @@ const extensiveModelFallback: ProviderSettings['modelOptionsFallback'] = (modelN if (modelName.includes('deepseek')) return toFallback({ ...openSourceModelOptions_assumingOAICompat.deepseekCoderV2, contextWindow: 32_000, maxOutputTokens: 4_096, }) if (modelName.includes('llama3')) return toFallback({ ...openSourceModelOptions_assumingOAICompat.llama3, contextWindow: 32_000, maxOutputTokens: 4_096, }) if (modelName.includes('qwen') && modelName.includes('2.5') && modelName.includes('coder')) return toFallback({ ...openSourceModelOptions_assumingOAICompat['qwen2.5coder'], contextWindow: 32_000, maxOutputTokens: 4_096, }) - if (modelName.includes('codestral')) return toFallback({ ...openSourceModelOptions_assumingOAICompat.codestral, contextWindow: 32_000, maxOutputTokens: 4_096, }) + + /* Mistral Options fallback for Mistral and others providers keeping the code relatively clean for all models */ + if (modelName.includes('mistral')) return toFallback({ ...openSourceModelOptions_assumingOAICompat['mistral-large-latest'] }) + if (modelName.includes('codestral')) return toFallback({ ...openSourceModelOptions_assumingOAICompat['codestral-latest'] }) + if (/\bo1\b/.test(modelName) || /\bo3\b/.test(modelName)) return toFallback(openAIModelOptions['o1']) return toFallback(modelOptionsDefaults) } - - - - - // ---------------- ANTHROPIC ---------------- const anthropicModelOptions = { 'claude-3-7-sonnet-20250219': { // https://docs.anthropic.com/en/docs/about-claude/models/all-models#model-comparison-table @@ -591,9 +616,6 @@ const openRouterSettings: ProviderSettings = { modelOptionsFallback: (modelName) => extensiveModelFallback(modelName), } - - - // ---------------- model settings of everything above ---------------- const modelSettingsOfProvider: { [providerName in ProviderName]: ProviderSettings } = { @@ -611,7 +633,7 @@ const modelSettingsOfProvider: { [providerName in ProviderName]: ProviderSetting vLLM: vLLMSettings, ollama: ollamaSettings, openAICompatible: openaiCompatible, - + mistral: mistralSettings, // googleVertex: {}, // microsoftAzure: {}, } as const diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index 87efbaf5..79d2204b 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -149,7 +149,7 @@ export const displayInfoOfProviderName = (providerName: ProviderName): DisplayIn } else if (providerName === 'mistral') { return { - title: 'Mistal.ai API', + title: 'Mistral.ai API', } } 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 844a8a78..26af462a 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 @@ -7,6 +7,10 @@ import Anthropic from '@anthropic-ai/sdk'; import { Ollama } from 'ollama'; import OpenAI, { ClientOptions } from 'openai'; + + + + import { Model as OpenAIModel } from 'openai/resources/models.js'; import { extractReasoningOnFinalMessage, extractReasoningOnTextWrapper } from '../../common/helpers/extractCodeFromResult.js'; import { LLMChatMessage, LLMFIMMessage, ModelListParams, OllamaModelResponse, OnError, OnFinalMessage, OnText } from '../../common/sendLLMMessageTypes.js'; @@ -112,6 +116,10 @@ const newOpenAICompatibleSDK = ({ settingsOfProvider, providerName, includeInPay const thisConfig = settingsOfProvider[providerName] return new OpenAI({ baseURL: 'https://api.x.ai/v1', apiKey: thisConfig.apiKey, ...commonPayloadOpts }) } + else if (providerName === 'mistral') { + const thisConfig = settingsOfProvider[providerName] + return new OpenAI({ baseURL: 'https://api.mistral.ai/v1', apiKey: thisConfig.apiKey, ...commonPayloadOpts }) + } else throw new Error(`Void providerName was invalid: ${providerName}.`) } @@ -495,11 +503,6 @@ export const sendLLMMessageToProviderImplementation = { sendFIM: null, list: null, }, - mistral: { - sendChat: (params) => _sendOpenAICompatibleChat(params), - sendFIM: null, // TODO // https://docs.mistral.ai/api/#tag/fim - list: null, - }, ollama: { sendChat: (params) => _sendOpenAICompatibleChat(params), sendFIM: sendOllamaFIM, @@ -530,6 +533,11 @@ export const sendLLMMessageToProviderImplementation = { sendFIM: null, list: null, }, + mistral: { + sendChat: (params) => _sendMistralChat(params), + sendFIM: (params) => _sendMistralFIM(params), + list: null, + }, } satisfies CallFnOfProvider From b7f764799cc1819dba0eccf0082d2deabcf0ac86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Commaret?= Date: Wed, 12 Mar 2025 15:34:35 +0100 Subject: [PATCH 049/173] stuff changed there also --- package-lock.json | 8 +-- package.json | 2 +- .../contrib/void/common/modelCapabilities.ts | 2 +- .../llmMessage/sendLLMMessage.impl.ts | 53 +++++++++++++++++-- 4 files changed, 56 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4a403917..8c614833 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@google/generative-ai": "^0.22.0", "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", - "@mistralai/mistralai": "^1.5.0", + "@mistralai/mistralai": "^1.5.1", "@parcel/watcher": "2.5.1", "@types/semver": "^7.5.8", "@vscode/deviceid": "^0.1.1", @@ -2540,9 +2540,9 @@ "integrity": "sha512-n1VPsljTSkthsAFYdiWfC+DKzK2WwcRp83Y1YAqdX552BstvsDjft9YXppjUzp11BPsapDoO1LDgrDB0XVsfNQ==" }, "node_modules/@mistralai/mistralai": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-1.5.0.tgz", - "integrity": "sha512-AIn8pwAwA/fDvEUvmkt+40zH1ZmfaG3Q7oUWl17GUEC1tU7ZPwYz8Cv9P59lyS1SisHdDSu81oknO7f1ywkz8Q==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-1.5.1.tgz", + "integrity": "sha512-Ie0EH4dAO11MEXR5N2kS2cgr+ycTWvqN/yP9bKrtmUEqjdcF4i7DLxtrFMUw5l2dOPhrkX93G4SziFiATPWu2w==", "dependencies": { "zod-to-json-schema": "^3.24.1" }, diff --git a/package.json b/package.json index 52387a3e..2a8f0a57 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "@google/generative-ai": "^0.22.0", "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", - "@mistralai/mistralai": "^1.5.0", + "@mistralai/mistralai": "^1.5.1", "@parcel/watcher": "2.5.1", "@types/semver": "^7.5.8", "@vscode/deviceid": "^0.1.1", diff --git a/src/vs/workbench/contrib/void/common/modelCapabilities.ts b/src/vs/workbench/contrib/void/common/modelCapabilities.ts index 266b2d40..7bffa921 100644 --- a/src/vs/workbench/contrib/void/common/modelCapabilities.ts +++ b/src/vs/workbench/contrib/void/common/modelCapabilities.ts @@ -582,7 +582,7 @@ const openRouterModelOptions_assumingOpenAICompat = { supportsReasoning: false, }, 'mistralai/codestral-2501': { - ...openSourceModelOptions_assumingOAICompat.codestral, + ...openSourceModelOptions_assumingOAICompat['codestral-latest'], contextWindow: 256_000, maxOutputTokens: null, cost: { input: 0.3, output: 0.9 }, 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 26af462a..a4efefb0 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 @@ -8,7 +8,10 @@ import { Ollama } from 'ollama'; import OpenAI, { ClientOptions } from 'openai'; - +/* Mistral standalone Fim endpoint */ +import { MistralCore } from "@mistralai/mistralai/core.js"; +import { fimComplete } from "@mistralai/mistralai/funcs/fimComplete.js"; +/* End Mistral standalone Fim endpoint */ import { Model as OpenAIModel } from 'openai/resources/models.js'; @@ -118,7 +121,7 @@ const newOpenAICompatibleSDK = ({ settingsOfProvider, providerName, includeInPay } else if (providerName === 'mistral') { const thisConfig = settingsOfProvider[providerName] - return new OpenAI({ baseURL: 'https://api.mistral.ai/v1', apiKey: thisConfig.apiKey, ...commonPayloadOpts }) + return new OpenAI({ baseURL: 'https://api.mistral.ai/v1', apiKey: thisConfig.apiKey }) } else throw new Error(`Void providerName was invalid: ${providerName}.`) @@ -472,7 +475,51 @@ const sendOllamaFIM = ({ messages: messages_, onFinalMessage, onError, settingsO }) } +const _sendMistralFIM = ({ messages: messages_, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, aiInstructions }: SendFIMParams_Internal) => { + const { modelName, supportsFIM } = getModelCapabilities(providerName, modelName_) + if (!supportsFIM) { + if (modelName === modelName_) + onError({ message: `Model ${modelName} does not support FIM.`, fullError: null }) + else + onError({ message: `Model ${modelName_} (${modelName}) does not support FIM.`, fullError: null }) + return + } + const messages = prepareFIMMessage({ messages: messages_, aiInstructions }) + const mistral = new MistralCore({ apiKey: settingsOfProvider.mistral.apiKey }) + + // DEBUG : request params + console.log('🔍 Sending FIM request with params:', { + model: modelName, + promptLength: messages.prefix.length, + suffixLength: messages.suffix.length, + stream: false, + maxTokens: messages.maxTokens + }); + + fimComplete( + mistral, { + model: modelName, + prompt: messages.prefix, + suffix: messages.suffix, + stream: false, + topP: 1, + maxTokens: messages.maxTokens, + stop: messages.stopTokens + }, + ) + + .then(async response => { + let content = response?.ok ? response.value.choices?.[0]?.message?.content : ''; + const fullText = typeof content === 'string' ? content : + Array.isArray(content) ? content.map(chunk => chunk.type === 'text' ? chunk.text : '').join('') : ''; + onFinalMessage({ fullText, fullReasoning: '', anthropicReasoning: null }); + console.log('✅ Réponse FIM reçue:', fullText); + }) + .catch(error => { + onError({ message: error + '', fullError: error }); + }) +} type CallFnOfProvider = { [providerName in ProviderName]: { @@ -534,7 +581,7 @@ export const sendLLMMessageToProviderImplementation = { list: null, }, mistral: { - sendChat: (params) => _sendMistralChat(params), + sendChat: (params) => _sendOpenAICompatibleChat(params), sendFIM: (params) => _sendMistralFIM(params), list: null, }, From bc6f68e1051c0161c2a739be11e53c4f8aded26c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Commaret?= Date: Wed, 12 Mar 2025 18:52:54 +0100 Subject: [PATCH 050/173] Hope it works --- .../llmMessage/sendLLMMessage.impl.ts | 74 ++++++++++++++----- 1 file changed, 57 insertions(+), 17 deletions(-) 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 a4efefb0..a7e98938 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 @@ -7,11 +7,8 @@ import Anthropic from '@anthropic-ai/sdk'; import { Ollama } from 'ollama'; import OpenAI, { ClientOptions } from 'openai'; - -/* Mistral standalone Fim endpoint */ -import { MistralCore } from "@mistralai/mistralai/core.js"; +import Mistral from '@mistralai/mistralai'; import { fimComplete } from "@mistralai/mistralai/funcs/fimComplete.js"; -/* End Mistral standalone Fim endpoint */ import { Model as OpenAIModel } from 'openai/resources/models.js'; @@ -121,7 +118,7 @@ const newOpenAICompatibleSDK = ({ settingsOfProvider, providerName, includeInPay } else if (providerName === 'mistral') { const thisConfig = settingsOfProvider[providerName] - return new OpenAI({ baseURL: 'https://api.mistral.ai/v1', apiKey: thisConfig.apiKey }) + return new Mistral({ apiKey: thisConfig.apiKey, ...commonPayloadOpts }) } else throw new Error(`Void providerName was invalid: ${providerName}.`) @@ -475,6 +472,59 @@ const sendOllamaFIM = ({ messages: messages_, onFinalMessage, onError, settingsO }) } +const _sendMistralChat = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, aiInstructions, tools: tools_ }: SendChatParams_Internal) => { + const { + modelName, + supportsReasoning, + supportsSystemMessage, + supportsTools, + // maxOutputTokens, right now we are ignoring this + } = getModelCapabilities(providerName, modelName_) + + const { + canIOReasoning, + openSourceThinkTags, + } = supportsReasoning || {} + + const { providerReasoningIOSettings } = getProviderCapabilities(providerName) + + const { messages } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsTools, supportsAnthropicReasoningSignature: false }) + + const thisConfig = settingsOfProvider[providerName] + const mistral = new Mistral({ apiKey: thisConfig.apiKey }) + + let fullTextSoFar = '' + let fullReasoningSoFar = '' + + const { needsManualParse: needsManualReasoningParse } = providerReasoningIOSettings?.output ?? {} + const manuallyParseReasoning = needsManualReasoningParse && canIOReasoning && openSourceThinkTags + if (manuallyParseReasoning) { + onText = extractReasoningOnTextWrapper(onText, openSourceThinkTags) + } + + mistral.chat.complete({ + model: modelName, + messages: messages, + stream: false, + }) + .then(response => { + const content = response.choices?.[0]?.message?.content + const fullText = typeof content === 'string' ? content : + Array.isArray(content) ? content.map(chunk => chunk.type === 'text' ? chunk.text : '').join('') : ''; + + onFinalMessage({ fullText, fullReasoning: '', toolCalls: [], anthropicReasoning: null }); + }) + .catch(error => { + if (error.status === 401) { + onError({ message: invalidApiKeyMessage(providerName), fullError: error }); + } else { + onError({ message: error + '', fullError: error }); + } + }) + +} + + const _sendMistralFIM = ({ messages: messages_, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, aiInstructions }: SendFIMParams_Internal) => { const { modelName, supportsFIM } = getModelCapabilities(providerName, modelName_) if (!supportsFIM) { @@ -486,16 +536,7 @@ const _sendMistralFIM = ({ messages: messages_, onFinalMessage, onError, setting } const messages = prepareFIMMessage({ messages: messages_, aiInstructions }) - const mistral = new MistralCore({ apiKey: settingsOfProvider.mistral.apiKey }) - - // DEBUG : request params - console.log('🔍 Sending FIM request with params:', { - model: modelName, - promptLength: messages.prefix.length, - suffixLength: messages.suffix.length, - stream: false, - maxTokens: messages.maxTokens - }); + const mistral = new Mistral({ providerName, settingsOfProvider, includeInPayload }) fimComplete( mistral, { @@ -508,7 +549,6 @@ const _sendMistralFIM = ({ messages: messages_, onFinalMessage, onError, setting stop: messages.stopTokens }, ) - .then(async response => { let content = response?.ok ? response.value.choices?.[0]?.message?.content : ''; const fullText = typeof content === 'string' ? content : @@ -581,7 +621,7 @@ export const sendLLMMessageToProviderImplementation = { list: null, }, mistral: { - sendChat: (params) => _sendOpenAICompatibleChat(params), + sendChat: (params) => _sendMistralChat(params), sendFIM: (params) => _sendMistralFIM(params), list: null, }, From cd512402d94a6575ae710660401642c0b0b94fa3 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Wed, 12 Mar 2025 14:34:58 -0700 Subject: [PATCH 051/173] collab --- .../react/src/sidebar-tsx/SidebarChat.tsx | 50 ++++++++++++------- 1 file changed, 31 insertions(+), 19 deletions(-) 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 aac9c3f2..3961d0d9 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 @@ -1043,7 +1043,7 @@ const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName return getBasename(toolParams.uri.fsPath); } else if (toolName === 'delete_uri') { const toolParams = _toolParams as ToolCallParams['delete_uri'] - return getBasename(toolParams.uri.fsPath) + ' (deleted)'; + return getBasename(toolParams.uri.fsPath); } else if (toolName === 'edit') { const toolParams = _toolParams as ToolCallParams['edit'] return getBasename(toolParams.uri.fsPath); @@ -1306,16 +1306,19 @@ const toolNameToComponent: { [T in ToolName]: { const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params) const icon = null - if (toolMessage.result.type === 'rejected') return null const isError = toolMessage.result.type === 'error' const componentParams: ToolHeaderParams = { title, desc1, isError, icon } - if (toolMessage.result.type !== 'error') { + if (toolMessage.result.type === 'success') { const { params } = toolMessage.result componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } } - else { + else if (toolMessage.result.type === 'rejected') { + const { params } = toolMessage.result + componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } + } + else if (toolMessage.result.type === 'error') { componentParams.children = <> {toolMessage.result.value} @@ -1347,16 +1350,18 @@ const toolNameToComponent: { [T in ToolName]: { const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params) const icon = null - if (toolMessage.result.type === 'rejected') return null - const isError = toolMessage.result.type === 'error' const componentParams: ToolHeaderParams = { title, desc1, isError, icon } - if (toolMessage.result.type !== 'error') { + if (toolMessage.result.type === 'success') { const { params } = toolMessage.result componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } } - else { + else if (toolMessage.result.type === 'rejected') { + const { params } = toolMessage.result + componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } + } + else if (toolMessage.result.type === 'error') { componentParams.children = <> {toolMessage.result.value} @@ -1389,17 +1394,20 @@ const toolNameToComponent: { [T in ToolName]: { const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params) const icon = null - if (toolMessage.result.type === 'rejected') return null - const isError = toolMessage.result.type === 'error' const componentParams: ToolHeaderParams = { title, desc1, isError, icon } - if (toolMessage.result.type !== 'error') { + if (toolMessage.result.type === 'success') { const { params } = toolMessage.result componentParams.children = componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } } - else { + else if (toolMessage.result.type === 'rejected') { + const { params } = toolMessage.result + componentParams.children = + componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } + } + else if (toolMessage.result.type === 'error') { componentParams.children = <> {toolMessage.result.value} @@ -1420,11 +1428,10 @@ const toolNameToComponent: { [T in ToolName]: { const isError = false const componentParams: ToolHeaderParams = { title, desc1, isError, icon, } - const { proposedTerminalId } = toolRequest.params + const { proposedTerminalId, waitForCompletion } = toolRequest.params if (terminalToolsService.terminalExists(proposedTerminalId)) componentParams.onClick = () => terminalToolsService.openTerminal(proposedTerminalId) - - if (!toolRequest.params.waitForCompletion) + if (!waitForCompletion) componentParams.desc2 = '(background task)' // TODO!!! open terminal @@ -1438,12 +1445,10 @@ const toolNameToComponent: { [T in ToolName]: { const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params) const icon = null - if (toolMessage.result.type === 'rejected') return null - const isError = toolMessage.result.type === 'error' const componentParams: ToolHeaderParams = { title, desc1, isError, icon } - if (toolMessage.result.type !== 'error') { + if (toolMessage.result.type === 'success') { const { command } = toolMessage.result.params const { terminalId, resolveReason, result } = toolMessage.result.value @@ -1469,7 +1474,14 @@ const toolNameToComponent: { [T in ToolName]: { if (resolveReason.type === 'bgtask') componentParams.desc2 = '(background task)' } - else { + else if (toolMessage.result.type === 'rejected') { + const { proposedTerminalId, waitForCompletion } = toolMessage.result.params + if (terminalToolsService.terminalExists(proposedTerminalId)) + componentParams.onClick = () => terminalToolsService.openTerminal(proposedTerminalId) + if (!waitForCompletion) + componentParams.desc2 = '(background task)' + } + else if (toolMessage.result.type === 'error') { componentParams.children = <> {toolMessage.result.value} From 8dcc3bf187190f402e2aeb240cb850092de9428d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Commaret?= Date: Thu, 13 Mar 2025 04:04:51 +0100 Subject: [PATCH 052/173] fix to do to types of Mistral connexion --- .../llmMessage/sendLLMMessage.impl.ts | 107 ++++++++++-------- 1 file changed, 60 insertions(+), 47 deletions(-) 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 a7e98938..0273e916 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 @@ -7,7 +7,8 @@ import Anthropic from '@anthropic-ai/sdk'; import { Ollama } from 'ollama'; import OpenAI, { ClientOptions } from 'openai'; -import Mistral from '@mistralai/mistralai'; +import Mistral from "@mistralai/mistralai"; +import { chatComplete } from "@mistralai/mistralai/funcs/chatComplete.js"; import { fimComplete } from "@mistralai/mistralai/funcs/fimComplete.js"; @@ -20,6 +21,7 @@ import { getModelSelectionState, getModelCapabilities, getProviderCapabilities } import { InternalToolInfo, ToolName, isAToolName } from '../../common/toolsServiceTypes.js'; + type InternalCommonMessageParams = { aiInstructions: string; onText: OnText; @@ -118,7 +120,7 @@ const newOpenAICompatibleSDK = ({ settingsOfProvider, providerName, includeInPay } else if (providerName === 'mistral') { const thisConfig = settingsOfProvider[providerName] - return new Mistral({ apiKey: thisConfig.apiKey, ...commonPayloadOpts }) + return new OpenAI({ apiKey: thisConfig.apiKey, ...commonPayloadOpts }) } else throw new Error(`Void providerName was invalid: ${providerName}.`) @@ -472,60 +474,70 @@ const sendOllamaFIM = ({ messages: messages_, onFinalMessage, onError, settingsO }) } -const _sendMistralChat = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, aiInstructions, tools: tools_ }: SendChatParams_Internal) => { +const sendMistralChat = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, aiInstructions }: SendChatParams_Internal) => { const { modelName, - supportsReasoning, supportsSystemMessage, supportsTools, - // maxOutputTokens, right now we are ignoring this } = getModelCapabilities(providerName, modelName_) - const { - canIOReasoning, - openSourceThinkTags, - } = supportsReasoning || {} - - const { providerReasoningIOSettings } = getProviderCapabilities(providerName) - - const { messages } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsTools, supportsAnthropicReasoningSignature: false }) - - const thisConfig = settingsOfProvider[providerName] - const mistral = new Mistral({ apiKey: thisConfig.apiKey }) - - let fullTextSoFar = '' - let fullReasoningSoFar = '' - - const { needsManualParse: needsManualReasoningParse } = providerReasoningIOSettings?.output ?? {} - const manuallyParseReasoning = needsManualReasoningParse && canIOReasoning && openSourceThinkTags - if (manuallyParseReasoning) { - onText = extractReasoningOnTextWrapper(onText, openSourceThinkTags) - } - - mistral.chat.complete({ - model: modelName, - messages: messages, - stream: false, + const { messages } = prepareMessages({ + messages: messages_, + aiInstructions, + supportsSystemMessage, + supportsTools, + supportsAnthropicReasoningSignature: false }) - .then(response => { - const content = response.choices?.[0]?.message?.content - const fullText = typeof content === 'string' ? content : - Array.isArray(content) ? content.map(chunk => chunk.type === 'text' ? chunk.text : '').join('') : ''; - onFinalMessage({ fullText, fullReasoning: '', toolCalls: [], anthropicReasoning: null }); - }) - .catch(error => { - if (error.status === 401) { - onError({ message: invalidApiKeyMessage(providerName), fullError: error }); + const mistral = new Mistral({ apiKey: settingsOfProvider[providerName].apiKey }) + + let fullText = '' + + console.log('🔍 Debug - Messages envoyés:', messages) + + chatComplete( + mistral, { + model: modelName, + messages: messages.map((m: any) => ({ + role: m.role, + content: m.content + })), + stream: true, + } + ) + .then(response => { + console.log('🔍 Debug - Réponse initiale:', response) + + if (!response?.ok) { + throw new Error('Response not ok') + } + + // Traitement direct de la réponse + const content = response.value.choices?.[0]?.message?.content + if (content) { + fullText = typeof content === 'string' ? content : content.map(chunk => chunk.type === 'text' ? chunk.text : '').join('') + onText({ fullText, fullReasoning: '' }) + onFinalMessage({ + fullText, + fullReasoning: '', + toolCalls: [], + anthropicReasoning: null + }) } else { - onError({ message: error + '', fullError: error }); + onError({ message: 'Void: Response from model was empty.', fullError: null }) + } + }) + .catch(error => { + console.error('❌ Debug - Erreur capturée:', error) + if (error.status === 401) { + onError({ message: invalidApiKeyMessage(providerName), fullError: error }) + } else { + onError({ message: error + '', fullError: error }) } }) - } - -const _sendMistralFIM = ({ messages: messages_, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, aiInstructions }: SendFIMParams_Internal) => { +const sendMistralFIM = ({ messages: messages_, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, aiInstructions }: SendFIMParams_Internal) => { const { modelName, supportsFIM } = getModelCapabilities(providerName, modelName_) if (!supportsFIM) { if (modelName === modelName_) @@ -536,8 +548,9 @@ const _sendMistralFIM = ({ messages: messages_, onFinalMessage, onError, setting } const messages = prepareFIMMessage({ messages: messages_, aiInstructions }) - const mistral = new Mistral({ providerName, settingsOfProvider, includeInPayload }) + const mistral = new Mistral({ apiKey: settingsOfProvider[providerName].apiKey }) + console.log('messages FIM', messages) fimComplete( mistral, { model: modelName, @@ -621,8 +634,8 @@ export const sendLLMMessageToProviderImplementation = { list: null, }, mistral: { - sendChat: (params) => _sendMistralChat(params), - sendFIM: (params) => _sendMistralFIM(params), + sendChat: (params) => sendMistralChat(params), + sendFIM: (params) => sendMistralFIM(params), list: null, }, } satisfies CallFnOfProvider @@ -640,7 +653,7 @@ codestral https://ollama.com/library/codestral/blobs/51707752a87c [SUFFIX]{{ .Suffix }}[PREFIX] {{ .Prompt }} deepseek-coder-v2 https://ollama.com/library/deepseek-coder-v2/blobs/22091531faf0 -<|fim▁begin|>{{ .Prompt }}<|fim▁hole|>{{ .Suffix }}<|fim▁end|> +{{ .Prompt }} starcoder2 https://ollama.com/library/starcoder2/blobs/3b190e68fefe From 654b577ce6ea04d651eb9afc0562f96188835fa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Commaret?= Date: Thu, 13 Mar 2025 18:23:05 +0100 Subject: [PATCH 053/173] Looks really hacky, but working ! YES ! --- .../llmMessage/sendLLMMessage.impl.ts | 121 +++++------------- 1 file changed, 30 insertions(+), 91 deletions(-) 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 0273e916..87507448 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 @@ -6,13 +6,14 @@ import Anthropic from '@anthropic-ai/sdk'; import { Ollama } from 'ollama'; import OpenAI, { ClientOptions } from 'openai'; - -import Mistral from "@mistralai/mistralai"; -import { chatComplete } from "@mistralai/mistralai/funcs/chatComplete.js"; -import { fimComplete } from "@mistralai/mistralai/funcs/fimComplete.js"; - - import { Model as OpenAIModel } from 'openai/resources/models.js'; + +// Mistral Core functions // +import { MistralCore } from "@mistralai/mistralai/core.js"; +import { fimComplete } from "@mistralai/mistralai/funcs/fimComplete.js"; +import { chatComplete } from "@mistralai/mistralai/funcs/chatComplete.js"; + + import { extractReasoningOnFinalMessage, extractReasoningOnTextWrapper } from '../../common/helpers/extractCodeFromResult.js'; import { LLMChatMessage, LLMFIMMessage, ModelListParams, OllamaModelResponse, OnError, OnFinalMessage, OnText } from '../../common/sendLLMMessageTypes.js'; import { defaultProviderSettings, displayInfoOfProviderName, ModelSelectionOptions, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js'; @@ -21,7 +22,6 @@ import { getModelSelectionState, getModelCapabilities, getProviderCapabilities } import { InternalToolInfo, ToolName, isAToolName } from '../../common/toolsServiceTypes.js'; - type InternalCommonMessageParams = { aiInstructions: string; onText: OnText; @@ -120,7 +120,7 @@ const newOpenAICompatibleSDK = ({ settingsOfProvider, providerName, includeInPay } else if (providerName === 'mistral') { const thisConfig = settingsOfProvider[providerName] - return new OpenAI({ apiKey: thisConfig.apiKey, ...commonPayloadOpts }) + return new OpenAI({ baseURL: 'https://api.mistral.ai/v1', apiKey: thisConfig.apiKey, ...commonPayloadOpts }) } else throw new Error(`Void providerName was invalid: ${providerName}.`) @@ -474,67 +474,19 @@ const sendOllamaFIM = ({ messages: messages_, onFinalMessage, onError, settingsO }) } +//////// MISTRAL //////// const sendMistralChat = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, aiInstructions }: SendChatParams_Internal) => { - const { - modelName, - supportsSystemMessage, - supportsTools, - } = getModelCapabilities(providerName, modelName_) - - const { messages } = prepareMessages({ + _sendOpenAICompatibleChat({ messages: messages_, - aiInstructions, - supportsSystemMessage, - supportsTools, - supportsAnthropicReasoningSignature: false - }) - - const mistral = new Mistral({ apiKey: settingsOfProvider[providerName].apiKey }) - - let fullText = '' - - console.log('🔍 Debug - Messages envoyés:', messages) - - chatComplete( - mistral, { - model: modelName, - messages: messages.map((m: any) => ({ - role: m.role, - content: m.content - })), - stream: true, - } - ) - .then(response => { - console.log('🔍 Debug - Réponse initiale:', response) - - if (!response?.ok) { - throw new Error('Response not ok') - } - - // Traitement direct de la réponse - const content = response.value.choices?.[0]?.message?.content - if (content) { - fullText = typeof content === 'string' ? content : content.map(chunk => chunk.type === 'text' ? chunk.text : '').join('') - onText({ fullText, fullReasoning: '' }) - onFinalMessage({ - fullText, - fullReasoning: '', - toolCalls: [], - anthropicReasoning: null - }) - } else { - onError({ message: 'Void: Response from model was empty.', fullError: null }) - } - }) - .catch(error => { - console.error('❌ Debug - Erreur capturée:', error) - if (error.status === 401) { - onError({ message: invalidApiKeyMessage(providerName), fullError: error }) - } else { - onError({ message: error + '', fullError: error }) - } - }) + onText, + onFinalMessage, + onError, + settingsOfProvider, + modelName: modelName_, + _setAborter, + providerName, + aiInstructions + }); } const sendMistralFIM = ({ messages: messages_, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, aiInstructions }: SendFIMParams_Internal) => { @@ -548,32 +500,19 @@ const sendMistralFIM = ({ messages: messages_, onFinalMessage, onError, settings } const messages = prepareFIMMessage({ messages: messages_, aiInstructions }) - const mistral = new Mistral({ apiKey: settingsOfProvider[providerName].apiKey }) - - console.log('messages FIM', messages) - fimComplete( - mistral, { - model: modelName, - prompt: messages.prefix, - suffix: messages.suffix, - stream: false, - topP: 1, - maxTokens: messages.maxTokens, - stop: messages.stopTokens - }, - ) - .then(async response => { - let content = response?.ok ? response.value.choices?.[0]?.message?.content : ''; - const fullText = typeof content === 'string' ? content : - Array.isArray(content) ? content.map(chunk => chunk.type === 'text' ? chunk.text : '').join('') : ''; - onFinalMessage({ fullText, fullReasoning: '', anthropicReasoning: null }); - console.log('✅ Réponse FIM reçue:', fullText); - }) - .catch(error => { - onError({ message: error + '', fullError: error }); - }) + _sendOpenAICompatibleFIM({ + messages: messages_, + onFinalMessage, + onError, + settingsOfProvider, + modelName: modelName_, + _setAborter, + providerName, + aiInstructions + }); } + type CallFnOfProvider = { [providerName in ProviderName]: { sendChat: (params: SendChatParams_Internal) => void; From 27b782ff320c6f5bf103fde6e45ad4f99f13ef70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Commaret?= Date: Thu, 13 Mar 2025 18:24:05 +0100 Subject: [PATCH 054/173] HOURRA ! --- .../void/electron-main/llmMessage/sendLLMMessage.impl.ts | 6 ------ 1 file changed, 6 deletions(-) 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 87507448..0c10e5ac 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 @@ -8,12 +8,6 @@ import { Ollama } from 'ollama'; import OpenAI, { ClientOptions } from 'openai'; import { Model as OpenAIModel } from 'openai/resources/models.js'; -// Mistral Core functions // -import { MistralCore } from "@mistralai/mistralai/core.js"; -import { fimComplete } from "@mistralai/mistralai/funcs/fimComplete.js"; -import { chatComplete } from "@mistralai/mistralai/funcs/chatComplete.js"; - - import { extractReasoningOnFinalMessage, extractReasoningOnTextWrapper } from '../../common/helpers/extractCodeFromResult.js'; import { LLMChatMessage, LLMFIMMessage, ModelListParams, OllamaModelResponse, OnError, OnFinalMessage, OnText } from '../../common/sendLLMMessageTypes.js'; import { defaultProviderSettings, displayInfoOfProviderName, ModelSelectionOptions, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js'; From edefe841d5959e04cc43aa551af100c630491094 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 13 Mar 2025 16:54:29 -0700 Subject: [PATCH 055/173] changes in the BG work!! + autosave --- .../contrib/void/browser/chatThreadService.ts | 1 + .../contrib/void/browser/editCodeService.ts | 258 ++++++++++-------- .../void/browser/editCodeServiceInterface.ts | 3 +- .../contrib/void/browser/helpers/readFile.ts | 52 ---- .../src/markdown/ApplyBlockHoverButtons.tsx | 12 +- .../react/src/markdown/ChatMarkdownRender.tsx | 2 - .../react/src/sidebar-tsx/SidebarChat.tsx | 20 +- .../void/browser/react/src/util/inputs.tsx | 4 +- .../contrib/void/browser/toolsService.ts | 5 +- .../contrib/void/common/prompt/prompts.ts | 2 +- .../contrib/void/common/voidFileService.ts | 38 ++- 11 files changed, 204 insertions(+), 193 deletions(-) delete mode 100644 src/vs/workbench/contrib/void/browser/helpers/readFile.ts diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 76b06723..04cef415 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -818,6 +818,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { } } } finally { + modelRef.object.dispose(); modelRef.dispose(); } } diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 9bd03b56..f8e47af9 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -12,7 +12,7 @@ import { ICodeEditor, IOverlayWidget, IViewZone, OverlayWidgetPositionPreference import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; // import { throttle } from '../../../../base/common/decorators.js'; import { ComputedDiff, findDiffs } from './helpers/findDiffs.js'; -import { EndOfLinePreference, IModelDecorationOptions, ITextModel } from '../../../../editor/common/model.js'; +import { IModelDecorationOptions, ITextModel } from '../../../../editor/common/model.js'; import { IRange } from '../../../../editor/common/core/range.js'; import { registerColor } from '../../../../platform/theme/common/colorUtils.js'; import { Color, RGBA } from '../../../../base/common/color.js'; @@ -45,6 +45,9 @@ import { IVoidFileService } from '../common/voidFileService.js'; import { IEditCodeService, URIStreamState, AddCtrlKOpts, StartApplyingOpts } from './editCodeServiceInterface.js'; import { IVoidSettingsService } from '../common/voidSettingsService.js'; import { FeatureName } from '../common/voidSettingsTypes.js'; +import { ILanguageService } from '../../../../editor/common/languages/language.js'; +// import { IFileService } from '../../../../platform/files/common/files.js'; +// import { VSBuffer } from '../../../../base/common/buffer.js'; const configOfBG = (color: Color) => { return { dark: color, light: color, hcDark: color, hcLight: color, } @@ -67,7 +70,7 @@ registerColor('void.sweepIdxBG', configOfBG(sweepIdxBG), '', true); - +const numLinesOfStr = (str: string) => str.split('\n').length const getLeadingWhitespacePx = (editor: ICodeEditor, startLine: number): number => { @@ -118,7 +121,7 @@ const findTextInCode = (text: string, fileContents: string, startingAtLine?: num const lastIdx = fileContents.lastIndexOf(text) if (lastIdx !== idx) return 'Not unique' as const const startLine = fileContents.substring(0, idx).split('\n').length - const numLines = text.split('\n').length + const numLines = numLinesOfStr(text) const endLine = startLine + numLines - 1 return [startLine, endLine] as const } @@ -237,7 +240,7 @@ class EditCodeService extends Disposable implements IEditCodeService { // URI <--> model - diffAreasOfURI: Record> = {} + diffAreasOfURI: Record | undefined> = {} diffAreaOfId: Record = {}; diffOfId: Record = {}; // redundant with diffArea._diffs @@ -259,7 +262,7 @@ class EditCodeService extends Disposable implements IEditCodeService { constructor( // @IHistoryService private readonly _historyService: IHistoryService, // history service is the history of pressing alt left/right - @ICodeEditorService private readonly _editorService: ICodeEditorService, + @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, @IModelService private readonly _modelService: IModelService, @IUndoRedoService private readonly _undoRedoService: IUndoRedoService, // undoRedo service is the history of pressing ctrl+z @ILLMMessageService private readonly _llmMessageService: ILLMMessageService, @@ -271,15 +274,22 @@ class EditCodeService extends Disposable implements IEditCodeService { @ICommandService private readonly _commandService: ICommandService, @IVoidFileService private readonly _voidFileService: IVoidFileService, @IVoidSettingsService private readonly _settingsService: IVoidSettingsService, + // @IFileService private readonly _fileService: IFileService, + @ILanguageService private readonly _languageService: ILanguageService, ) { super(); // this function initializes data structures and listens for changes + const registeredModelListeners = new Set() const initializeModel = (model: ITextModel) => { + + // do not add listeners to the same model twice - important, or will see duplicates + if (registeredModelListeners.has(model.uri.fsPath)) return + registeredModelListeners.add(model.uri.fsPath) + if (!(model.uri.fsPath in this.diffAreasOfURI)) { this.diffAreasOfURI[model.uri.fsPath] = new Set(); } - else return // do not add listeners to the same model twice - important, or will see duplicates // when the user types, realign diff areas and re-render them this._register( @@ -292,7 +302,7 @@ class EditCodeService extends Disposable implements IEditCodeService { ) // when a stream starts or ends, fire the event for onDidChangeURIStreamState - let prevStreamState = this.getURIStreamState({ uri: model.uri }) + let prevStreamState = this.getURIStreamState({ uri: null }) const updateAcceptRejectAllUI = () => { const state = this.getURIStreamState({ uri: model.uri }) let prevStateActual = prevStreamState @@ -316,11 +326,12 @@ class EditCodeService extends Disposable implements IEditCodeService { this._register(this._onDidChangeDiffZoneStreaming.event(({ uri: uri_ }) => { if (uri_.fsPath === model.uri.fsPath) updateAcceptRejectAllUI() })) this._register(this._onDidAddOrDeleteDiffZones.event(({ uri: uri_ }) => { if (uri_.fsPath === model.uri.fsPath) updateAcceptRejectAllUI() })) - + // when the model first mounts, refresh any diffs that might be on it (happens if diffs were added in the BG) + this._refreshStylesAndDiffsInURI(model.uri) } // initialize all existing models + initialize when a new model mounts for (let model of this._modelService.getModels()) { initializeModel(model) } - this._register(this._modelService.onModelAdded(model => initializeModel(model))); + this._register(this._modelService.onModelAdded(model => { console.log('initialized model!!', model.uri.fsPath); initializeModel(model) })); // this function adds listeners to refresh styles when editor changes tab @@ -328,9 +339,10 @@ class EditCodeService extends Disposable implements IEditCodeService { const uri = editor.getModel()?.uri ?? null if (uri) this._refreshStylesAndDiffsInURI(uri) } + // add listeners for all existing editors + listen for editor being added - for (let editor of this._editorService.listCodeEditors()) { initializeEditor(editor) } - this._register(this._editorService.onCodeEditorAdd(editor => { initializeEditor(editor) })) + for (let editor of this._codeEditorService.listCodeEditors()) { initializeEditor(editor) } + this._register(this._codeEditorService.onCodeEditorAdd(editor => { initializeEditor(editor) })) } @@ -342,16 +354,6 @@ class EditCodeService extends Disposable implements IEditCodeService { this._refreshStylesAndDiffsInURI(uri) } - private _onInternalChangeContent(uri: URI, { shouldRealign }: { shouldRealign: false | { newText: string, oldRange: IRange } }) { - if (shouldRealign) { - const { newText, oldRange } = shouldRealign - // console.log('realiging', newText, oldRange) - this._realignAllDiffAreasLines(uri, newText, oldRange) - } - this._refreshStylesAndDiffsInURI(uri) - - } - @@ -424,7 +426,7 @@ class EditCodeService extends Disposable implements IEditCodeService { private _computeDiffsAndAddStylesToURI = (uri: URI) => { - const fullFileText = this._readURI(uri) ?? '' + const fullFileText = this._voidFileService.readModel(uri) ?? '' for (const diffareaid of this.diffAreasOfURI[uri.fsPath] || []) { const diffArea = this.diffAreaOfId[diffareaid] @@ -485,7 +487,7 @@ class EditCodeService extends Disposable implements IEditCodeService { private _addCtrlKZoneInput = (ctrlKZone: CtrlKZone) => { const { editorId } = ctrlKZone - const editor = this._editorService.listCodeEditors().find(e => e.getId() === editorId) + const editor = this._codeEditorService.listCodeEditors().find(e => e.getId() === editorId) if (!editor) { return null } let zoneId: string | null = null @@ -587,7 +589,8 @@ class EditCodeService extends Disposable implements IEditCodeService { const disposeInThisEditorFns: (() => void)[] = [] - const model = this._modelService.getModel(uri) + const model = this._getModel(uri) + if (model === null) return // green decoration and minimap decoration if (type !== 'deletion') { @@ -724,6 +727,18 @@ class EditCodeService extends Disposable implements IEditCodeService { + + + + // private _readURI(uri: URI, range?: IRange): string | null { + // if (!range) return this._getModel(uri)?.getValue(EndOfLinePreference.LF) ?? null + // else return this._getModel(uri)?.getValueInRange(range, EndOfLinePreference.LF) ?? null + // } + // private _getNumLines(uri: URI): number | null { + // return this._getModel(uri)?.getLineCount() ?? null + // } + + private _getModel(uri: URI) { const model = this._modelService.getModel(uri) if (!model || model.isDisposed()) { @@ -731,15 +746,22 @@ class EditCodeService extends Disposable implements IEditCodeService { } return model } - private _readURI(uri: URI, range?: IRange): string | null { - if (!range) return this._getModel(uri)?.getValue(EndOfLinePreference.LF) ?? null - else return this._getModel(uri)?.getValueInRange(range, EndOfLinePreference.LF) ?? null - } - private _getNumLines(uri: URI): number | null { - return this._getModel(uri)?.getLineCount() ?? null + // not obvious at all, but if we want the model we can just create it. our listeners in the constructor handle it + private async _forceModel(uri: URI) { + + const m = this._getModel(uri) + if (m !== null) return m + + const fileStr = await this._voidFileService.readFile(uri) + if (fileStr === null) return null + const lang = this._languageService.createByFilepathOrFirstLine(uri, fileStr?.split('\n')?.[0]) + const model = this._modelService.createModel(fileStr, lang, uri); + return model } + + private _getActiveEditorURI(): URI | null { - const editor = this._editorService.getActiveCodeEditor() + const editor = this._codeEditorService.getActiveCodeEditor() if (!editor) return null const uri = editor.getModel()?.uri if (!uri) return null @@ -747,38 +769,44 @@ class EditCodeService extends Disposable implements IEditCodeService { } weAreWriting = false - private _writeText(uri: URI, text: string, range: IRange, { shouldRealignDiffAreas }: { shouldRealignDiffAreas: boolean }) { - const model = this._getModel(uri) - if (!model) return - const uriStr = this._readURI(uri, range) - if (uriStr === null) return + private async _writeURIText(uri: URI, text: string, range_: IRange | 'wholeFileRange', { shouldRealignDiffAreas, }: { shouldRealignDiffAreas: boolean, }) { + let m = this._getModel(uri) + if (m === null) { m = await this._forceModel(uri) } + if (m === null) return // if still null, return + const model: ITextModel = m + const range: IRange = range_ === 'wholeFileRange' ? + { startLineNumber: 1, startColumn: 1, endLineNumber: model.getLineCount(), endColumn: Number.MAX_SAFE_INTEGER } // whole file + : range_ - // heuristic check if don't need to make edits + // realign is 100% independent from written text (diffareas are nonphysical), can do this first + if (shouldRealignDiffAreas) { + const newText = text + const oldRange = range + this._realignAllDiffAreasLines(uri, newText, oldRange) + } + + const uriStr = model.getValue() + + // heuristic check const dontNeedToWrite = uriStr === text if (dontNeedToWrite) { - // at the end of a write, we still expect to refresh all styles - // e.g. sometimes we expect to restore all the decorations even if no edits were made when _writeText is used - this._refreshStylesAndDiffsInURI(uri) + this._refreshStylesAndDiffsInURI(uri) // at the end of a write, we still expect to refresh all styles. e.g. sometimes we expect to restore all the decorations even if no edits were made when _writeText is used return } - // minimal edits so not so flashy - // const edits = this.worker.$Void_computeMoreMinimalEdits(uri.toString(), [{ range, text }], false) this.weAreWriting = true model.applyEdits([{ range, text }]) this.weAreWriting = false - this._onInternalChangeContent(uri, { shouldRealign: shouldRealignDiffAreas && { newText: text, oldRange: range } }) - + this._refreshStylesAndDiffsInURI(uri) } - private _addToHistory(uri: URI, opts?: { onUndo?: () => void }) { - const getCurrentSnapshot = (): HistorySnapshot => { + const getCurrentSnapshot = async (): Promise => { const snapshottedDiffAreaOfId: Record = {} for (const diffareaid in this.diffAreaOfId) { @@ -790,13 +818,16 @@ class EditCodeService extends Disposable implements IEditCodeService { Object.fromEntries(diffAreaSnapshotKeys.map(key => [key, diffArea[key]])) ) as DiffAreaSnapshot } + + const entireFileCode = await this._voidFileService.readFile(uri) ?? '' return { snapshottedDiffAreaOfId, - entireFileCode: this._readURI(uri) ?? '', // the whole file's code + entireFileCode, // the whole file's code } } - const restoreDiffAreas = (snapshot: HistorySnapshot) => { + const restoreDiffAreas = async (snapshotPromise: Promise) => { + const snapshot = await snapshotPromise // for each diffarea in this uri, stop streaming if currently streaming for (const diffareaid in this.diffAreaOfId) { @@ -807,7 +838,7 @@ class EditCodeService extends Disposable implements IEditCodeService { // delete all diffareas on this uri (clearing their styles) this._deleteAllDiffAreas(uri) - this.diffAreasOfURI[uri.fsPath].clear() + this.diffAreasOfURI[uri.fsPath]?.clear() const { snapshottedDiffAreaOfId, entireFileCode: entireModelCode } = structuredClone(snapshot) // don't want to destroy the snapshot @@ -835,23 +866,19 @@ class EditCodeService extends Disposable implements IEditCodeService { _linkedStreamingDiffZone: null, // when restoring, we will never be streaming } } - this.diffAreasOfURI[uri.fsPath].add(diffareaid) + this._addOrInitializeDiffAreaAtURI(uri, diffareaid) } this._onDidAddOrDeleteDiffZones.fire({ uri }) // restore file content - const numLines = this._getNumLines(uri) - if (numLines === null) return - - - this._writeText(uri, entireModelCode, - { startColumn: 1, startLineNumber: 1, endLineNumber: numLines, endColumn: Number.MAX_SAFE_INTEGER }, + this._writeURIText(uri, entireModelCode, + 'wholeFileRange', { shouldRealignDiffAreas: false } ) } - const beforeSnapshot: HistorySnapshot = getCurrentSnapshot() - let afterSnapshot: HistorySnapshot | null = null + const beforeSnapshot: Promise = getCurrentSnapshot() + let afterSnapshot: Promise | null = null const elt: IUndoRedoElement = { type: UndoRedoElementType.Resource, @@ -863,7 +890,7 @@ class EditCodeService extends Disposable implements IEditCodeService { } this._undoRedoService.pushElement(elt) - const onFinishEdit = () => { afterSnapshot = getCurrentSnapshot() } + const onFinishEdit = async () => { afterSnapshot = getCurrentSnapshot() } return { onFinishEdit } } @@ -906,26 +933,26 @@ class EditCodeService extends Disposable implements IEditCodeService { private _deleteDiffZone(diffZone: DiffZone) { this._clearAllDiffAreaEffects(diffZone) delete this.diffAreaOfId[diffZone.diffareaid] - this.diffAreasOfURI[diffZone._URI.fsPath].delete(diffZone.diffareaid.toString()) + this.diffAreasOfURI[diffZone._URI.fsPath]?.delete(diffZone.diffareaid.toString()) this._onDidAddOrDeleteDiffZones.fire({ uri: diffZone._URI }) } private _deleteTrackingZone(trackingZone: TrackingZone) { delete this.diffAreaOfId[trackingZone.diffareaid] - this.diffAreasOfURI[trackingZone._URI.fsPath].delete(trackingZone.diffareaid.toString()) + this.diffAreasOfURI[trackingZone._URI.fsPath]?.delete(trackingZone.diffareaid.toString()) } private _deleteCtrlKZone(ctrlKZone: CtrlKZone) { this._clearAllEffects(ctrlKZone._URI) ctrlKZone._mountInfo?.dispose() delete this.diffAreaOfId[ctrlKZone.diffareaid] - this.diffAreasOfURI[ctrlKZone._URI.fsPath].delete(ctrlKZone.diffareaid.toString()) + this.diffAreasOfURI[ctrlKZone._URI.fsPath]?.delete(ctrlKZone.diffareaid.toString()) } private _deleteAllDiffAreas(uri: URI) { const diffAreas = this.diffAreasOfURI[uri.fsPath] - diffAreas.forEach(diffareaid => { + diffAreas?.forEach(diffareaid => { const diffArea = this.diffAreaOfId[diffareaid] if (diffArea.type === 'DiffZone') this._deleteDiffZone(diffArea) @@ -934,13 +961,16 @@ class EditCodeService extends Disposable implements IEditCodeService { }) } - + private _addOrInitializeDiffAreaAtURI = (uri: URI, diffareaid: string | number) => { + if (!(uri.fsPath in this.diffAreasOfURI)) this.diffAreasOfURI[uri.fsPath] = new Set() + this.diffAreasOfURI[uri.fsPath]?.add(diffareaid.toString()) + } private _diffareaidPool = 0 // each diffarea has an id private _addDiffArea(diffArea: Omit): T { const diffareaid = this._diffareaidPool++ const diffArea2 = { ...diffArea, diffareaid } as T - this.diffAreasOfURI[diffArea2._URI.fsPath].add(diffareaid.toString()) + this._addOrInitializeDiffAreaAtURI(diffArea._URI, diffareaid) this.diffAreaOfId[diffareaid] = diffArea2 return diffArea2 } @@ -958,7 +988,7 @@ class EditCodeService extends Disposable implements IEditCodeService { } const fn = this._addDiffStylesToURI(uri, newDiff) - diffZone._removeStylesFns.add(fn) + if (fn) diffZone._removeStylesFns.add(fn) this.diffOfId[diffid] = newDiff diffZone._diffOfId[diffid] = newDiff @@ -974,9 +1004,6 @@ class EditCodeService extends Disposable implements IEditCodeService { // console.log('recent change', recentChange) - const model = this._getModel(uri) - if (!model) return - // compute net number of newlines lines that were added/removed const startLine = recentChange.startLineNumber const endLine = recentChange.endLineNumber @@ -984,7 +1011,7 @@ class EditCodeService extends Disposable implements IEditCodeService { const newTextHeight = (text.match(/\n/g) || []).length + 1 // number of newlines is number of \n's + 1, e.g. "ab\ncd" // compute overlap with each diffArea and shrink/elongate each diffArea accordingly - for (const diffareaid of this.diffAreasOfURI[model.uri.fsPath] || []) { + for (const diffareaid of this.diffAreasOfURI[uri.fsPath] || []) { const diffArea = this.diffAreaOfId[diffareaid] // if the diffArea is entirely above the range, it is not affected @@ -1085,7 +1112,7 @@ class EditCodeService extends Disposable implements IEditCodeService { // at the start, add a newline between the stream and originalCode to make reasoning easier if (!latestMutable.addedSplitYet) { - this._writeText(uri, '\n', + this._writeURIText(uri, '\n', { startLineNumber: latestMutable.line, startColumn: latestMutable.col, endLineNumber: latestMutable.line, endColumn: latestMutable.col, }, { shouldRealignDiffAreas: true } ) @@ -1094,7 +1121,7 @@ class EditCodeService extends Disposable implements IEditCodeService { } // insert deltaText at latest line and col - this._writeText(uri, deltaText, + this._writeURIText(uri, deltaText, { startLineNumber: latestMutable.line, startColumn: latestMutable.col, endLineNumber: latestMutable.line, endColumn: latestMutable.col }, { shouldRealignDiffAreas: true } ) @@ -1108,7 +1135,7 @@ class EditCodeService extends Disposable implements IEditCodeService { if (latestMutable.originalCodeStartLine < startLineInOriginalCode) { // moved up, delete const numLinesDeleted = startLineInOriginalCode - latestMutable.originalCodeStartLine - this._writeText(uri, '', + this._writeURIText(uri, '', { startLineNumber: latestMutable.line, startColumn: latestMutable.col, endLineNumber: latestMutable.line + numLinesDeleted, endColumn: Number.MAX_SAFE_INTEGER, }, { shouldRealignDiffAreas: true } ) @@ -1116,7 +1143,7 @@ class EditCodeService extends Disposable implements IEditCodeService { } else if (latestMutable.originalCodeStartLine > startLineInOriginalCode) { const newText = '\n' + originalCode.split('\n').slice((startLineInOriginalCode - 1), (latestMutable.originalCodeStartLine - 1) - 1 + 1).join('\n') - this._writeText(uri, newText, + this._writeURIText(uri, newText, { startLineNumber: latestMutable.line, startColumn: latestMutable.col, endLineNumber: latestMutable.line, endColumn: latestMutable.col }, { shouldRealignDiffAreas: true } ) @@ -1189,14 +1216,21 @@ class EditCodeService extends Disposable implements IEditCodeService { - // throws if there's an error - public startApplying(opts: StartApplyingOpts): [URI, Promise] | null { + // the applyDonePromise this returns can throw an error (reject) + public async startApplying(opts: StartApplyingOpts): Promise<[URI, Promise] | null> { let res: [DiffZone, Promise] | undefined = undefined - if (opts.type === 'rewrite') res = this._initializeWriteoverStream(opts) - else if (opts.type === 'searchReplace') res = this._initializeSearchAndReplaceStream(opts) + if (opts.type === 'rewrite') res = await this._initializeWriteoverStream(opts) + else if (opts.type === 'searchReplace') res = await this._initializeSearchAndReplaceStream(opts) if (!res) return null const [diffZone, applyDonePromise] = res + applyDonePromise.then(() => { + const diffareaids = this.diffAreasOfURI[diffZone._URI.fsPath] + for (const diffareaid of diffareaids || []) { + const diffArea = this.diffAreaOfId[diffareaid] + console.log('DA!!', diffArea) + } + }) return [diffZone._URI, applyDonePromise] } @@ -1222,29 +1256,33 @@ class EditCodeService extends Disposable implements IEditCodeService { - private _initializeWriteoverStream(opts: StartApplyingOpts): [DiffZone, Promise] | undefined { + private async _initializeWriteoverStream(opts: StartApplyingOpts): Promise<[DiffZone, Promise] | undefined> { const { from, } = opts let startLine: number let endLine: number let uri: URI + let currentFileStr: string if (from === 'ClickApply') { const uri_ = this._getActiveEditorURI() if (!uri_) return uri = uri_ + const c_ = await this._voidFileService.readFile(uri) + if (c_ === null) return + currentFileStr = c_ - // reject all diffZones on this URI, adding to history (there can't possibly be overlap after this) - this.removeDiffAreas({ uri, behavior: 'reject', removeCtrlKs: true }) + const numLines = numLinesOfStr(currentFileStr) + + // remove all diffZones on this URI, adding to history (there can't possibly be overlap after this) + const behavior: 'accept' | 'reject' = opts.startBehavior === 'accept-conflicts' ? 'accept' : 'reject' + this.removeDiffAreas({ uri, behavior, removeCtrlKs: true }) // in ctrl+L the start and end lines are the full document - const numLines = this._getNumLines(uri) - if (numLines === null) return startLine = 1 endLine = numLines - } else if (from === 'QuickEdit') { const { diffareaid } = opts @@ -1253,6 +1291,11 @@ class EditCodeService extends Disposable implements IEditCodeService { const { startLine: startLine_, endLine: endLine_, _URI } = ctrlKZone uri = _URI + + const c_ = await this._voidFileService.readFile(uri) + if (c_ === null) return + currentFileStr = c_ + startLine = startLine_ endLine = endLine_ } @@ -1260,14 +1303,9 @@ class EditCodeService extends Disposable implements IEditCodeService { throw new Error(`Void: diff.type not recognized on: ${from}`) } - const currentFileStr = this._readURI(uri) - if (currentFileStr === null) return const originalCode = currentFileStr.split('\n').slice((startLine - 1), (endLine - 1) + 1).join('\n') - - let streamRequestIdRef: { current: string | null } = { current: null } - // promise that resolves when the apply is done let resApplyPromise: () => void let rejApplyPromise: (e: any) => void @@ -1408,10 +1446,11 @@ class EditCodeService extends Disposable implements IEditCodeService { // console.log('DONE! FULL TEXT\n', extractText(fullText), diffZone.startLine, diffZone.endLine) // at the end, re-write whole thing to make sure no sync errors const [croppedText, _1, _2] = extractText(fullText, 0) - this._writeText(uri, croppedText, + this._writeURIText(uri, croppedText, { startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed { shouldRealignDiffAreas: true } ) + this._voidFileService.saveOrWriteFileAssumingModelExists(uri) onDone() resMessageDonePromise() }, @@ -1434,7 +1473,7 @@ class EditCodeService extends Disposable implements IEditCodeService { - private _initializeSearchAndReplaceStream(opts: StartApplyingOpts & { from: 'ClickApply' }): [DiffZone, Promise] | undefined { + private async _initializeSearchAndReplaceStream(opts: StartApplyingOpts & { from: 'ClickApply' }): Promise<[DiffZone, Promise] | undefined> { const { from, applyStr, uri: givenURI, } = opts let uri: URI @@ -1447,13 +1486,11 @@ class EditCodeService extends Disposable implements IEditCodeService { uri = givenURI } - // generate search/replace block text - const originalFileCode = this._voidFileService.readModel(uri) + const originalFileCode = await this._voidFileService.readFile(uri) if (originalFileCode === null) return - const numLines = this._getNumLines(uri) - if (numLines === null) return + const numLines = numLinesOfStr(originalFileCode) // reject all diffZones on this URI, adding to history (there can't possibly be overlap after this) this.removeDiffAreas({ uri, behavior: 'reject', removeCtrlKs: true }) @@ -1507,6 +1544,7 @@ class EditCodeService extends Disposable implements IEditCodeService { this._onDidChangeDiffZoneStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) this._onDidAddOrDeleteDiffZones.fire({ uri }) + console.log('b', uri) const convertOriginalRangeToFinalRange = (originalRange: readonly [number, number]): [number, number] => { @@ -1608,9 +1646,8 @@ class EditCodeService extends Disposable implements IEditCodeService { } } else { - // TODO!!! test this // starting line is at least the number of lines in the generated code minus 1 - const numLinesInOrig = block.orig.split('\n').length - 1 + const numLinesInOrig = numLinesOfStr(block.orig) diffZone._streamState.line = Math.max(numLinesInOrig - 1, 1, diffZone._streamState.line ?? 1) } // must be done writing original to move on to writing streamed content @@ -1670,12 +1707,11 @@ class EditCodeService extends Disposable implements IEditCodeService { console.error('DiffZone was not in streaming state in _initializeSearchAndReplaceStream') continue } - if (!latestStreamLocationMutable) continue // if a block is done, finish it by writing all if (block.state === 'done') { const { startLine: finalStartLine, endLine: finalEndLine } = addedTrackingZoneOfBlockNum[blockNum] - this._writeText(uri, block.final, + this._writeURIText(uri, block.final, { startLineNumber: finalStartLine, startColumn: 1, endLineNumber: finalEndLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed { shouldRealignDiffAreas: true } ) @@ -1685,6 +1721,7 @@ class EditCodeService extends Disposable implements IEditCodeService { } // write the added text to the file + if (!latestStreamLocationMutable) continue const deltaFinalText = block.final.substring((oldBlocks[blockNum]?.final ?? '').length, Infinity) this._writeStreamedDiffZoneLLMText(uri, block.orig, block.final, deltaFinalText, latestStreamLocationMutable) oldBlocks = blocks // oldblocks is only used if writingFinal @@ -1726,14 +1763,13 @@ class EditCodeService extends Disposable implements IEditCodeService { ...lines.slice((originalEnd - 1) + 1, Infinity) ].join('\n') } - const numLines = this._getNumLines(uri) - if (numLines !== null) { - this._writeText(uri, newCode, - { startLineNumber: 1, startColumn: 1, endLineNumber: numLines, endColumn: Number.MAX_SAFE_INTEGER }, - { shouldRealignDiffAreas: true } - ) - } + this._writeURIText(uri, newCode, + 'wholeFileRange', + { shouldRealignDiffAreas: true } + ) + + this._voidFileService.saveOrWriteFileAssumingModelExists(uri) onDone() resMessageDonePromise() }, @@ -1816,7 +1852,7 @@ class EditCodeService extends Disposable implements IEditCodeService { getURIStreamState = ({ uri }: { uri: URI | null }) => { if (uri === null) return 'idle' - const diffZones = [...this.diffAreasOfURI[uri.fsPath].values()] + const diffZones = [...this.diffAreasOfURI[uri.fsPath]?.values() ?? []] .map(diffareaid => this.diffAreaOfId[diffareaid]) .filter(diffArea => !!diffArea && diffArea.type === 'DiffZone') const isStreaming = diffZones.find(diffZone => !!diffZone._streamState.isStreaming) @@ -1853,7 +1889,7 @@ class EditCodeService extends Disposable implements IEditCodeService { const writeText = diffZone.originalCode const toRange: IRange = { startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER } - this._writeText(uri, writeText, toRange, { shouldRealignDiffAreas: true }) + this._writeURIText(uri, writeText, toRange, { shouldRealignDiffAreas: true }) this._deleteDiffZone(diffZone) } @@ -1863,11 +1899,11 @@ class EditCodeService extends Disposable implements IEditCodeService { public removeDiffAreas({ uri, removeCtrlKs, behavior }: { uri: URI, removeCtrlKs: boolean, behavior: 'reject' | 'accept' }) { const diffareaids = this.diffAreasOfURI[uri.fsPath] - if (diffareaids.size === 0) return // do nothing + if ((diffareaids?.size ?? 0) === 0) return // do nothing const { onFinishEdit } = this._addToHistory(uri) - for (const diffareaid of diffareaids) { + for (const diffareaid of diffareaids ?? []) { const diffArea = this.diffAreaOfId[diffareaid] if (!diffArea) continue @@ -2024,7 +2060,7 @@ class EditCodeService extends Disposable implements IEditCodeService { } // update the file - this._writeText(uri, writeText, toRange, { shouldRealignDiffAreas: true }) + this._writeURIText(uri, writeText, toRange, { shouldRealignDiffAreas: true }) // originalCode does not change! diff --git a/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts b/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts index 0ffbf047..99e0c937 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts @@ -20,6 +20,7 @@ export type StartApplyingOpts = ({ type: 'searchReplace' | 'rewrite'; applyStr: string; uri: 'current' | URI; + startBehavior: 'accept-conflicts' | 'reject-conflicts'; }) @@ -37,7 +38,7 @@ export const IEditCodeService = createDecorator('editCodeServi export interface IEditCodeService { readonly _serviceBrand: undefined; - startApplying(opts: StartApplyingOpts): [URI, Promise] | null; + startApplying(opts: StartApplyingOpts): Promise<[URI, Promise] | null>; addCtrlKZone(opts: AddCtrlKOpts): number | undefined; removeCtrlKZone(opts: { diffareaid: number }): void; diff --git a/src/vs/workbench/contrib/void/browser/helpers/readFile.ts b/src/vs/workbench/contrib/void/browser/helpers/readFile.ts deleted file mode 100644 index f7afa5dd..00000000 --- a/src/vs/workbench/contrib/void/browser/helpers/readFile.ts +++ /dev/null @@ -1,52 +0,0 @@ -/*-------------------------------------------------------------------------------------- - * Copyright 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. - *--------------------------------------------------------------------------------------*/ - -import { URI } from '../../../../../base/common/uri.js' -import { EndOfLinePreference } from '../../../../../editor/common/model.js' -import { IModelService } from '../../../../../editor/common/services/model.js' -import { IFileService } from '../../../../../platform/files/common/files.js' - - -// attempts to read URI of currently opened model, then of raw file -export const VSReadFile = async (uri: URI, modelService: IModelService, fileService: IFileService) => { - - const modelResult = await _VSReadModel(modelService, uri) - if (modelResult) return modelResult - - const fileResult = await _VSReadFileRaw(fileService, uri) - if (fileResult) return fileResult - - return '' - -} - -// read files from VSCode. preferred (but appears to only work if the model of this URI already exists. If it doesn't use the other function.) -const _VSReadModel = async (modelService: IModelService, uri: URI): Promise => { - - // attempt to read saved model (doesn't work if application was reloaded...) - const model = modelService.getModel(uri) - if (model) { - return model.getValue(EndOfLinePreference.LF) - } - - // backup logic - look at all opened models and check if they have the same `fsPath` - const models = modelService.getModels() - for (const model of models) { - if (model.uri.fsPath === uri.fsPath) - return model.getValue(EndOfLinePreference.LF); - } - - return null -} - -const _VSReadFileRaw = async (fileService: IFileService, uri: URI) => { - try { - const res = await fileService.readFile(uri) - const str = res.value.toString() - return str - } catch (e) { - return null - } -} 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 d44dfcf7..a0e0ec4d 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 @@ -118,16 +118,18 @@ export const useApplyButtonHTML = ({ codeStr, applyBoxId }: { codeStr: string, a }, [applyBoxId, editCodeService, getUriBeingApplied]) ) - const onSubmit = useCallback(() => { + const onClickSubmit = useCallback(async () => { if (isDisabled) return if (getStreamState() === 'streaming') return - const [newApplyingUri, _] = editCodeService.startApplying({ + const [newApplyingUri, _] = await editCodeService.startApplying({ from: 'ClickApply', type: 'searchReplace', applyStr: codeStr, uri: 'current', + startBehavior: 'reject-conflicts', }) ?? [] applyingURIOfApplyBoxIdRef.current[applyBoxId] = newApplyingUri ?? undefined + rerender(c => c + 1) metricsService.capture('Apply Code', { length: codeStr.length }) // capture the length only }, [isDisabled, getStreamState, editCodeService, codeStr, applyBoxId, metricsService]) @@ -154,8 +156,8 @@ export const useApplyButtonHTML = ({ codeStr, applyBoxId }: { codeStr: string, a const onReapply = useCallback(() => { onReject() - onSubmit() - }, [onReject, onSubmit]) + onClickSubmit() + }, [onReject, onClickSubmit]) const currStreamState = getStreamState() @@ -166,7 +168,7 @@ export const useApplyButtonHTML = ({ codeStr, applyBoxId }: { codeStr: string, a const playButton = ( ) diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index baa0620e..4eed66fe 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -315,9 +315,7 @@ const RenderToken = ({ token, nested, chatMessageLocation, tokenIdx, ...options // inline code if (t.type === "codespan") { - console.log('isLinkDetectionEnabled', options.isLinkDetectionEnabled) if (options.isLinkDetectionEnabled && chatMessageLocation) { - return -
    - {children || '(no results)'} -
    + {children || '(no results)'}
    }
    @@ -1382,8 +1381,14 @@ const toolNameToComponent: { [T in ToolName]: { const componentParams: ToolHeaderParams = { title, desc1, isError, icon, } const { params } = toolRequest - componentParams.children = - componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } + componentParams.children =
    +
    { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }}> + {getBasename(params.uri.fsPath)} +
    + +
    return }, @@ -1452,8 +1457,7 @@ const toolNameToComponent: { [T in ToolName]: { const { command } = toolMessage.result.params const { terminalId, resolveReason, result } = toolMessage.result.value - componentParams.children =
    - + componentParams.children =
    terminalToolsService.openTerminal(terminalId)} 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 f4042a2e..bf752bc2 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 @@ -883,8 +883,8 @@ export const VoidCodeEditor = ({ initValue, language, maxHeight, showScrollbars onCreateInstance={useCallback((editor: CodeEditorWidget) => { const model = modelOfEditorId[id] ?? modelService.createModel( - initValueRef.current + '\n', { - languageId: languageRef.current ? languageRef.current : 'typescript', + initValueRef.current, { + languageId: languageRef.current ? languageRef.current : 'plaintext', onDidChange: (e) => { return { dispose: () => { } } } // no idea why they'd require this }) modelRef.current = model diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index cf4b4c50..fb061bc1 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -272,7 +272,7 @@ export class ToolsService implements IToolsService { this.callTool = { read_file: async ({ uri, pageNumber }) => { const readFileContents = await voidFileService.readFile(uri) - + if (readFileContents === null) { throw new Error(`Contents were empty. There may have been an error, or the file may not exist.`) } const fromIdx = MAX_FILE_CHARS_PAGE * (pageNumber - 1) const toIdx = MAX_FILE_CHARS_PAGE * pageNumber - 1 const fileContents = readFileContents.slice(fromIdx, toIdx + 1) // paginate @@ -326,11 +326,12 @@ export class ToolsService implements IToolsService { }, edit: async ({ uri, changeDescription }) => { - const [_, applyDonePromise] = editCodeService.startApplying({ + const [_, applyDonePromise] = await editCodeService.startApplying({ // throws error if error uri, applyStr: changeDescription, from: 'ClickApply', type: 'searchReplace', + startBehavior: 'accept-conflicts', }) ?? [] await applyDonePromise return {} diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index c12752b1..2d956379 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -25,7 +25,7 @@ Do NOT output the whole file here if possible, and try to write as LITTLE as nee export const chat_systemMessage = (workspaces: string[], runningTerminalIds: string[], mode: 'agent' | 'gather' | 'chat') => `\ You are an expert coding ${mode === 'agent' ? 'agent' : 'assistant'} created by Void. Your job is to help the user ${mode === 'agent' ? 'develop, run, and make changes to their project' : 'search and understand their codebase'}. You will be given instructions to follow from the user, \`INSTRUCTIONS\`. You may also be given a list of files that the user has specifically selected, \`SELECTIONS\`. -Please assist the user with their query${mode === 'agent' ? `, bringing the task to completion (do not be lazy)` : ''}. The user's query is never invalid. +Please assist the user with their query${mode === 'agent' ? `, bringing the task to completion (make all necessary changes, and do not be lazy)` : ''}. The user's query is never invalid. The user's system information is as follows: - ${os} diff --git a/src/vs/workbench/contrib/void/common/voidFileService.ts b/src/vs/workbench/contrib/void/common/voidFileService.ts index cebad454..c276d3c1 100644 --- a/src/vs/workbench/contrib/void/common/voidFileService.ts +++ b/src/vs/workbench/contrib/void/common/voidFileService.ts @@ -3,18 +3,22 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ +import { VSBuffer } from '../../../../base/common/buffer.js'; import { URI } from '../../../../base/common/uri.js'; import { EndOfLinePreference } from '../../../../editor/common/model.js'; import { IModelService } from '../../../../editor/common/services/model.js'; import { IFileService } from '../../../../platform/files/common/files.js'; import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; +import { IEditorService } from '../../../services/editor/common/editorService.js'; export interface IVoidFileService { readonly _serviceBrand: undefined; - readFile(uri: URI, range?: { startLineNumber: number, endLineNumber: number }): Promise; + readFile(uri: URI, range?: { startLineNumber: number, endLineNumber: number }): Promise; readModel(uri: URI, range?: { startLineNumber: number, endLineNumber: number }): string | null; + + saveOrWriteFileAssumingModelExists(uri: URI): Promise; } export const IVoidFileService = createDecorator('VoidFileService'); @@ -26,11 +30,12 @@ export class VoidFileService implements IVoidFileService { constructor( @IModelService private readonly modelService: IModelService, @IFileService private readonly fileService: IFileService, + @IEditorService private readonly _editorService: IEditorService, ) { } - readFile = async (uri: URI, range?: { startLineNumber: number, endLineNumber: number }): Promise => { + readFile = async (uri: URI, range?: { startLineNumber: number, endLineNumber: number }): Promise => { // attempt to read the model const modelResult = this.readModel(uri, range); @@ -40,7 +45,7 @@ export class VoidFileService implements IVoidFileService { const fileResult = await this._readFileRaw(uri, range); if (fileResult) return fileResult; - return ''; + return null; } _readFileRaw = async (uri: URI, range?: { startLineNumber: number, endLineNumber: number }): Promise => { @@ -77,18 +82,33 @@ export class VoidFileService implements IVoidFileService { // if range, read it if (range) { - return model.getValueInRange({ - startLineNumber: range.startLineNumber, - endLineNumber: range.endLineNumber, - startColumn: 1, - endColumn: Number.MAX_VALUE - }, EndOfLinePreference.LF); + return model.getValueInRange({ startLineNumber: range.startLineNumber, endLineNumber: range.endLineNumber, startColumn: 1, endColumn: Number.MAX_VALUE }, EndOfLinePreference.LF); } else { return model.getValue(EndOfLinePreference.LF) } } + + + saveOrWriteFileAssumingModelExists = async (uri: URI): Promise => { + + const editorsOpen = [...this._editorService.findEditors(uri)] + if (editorsOpen.length !== 0) { + this._editorService.save(editorsOpen) + } + else { + // write the file using the contents of the existing model + const fileStr = this.modelService.getModel(uri)?.getValue() + if (fileStr === undefined) { + console.error('model not found for uri', uri.fsPath) + return + } + const buffer = VSBuffer.fromString(fileStr) + await this.fileService.writeFile(uri, buffer); + } + } + } registerSingleton(IVoidFileService, VoidFileService, InstantiationType.Eager); From def78affb4704a5b778031f20dc5715092086503 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 13 Mar 2025 21:05:44 -0700 Subject: [PATCH 056/173] ensureModelExists (forceModel) and fix quick edit state --- .../contrib/void/browser/editCodeService.ts | 69 ++++++++++++------- .../void/browser/editCodeServiceInterface.ts | 3 +- .../src/quick-edit-tsx/QuickEditChat.tsx | 1 - .../react/src/sidebar-tsx/SidebarChat.tsx | 3 +- .../react/src/util/mountFnGenerator.tsx | 1 - .../void/browser/react/src/util/services.tsx | 8 --- 6 files changed, 46 insertions(+), 39 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index f8e47af9..242ba070 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -3,7 +3,7 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import { Disposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js'; import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { ICodeEditor, IOverlayWidget, IViewZone, OverlayWidgetPositionPreference } from '../../../../editor/browser/editorBrowser.js'; @@ -331,7 +331,7 @@ class EditCodeService extends Disposable implements IEditCodeService { } // initialize all existing models + initialize when a new model mounts for (let model of this._modelService.getModels()) { initializeModel(model) } - this._register(this._modelService.onModelAdded(model => { console.log('initialized model!!', model.uri.fsPath); initializeModel(model) })); + this._register(this._modelService.onModelAdded(model => { initializeModel(model) })); // this function adds listeners to refresh styles when editor changes tab @@ -517,8 +517,9 @@ class EditCodeService extends Disposable implements IEditCodeService { }) // mount react + let disposablesRef: IDisposable[] | undefined = undefined this._instantiationService.invokeFunction(accessor => { - mountCtrlK(domNode, accessor, { + disposablesRef = mountCtrlK(domNode, accessor, { diffareaid: ctrlKZone.diffareaid, @@ -547,10 +548,11 @@ class EditCodeService extends Disposable implements IEditCodeService { }) - return () => editor.changeViewZones(accessor => { - if (zoneId) - accessor.removeZone(zoneId) - }) + // cleanup + return () => { + editor.changeViewZones(accessor => { if (zoneId) accessor.removeZone(zoneId) }) + disposablesRef?.forEach(d => d.dispose()) + } }) return { @@ -746,9 +748,9 @@ class EditCodeService extends Disposable implements IEditCodeService { } return model } - // not obvious at all, but if we want the model we can just create it. our listeners in the constructor handle it - private async _forceModel(uri: URI) { + // not obvious at all, but if we want the model we can just create it. our listeners in the constructor handle it. call this the moment we know we have a uri that we want to make changes to + private async _ensureModelExists(uri: URI) { const m = this._getModel(uri) if (m !== null) return m @@ -770,10 +772,8 @@ class EditCodeService extends Disposable implements IEditCodeService { weAreWriting = false private async _writeURIText(uri: URI, text: string, range_: IRange | 'wholeFileRange', { shouldRealignDiffAreas, }: { shouldRealignDiffAreas: boolean, }) { - let m = this._getModel(uri) - if (m === null) { m = await this._forceModel(uri) } - if (m === null) return // if still null, return - const model: ITextModel = m + const model = this._getModel(uri) + if (model === null) return const range: IRange = range_ === 'wholeFileRange' ? { startLineNumber: 1, startColumn: 1, endLineNumber: model.getLineCount(), endColumn: Number.MAX_SAFE_INTEGER } // whole file @@ -890,7 +890,7 @@ class EditCodeService extends Disposable implements IEditCodeService { } this._undoRedoService.pushElement(elt) - const onFinishEdit = async () => { afterSnapshot = getCurrentSnapshot() } + const onFinishEdit = () => { afterSnapshot = getCurrentSnapshot() } return { onFinishEdit } } @@ -1160,9 +1160,13 @@ class EditCodeService extends Disposable implements IEditCodeService { // called first, then call startApplying public addCtrlKZone({ startLine, endLine, editor }: AddCtrlKOpts) { + // don't need to await this, because in order to add a ctrl+K zone must already have the model open on your screen + // await this._ensureModelExists(uri) + const uri = editor.getModel()?.uri if (!uri) return + // check if there's overlap with any other ctrlKZone and if so, focus it const overlappingCtrlKZone = this._findOverlappingDiffArea({ startLine, endLine, uri, filter: (diffArea) => diffArea.type === 'CtrlKZone' }) if (overlappingCtrlKZone) { @@ -1219,18 +1223,18 @@ class EditCodeService extends Disposable implements IEditCodeService { // the applyDonePromise this returns can throw an error (reject) public async startApplying(opts: StartApplyingOpts): Promise<[URI, Promise] | null> { let res: [DiffZone, Promise] | undefined = undefined - if (opts.type === 'rewrite') res = await this._initializeWriteoverStream(opts) - else if (opts.type === 'searchReplace') res = await this._initializeSearchAndReplaceStream(opts) + + if (Math.random() > 0) { + console.log('writeover....') + res = await this._initializeWriteoverStream(opts) + } else { + + if (opts.type === 'rewrite') res = await this._initializeWriteoverStream(opts) + else if (opts.type === 'searchReplace') res = await this._initializeSearchAndReplaceStream(opts) + } if (!res) return null const [diffZone, applyDonePromise] = res - applyDonePromise.then(() => { - const diffareaids = this.diffAreasOfURI[diffZone._URI.fsPath] - for (const diffareaid of diffareaids || []) { - const diffArea = this.diffAreaOfId[diffareaid] - console.log('DA!!', diffArea) - } - }) return [diffZone._URI, applyDonePromise] } @@ -1270,9 +1274,12 @@ class EditCodeService extends Disposable implements IEditCodeService { const uri_ = this._getActiveEditorURI() if (!uri_) return uri = uri_ + await this._ensureModelExists(uri) + const c_ = await this._voidFileService.readFile(uri) if (c_ === null) return currentFileStr = c_ + console.log('got curent file', c_.length) const numLines = numLinesOfStr(currentFileStr) @@ -1291,6 +1298,7 @@ class EditCodeService extends Disposable implements IEditCodeService { const { startLine: startLine_, endLine: endLine_, _URI } = ctrlKZone uri = _URI + await this._ensureModelExists(uri) const c_ = await this._voidFileService.readFile(uri) if (c_ === null) return @@ -1317,7 +1325,7 @@ class EditCodeService extends Disposable implements IEditCodeService { onUndo: () => { if (diffZone._streamState.isStreaming) rejApplyPromise(new Error('Edit was interrupted by pressing undo.')) } }) - // __TODO__ let users customize modelFimTags + // TODO!!! let users customize modelFimTags const quickEditFIMTags = defaultQuickEditFimTags const adding: Omit = { @@ -1377,6 +1385,7 @@ class EditCodeService extends Disposable implements IEditCodeService { const onDone = () => { + console.log('called onDone') diffZone._streamState = { isStreaming: false, } this._onDidChangeDiffZoneStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) @@ -1463,6 +1472,7 @@ class EditCodeService extends Disposable implements IEditCodeService { }) await messageDonePromise + console.log('done waiting') } writeover().then(() => resApplyPromise()).catch((e) => rejApplyPromise(e)) @@ -1485,6 +1495,8 @@ class EditCodeService extends Disposable implements IEditCodeService { else { uri = givenURI } + await this._ensureModelExists(uri) + // generate search/replace block text const originalFileCode = await this._voidFileService.readFile(uri) @@ -1544,8 +1556,6 @@ class EditCodeService extends Disposable implements IEditCodeService { this._onDidChangeDiffZoneStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) this._onDidAddOrDeleteDiffZones.fire({ uri }) - console.log('b', uri) - const convertOriginalRangeToFinalRange = (originalRange: readonly [number, number]): [number, number] => { // adjust based on the changes by computing line offset @@ -1614,6 +1624,13 @@ class EditCodeService extends Disposable implements IEditCodeService { shouldSendAnotherMessage = false nMessagesSent += 1 + if (nMessagesSent >= 5) { + this._notifyError({ message: 'Void Error: Tried to Fast Apply 5 times but failed. Please try again with a smarter model or disable Fast Apply.', fullError: null }) + onDone() + this._undoHistory(uri) + break + } + let resMessageDonePromise: () => void = () => { } const messageDonePromise = new Promise((res, rej) => { resMessageDonePromise = res }) diff --git a/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts b/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts index 99e0c937..bdcdc05e 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts @@ -38,9 +38,10 @@ export const IEditCodeService = createDecorator('editCodeServi export interface IEditCodeService { readonly _serviceBrand: undefined; + // main entrypoints (initialize things for the functions below to be called): startApplying(opts: StartApplyingOpts): Promise<[URI, Promise] | null>; - addCtrlKZone(opts: AddCtrlKOpts): number | undefined; + removeCtrlKZone(opts: { diffareaid: number }): void; removeDiffAreas(opts: { uri: URI, removeCtrlKs: boolean, behavior: 'reject' | 'accept' }): void; diff --git a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx index c7ecf1ba..9fb95451 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx @@ -8,7 +8,6 @@ import { useSettingsState, useSidebarState, useChatThreadsState, useQuickEditSta import { TextAreaFns, VoidInputBox2 } from '../util/inputs.js'; import { QuickEditPropsType } from '../../../quickEditActions.js'; import { ButtonStop, ButtonSubmit, IconX, VoidChatArea } from '../sidebar-tsx/SidebarChat.js'; -import { ModelDropdown } from '../void-settings-tsx/ModelDropdown.js'; import { VOID_CTRL_K_ACTION_ID } from '../../../actionIDs.js'; import { useRefState } from '../util/helpers.js'; import { useScrollbarStyles } from '../util/useScrollbarStyles.js'; 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 57357e57..d65e15d6 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 @@ -236,7 +236,7 @@ const ReasoningOptionDropdown = () => { const value = voidSettingsState.optionsOfModelSelection[modelSelection.providerName]?.[modelSelection.modelName]?.reasoningBudget ?? defaultVal const nSteps = 8 // only used in calculating stepSize, stepSize is what actually matters - const stepSize = Math.round((max - min_) / 8) + const stepSize = Math.round((max - min_) / nSteps) const min = canToggleReasoning ? min_ - stepSize : min_ return
    @@ -249,7 +249,6 @@ const ReasoningOptionDropdown = () => { step={stepSize} value={value} onChange={(newVal) => { - console.log('NEWVAL', newVal) const disabled = newVal === min && canToggleReasoning voidSettingsService.setOptionsOfModelSelection(modelSelection.providerName, modelSelection.modelName, { reasoningEnabled: !disabled, reasoningBudget: newVal }) }} diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/mountFnGenerator.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/mountFnGenerator.tsx index 9839201d..5eba467d 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/mountFnGenerator.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/mountFnGenerator.tsx @@ -18,7 +18,6 @@ export const mountFnGenerator = (Component: (params: any) => React.ReactNode) => const disposables = _registerServices(accessor) - const root = ReactDOM.createRoot(rootElement) root.render(); // tailwind dark theme indicator 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 dd9c0756..4da31c05 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 @@ -85,18 +85,10 @@ const uriStreamingStateListeners: Set<(uri: URI, s: URIStreamState) => void> = n // must call this before you can use any of the hooks below // this should only be called ONCE! this is the only place you don't need to dispose onDidChange. If you use state.onDidChange anywhere else, make sure to dispose it! -let wasCalled = false export const _registerServices = (accessor: ServicesAccessor) => { const disposables: IDisposable[] = [] - // don't register services twice - if (wasCalled) { - return - // console.error(`⚠️ Void _registerServices was called again! It should only be called once.`) - } - wasCalled = true - _registerAccessor(accessor) const stateServices = { From c222a801a8ed201c47fbd54609dfd42d47da90d7 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 13 Mar 2025 21:29:45 -0700 Subject: [PATCH 057/173] fix CtrlK model selection --- .../contrib/void/browser/editCodeService.ts | 7 ++++++- .../react/src/quick-edit-tsx/QuickEditChat.tsx | 1 + .../browser/react/src/sidebar-tsx/SidebarChat.tsx | 13 +++++++++---- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 242ba070..4fed6693 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -771,7 +771,7 @@ class EditCodeService extends Disposable implements IEditCodeService { } weAreWriting = false - private async _writeURIText(uri: URI, text: string, range_: IRange | 'wholeFileRange', { shouldRealignDiffAreas, }: { shouldRealignDiffAreas: boolean, }) { + private _writeURIText(uri: URI, text: string, range_: IRange | 'wholeFileRange', { shouldRealignDiffAreas, }: { shouldRealignDiffAreas: boolean, }) { const model = this._getModel(uri) if (model === null) return @@ -1470,6 +1470,8 @@ class EditCodeService extends Disposable implements IEditCodeService { resMessageDonePromise() }, }) + // should never happen, just for safety + if (streamRequestIdRef.current === null) { return } await messageDonePromise console.log('done waiting') @@ -1799,6 +1801,9 @@ class EditCodeService extends Disposable implements IEditCodeService { }) + // should never happen, just for safety + if (streamRequestIdRef.current === null) { break } + await messageDonePromise } // end while diff --git a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx index 9fb95451..e42755c1 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx @@ -86,6 +86,7 @@ export const QuickEditChat = ({ const chatAreaRef = useRef(null) return
    `${threadId}-$ // SLIDER ONLY: -const ReasoningOptionDropdown = () => { +const ReasoningOptionDropdown = ({ featureName }: { featureName: FeatureName }) => { const accessor = useAccessor() const voidSettingsService = accessor.get('IVoidSettingsService') const voidSettingsState = useSettingsState() - const modelSelection = voidSettingsState.modelSelectionOfFeature['Chat'] + const modelSelection = voidSettingsState.modelSelectionOfFeature[featureName] if (!modelSelection) return null const { modelName, providerName } = modelSelection @@ -289,6 +289,8 @@ interface VoidChatAreaProps { onClickAnywhere?: () => void; // Optional close button onClose?: () => void; + + featureName: FeatureName; } export const VoidChatArea: React.FC = ({ @@ -306,6 +308,7 @@ export const VoidChatArea: React.FC = ({ showProspectiveSelections = true, selections, setSelections, + featureName, }) => { return (
    = ({
    {showModelDropdown && (
    - - + +
    )} @@ -852,6 +855,7 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isLoading }: ChatBubble } chatbubbleContents = { }, [onSubmit, onAbort, isStreaming]) const inputForm =
    0 ? 'absolute bottom-0' : ''}`}> Date: Fri, 14 Mar 2025 00:20:03 -0700 Subject: [PATCH 058/173] language detection --- .../contrib/void/browser/chatThreadService.ts | 6 +- .../contrib/void/browser/editCodeService.ts | 12 +- .../react/src/markdown/ChatMarkdownRender.tsx | 48 ++-- .../react/src/sidebar-tsx/SidebarChat.tsx | 6 +- .../void/browser/react/src/util/inputs.tsx | 4 +- .../void/browser/react/src/util/services.tsx | 4 +- .../contrib/void/browser/sidebarActions.ts | 2 + .../void/common/chatThreadServiceTypes.ts | 2 + .../void/common/helpers/detectLanguage.ts | 174 ------------- .../void/common/helpers/getLanguage.ts | 229 ++++++++++++++++++ .../contrib/void/common/prompt/prompts.ts | 99 ++++---- .../llmMessage/preprocessLLMMessages.ts | 2 + .../llmMessage/sendLLMMessage.impl.ts | 8 +- 13 files changed, 348 insertions(+), 248 deletions(-) delete mode 100644 src/vs/workbench/contrib/void/common/helpers/detectLanguage.ts create mode 100644 src/vs/workbench/contrib/void/common/helpers/getLanguage.ts diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 04cef415..6cbff796 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -204,7 +204,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { @IVoidSettingsService private readonly _settingsService: IVoidSettingsService, @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, @ITextModelService private readonly _textModelService: ITextModelService, - @ITerminalToolService private readonly terminalToolService: ITerminalToolService, + @ITerminalToolService private readonly _terminalToolService: ITerminalToolService, ) { super() this.state = { allThreads: {}, currentThreadId: null as unknown as string } // default state @@ -226,7 +226,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { private _convertThreadDataFromStorage(threadsStr: string): ChatThreads { return JSON.parse(threadsStr, (key, value) => { if (value && typeof value === 'object' && value.$mid === 1) { // $mid is the MarshalledId. $mid === 1 means it is a URI - return URI.from(value); + return URI.from(value); // TODO URI.revive instead of this? } return value; }); @@ -400,7 +400,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { // system message const workspaceFolders = this._workspaceContextService.getWorkspace().folders.map(f => f.uri.fsPath) - const terminalIds = this.terminalToolService.listTerminalIds() + const terminalIds = this._terminalToolService.listTerminalIds() const systemMessage = chat_systemMessage(workspaceFolders, terminalIds, chatMode) // all messages so far in the chat history (including tools) diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 4fed6693..c4b23325 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -31,7 +31,6 @@ import { mountCtrlK } from './react/out/quick-edit-tsx/index.js' import { QuickEditPropsType } from './quickEditActions.js'; import { IModelContentChangedEvent } from '../../../../editor/common/textModelEvents.js'; import { extractCodeFromFIM, extractCodeFromRegular, ExtractedSearchReplaceBlock, extractSearchReplaceBlocks } from '../common/helpers/extractCodeFromResult.js'; -import { filenameToVscodeLanguage } from '../common/helpers/detectLanguage.js'; import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; import { isMacintosh } from '../../../../base/common/platform.js'; import { EditorOption } from '../../../../editor/common/config/editorOptions.js'; @@ -46,6 +45,7 @@ import { IEditCodeService, URIStreamState, AddCtrlKOpts, StartApplyingOpts } fro import { IVoidSettingsService } from '../common/voidSettingsService.js'; import { FeatureName } from '../common/voidSettingsTypes.js'; import { ILanguageService } from '../../../../editor/common/languages/language.js'; +import { getFullLanguage, getLanguageFromModel } from '../common/helpers/getLanguage.js'; // import { IFileService } from '../../../../platform/files/common/files.js'; // import { VSBuffer } from '../../../../base/common/buffer.js'; @@ -756,7 +756,7 @@ class EditCodeService extends Disposable implements IEditCodeService { const fileStr = await this._voidFileService.readFile(uri) if (fileStr === null) return null - const lang = this._languageService.createByFilepathOrFirstLine(uri, fileStr?.split('\n')?.[0]) + const lang = getFullLanguage(this._languageService, { uri, fileContents: fileStr }) const model = this._modelService.createModel(fileStr, lang, uri); return model } @@ -1279,7 +1279,6 @@ class EditCodeService extends Disposable implements IEditCodeService { const c_ = await this._voidFileService.readFile(uri) if (c_ === null) return currentFileStr = c_ - console.log('got curent file', c_.length) const numLines = numLinesOfStr(currentFileStr) @@ -1312,6 +1311,8 @@ class EditCodeService extends Disposable implements IEditCodeService { } const originalCode = currentFileStr.split('\n').slice((startLine - 1), (endLine - 1) + 1).join('\n') + const language = getLanguageFromModel(uri, this._modelService) + let streamRequestIdRef: { current: string | null } = { current: null } // promise that resolves when the apply is done @@ -1346,6 +1347,8 @@ class EditCodeService extends Disposable implements IEditCodeService { this._onDidChangeDiffZoneStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) this._onDidAddOrDeleteDiffZones.fire({ uri }) + + if (from === 'QuickEdit') { const { diffareaid } = opts const ctrlKZone = this.diffAreaOfId[diffareaid] @@ -1359,7 +1362,7 @@ class EditCodeService extends Disposable implements IEditCodeService { let messages: LLMChatMessage[] if (from === 'ClickApply') { - const userContent = rewriteCode_userMessage({ originalCode, applyStr: opts.applyStr, uri }) + const userContent = rewriteCode_userMessage({ originalCode, applyStr: opts.applyStr, language }) messages = [ { role: 'system', content: rewriteCode_systemMessage, }, { role: 'user', content: userContent, } @@ -1373,7 +1376,6 @@ class EditCodeService extends Disposable implements IEditCodeService { const instructions = _mountInfo?.textAreaRef.current?.value ?? '' const { prefix, suffix } = voidPrefixAndSuffix({ fullFileStr: currentFileStr, startLine, endLine }) - const language = filenameToVscodeLanguage(uri.fsPath) ?? '' const userContent = ctrlKStream_userMessage({ selection: originalCode, instructions: instructions, prefix, suffix, isOllamaFIM: false, fimTags: quickEditFIMTags, language }) // type: 'messages', messages = [ diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index 4eed66fe..521b552d 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -6,12 +6,13 @@ import React, { JSX, useState } from 'react' import { marked, MarkedToken, Token } from 'marked' import { BlockCode, BlockCodeWithApply } from './BlockCode.js' -import { nameToVscodeLanguage } from '../../../../common/helpers/detectLanguage.js' +import { convertToVscodeLang, getFirstLine, getLanguage } from '../../../../common/helpers/getLanguage.js' import { useApplyButtonHTML } from './ApplyBlockHoverButtons.js' import { useAccessor, useChatThreadsState } from '../util/services.js' import { Range } from '../../../../../../services/search/common/searchExtTypes.js' import { IRange } from '../../../../../../../base/common/range.js' import { ScrollType } from '../../../../../../../editor/common/editorCommon.js' +import { URI } from '../../../../../../../base/common/uri.js' export type ChatMessageLocation = { @@ -101,7 +102,9 @@ const CodespanWithLink = ({ text, rawText, chatMessageLocation }: { text: string export type RenderTokenOptions = { isApplyEnabled?: boolean, isLinkDetectionEnabled?: boolean } -const RenderToken = ({ token, nested, chatMessageLocation, tokenIdx, ...options }: { token: Token | string, nested?: boolean, chatMessageLocation?: ChatMessageLocation, tokenIdx: string, } & RenderTokenOptions): JSX.Element => { +const RenderToken = ({ token, inPTag, chatMessageLocation, tokenIdx, ...options }: { token: Token | string, inPTag?: boolean, chatMessageLocation?: ChatMessageLocation, tokenIdx: string, } & RenderTokenOptions): JSX.Element => { + const accessor = useAccessor() + const languageService = accessor.get('ILanguageService') // deal with built-in tokens first (assume marked token) const t = token as MarkedToken @@ -115,28 +118,41 @@ const RenderToken = ({ token, nested, chatMessageLocation, tokenIdx, ...options } if (t.type === "code") { + const [firstLine, remainingContents] = getFirstLine(t.text) + const firstLineIsURI = URI.isUri(firstLine) - const language = t.lang === undefined ? undefined : nameToVscodeLanguage[t.lang] + let language: string | undefined = undefined + if (t.lang !== undefined) { + // convert markdown language to language that vscode recognizes (eg markdown doesn't know bash but it does know shell) + language = convertToVscodeLang(languageService, t.lang) + } - // TODO user should only be able to apply this when the code has been closed (t.raw ends with "```") + else if (!language) { // if still no lang + if (firstLineIsURI) { // get lang from the uri + const uri = URI.file(firstLine) + language = getLanguage(languageService, { uri, fileContents: remainingContents ?? undefined }) + } + else { // get lang from the contents + language = getLanguage(languageService, { uri: null, fileContents: remainingContents ?? undefined }) + } + } + const contents = firstLineIsURI ? (remainingContents || '') : t.text // exclude first-line URI from contents + // TODO!!! user should only be able to apply this when the code has been closed (t.raw ends with "```") if (options.isApplyEnabled && chatMessageLocation) { - const applyBoxId = getApplyBoxId({ threadId: chatMessageLocation.threadId, messageIdx: chatMessageLocation.messageIdx, tokenIdx: tokenIdx, }) - return } - return } @@ -223,7 +239,7 @@ const RenderToken = ({ token, nested, chatMessageLocation, tokenIdx, ...options return
  • - +
  • } @@ -239,7 +255,7 @@ const RenderToken = ({ token, nested, chatMessageLocation, tokenIdx, ...options )} - + ))} @@ -252,14 +268,15 @@ const RenderToken = ({ token, nested, chatMessageLocation, tokenIdx, ...options {t.tokens.map((token, index) => ( ))} - if (nested) return contents + if (inPTag) return contents return

    {contents} @@ -343,12 +360,13 @@ const RenderToken = ({ token, nested, chatMessageLocation, tokenIdx, ...options ) } -export const ChatMarkdownRender = ({ string, nested = false, chatMessageLocation, ...options }: { string: string, nested?: boolean, chatMessageLocation: ChatMessageLocation | undefined } & RenderTokenOptions) => { + +export const ChatMarkdownRender = ({ string, inPTag = false, chatMessageLocation, ...options }: { string: string, inPTag?: boolean, chatMessageLocation: ChatMessageLocation | undefined } & RenderTokenOptions) => { const tokens = marked.lexer(string); // https://marked.js.org/using_pro#renderer return ( <> {tokens.map((token, index) => ( - + ))} ) 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 bcb9e5fd..18d3cead 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 @@ -25,11 +25,11 @@ import { VOID_CTRL_L_ACTION_ID } from '../../../actionIDs.js'; import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js'; import { FeatureName, isFeatureNameDisabled } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'; import { WarningBox } from '../void-settings-tsx/WarningBox.js'; -import { filenameToVscodeLanguage } from '../../../../common/helpers/detectLanguage.js'; import { getModelSelectionState, getModelCapabilities } from '../../../../common/modelCapabilities.js'; import { AlertTriangle, ChevronRight, Dot, Pencil, X } from 'lucide-react'; import { ChatMessage, StagingSelectionItem, ToolMessage, ToolRequestApproval } from '../../../../common/chatThreadServiceTypes.js'; import { ToolCallParams, ToolName, ToolNameWithApproval } from '../../../../common/toolsServiceTypes.js'; +import { getLanguageFromModel } from '../../../../common/helpers/getLanguage.js'; @@ -505,6 +505,7 @@ export const SelectedFiles = ( const accessor = useAccessor() const commandService = accessor.get('ICommandService') + const modelService = accessor.get('IModelService') // state for tracking prospective files const { currentUri } = useUriState() @@ -528,6 +529,7 @@ export const SelectedFiles = ( .map(uri => ({ type: 'File', fileURI: uri, + language: getLanguageFromModel(uri, modelService), selectionStr: null, range: null, state: { isOpened: false }, @@ -638,7 +640,7 @@ export const SelectedFiles = ( > 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 bf752bc2..069ed703 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 @@ -882,9 +882,11 @@ export const VoidCodeEditor = ({ initValue, language, maxHeight, showScrollbars }, [instantiationService])} onCreateInstance={useCallback((editor: CodeEditorWidget) => { + const languageId = languageRef.current ? languageRef.current : 'plaintext' + const model = modelOfEditorId[id] ?? modelService.createModel( initValueRef.current, { - languageId: languageRef.current ? languageRef.current : 'plaintext', + languageId: languageId, onDidChange: (e) => { return { dispose: () => { } } } // no idea why they'd require this }) modelRef.current = model 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 4da31c05..5e9f4d4a 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 @@ -46,6 +46,7 @@ import { IMetricsService } from '../../../../../../../workbench/contrib/void/com import { URI } from '../../../../../../../base/common/uri.js' import { IChatThreadService, ThreadsState, ThreadStreamState } from '../../../chatThreadService.js' import { ITerminalToolService } from '../../../terminalToolService.js' +import { ILanguageService } from '../../../../../../../editor/common/languages/language.js' @@ -225,7 +226,8 @@ const getReactAccessor = (accessor: ServicesAccessor) => { IConfigurationService: accessor.get(IConfigurationService), IPathService: accessor.get(IPathService), IMetricsService: accessor.get(IMetricsService), - ITerminalToolService: accessor.get(ITerminalToolService) + ITerminalToolService: accessor.get(ITerminalToolService), + ILanguageService: accessor.get(ILanguageService), } as const return reactAccessor diff --git a/src/vs/workbench/contrib/void/browser/sidebarActions.ts b/src/vs/workbench/contrib/void/browser/sidebarActions.ts index 799f7777..77993b57 100644 --- a/src/vs/workbench/contrib/void/browser/sidebarActions.ts +++ b/src/vs/workbench/contrib/void/browser/sidebarActions.ts @@ -123,12 +123,14 @@ registerAction2(class extends Action2 { const selection: StagingSelectionItem = !selectionRange || !selectionStr || (selectionRange.startLineNumber > selectionRange.endLineNumber) ? { type: 'File', fileURI: model.uri, + language: model.getLanguageId(), selectionStr: null, range: null, state: { isOpened: false, } } : { type: 'Selection', fileURI: model.uri, + language: model.getLanguageId(), selectionStr: selectionStr, range: selectionRange, state: { isOpened: true, } diff --git a/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts b/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts index 62051a00..893b4dc1 100644 --- a/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts @@ -50,6 +50,7 @@ export type ChatMessage = export type CodeSelection = { type: 'Selection'; fileURI: URI; + language: string; selectionStr: string; range: IRange; state: { @@ -60,6 +61,7 @@ export type CodeSelection = { export type FileSelection = { type: 'File'; fileURI: URI; + language: string; selectionStr: null; range: null; state: { diff --git a/src/vs/workbench/contrib/void/common/helpers/detectLanguage.ts b/src/vs/workbench/contrib/void/common/helpers/detectLanguage.ts deleted file mode 100644 index 9bc3c9bd..00000000 --- a/src/vs/workbench/contrib/void/common/helpers/detectLanguage.ts +++ /dev/null @@ -1,174 +0,0 @@ -/*-------------------------------------------------------------------------------------- - * Copyright 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. - *--------------------------------------------------------------------------------------*/ - -// eg "bash" -> "shell" -export const nameToVscodeLanguage: { [key: string]: string } = { - // Web Technologies - 'html': 'html', - 'css': 'css', - 'scss': 'scss', - 'sass': 'scss', - 'less': 'less', - 'javascript': 'typescript', - 'js': 'typescript', // use more general renderer - 'jsx': 'typescript', - 'typescript': 'typescript', - 'ts': 'typescript', - 'tsx': 'typescript', - 'json': 'json', - 'jsonc': 'json', - - // Programming Languages - 'python': 'python', - 'py': 'python', - 'java': 'java', - 'cpp': 'cpp', - 'c++': 'cpp', - 'c': 'c', - 'csharp': 'csharp', - 'cs': 'csharp', - 'c#': 'csharp', - 'go': 'go', - 'golang': 'go', - 'rust': 'rust', - 'rs': 'rust', - 'ruby': 'ruby', - 'rb': 'ruby', - 'php': 'php', - 'shell': 'shell', - 'bash': 'shell', - 'sh': 'shell', - 'zsh': 'shell', - - // Markup and Config - 'markdown': 'markdown', - 'md': 'markdown', - 'xml': 'xml', - 'svg': 'xml', - 'yaml': 'yaml', - 'yml': 'yaml', - 'ini': 'ini', - 'toml': 'ini', - - // Database and Query Languages - 'sql': 'sql', - 'mysql': 'sql', - 'postgresql': 'sql', - 'graphql': 'graphql', - 'gql': 'graphql', - - // Others - 'dockerfile': 'dockerfile', - 'docker': 'dockerfile', - 'makefile': 'makefile', - 'plaintext': 'plaintext', - 'text': 'plaintext' -}; - - - -// eg ".ts" -> "typescript" -const fileExtensionToVscodeLanguage: { [key: string]: string } = { - // Web - 'html': 'html', - 'htm': 'html', - 'css': 'css', - 'scss': 'scss', - 'less': 'less', - 'js': 'javascript', - 'jsx': 'javascript', - 'ts': 'typescript', - 'tsx': 'typescript', - 'json': 'json', - 'jsonc': 'json', - - // Programming Languages - 'py': 'python', - 'java': 'java', - 'cpp': 'cpp', - 'cc': 'cpp', - 'c': 'c', - 'h': 'cpp', - 'hpp': 'cpp', - 'cs': 'csharp', - 'go': 'go', - 'rs': 'rust', - 'rb': 'ruby', - 'php': 'php', - 'sh': 'shell', - 'bash': 'shell', - 'zsh': 'shell', - - // Markup/Config - 'md': 'markdown', - 'markdown': 'markdown', - 'xml': 'xml', - 'svg': 'xml', - 'yaml': 'yaml', - 'yml': 'yaml', - 'ini': 'ini', - 'toml': 'ini', - - // Other - 'sql': 'sql', - 'graphql': 'graphql', - 'gql': 'graphql', - 'dockerfile': 'dockerfile', - 'docker': 'dockerfile', - 'mk': 'makefile', - - // Config Files and Dot Files - 'npmrc': 'ini', - 'env': 'ini', - 'gitignore': 'ignore', - 'dockerignore': 'ignore', - 'eslintrc': 'json', - 'babelrc': 'json', - 'prettierrc': 'json', - 'stylelintrc': 'json', - 'editorconfig': 'ini', - 'htaccess': 'apacheconf', - 'conf': 'ini', - 'config': 'ini', - - // Package Files - 'package': 'json', - 'package-lock': 'json', - 'gemfile': 'ruby', - 'podfile': 'ruby', - 'rakefile': 'ruby', - - // Build Systems - 'cmake': 'cmake', - 'makefile': 'makefile', - 'gradle': 'groovy', - - // Shell Scripts - 'bashrc': 'shell', - 'zshrc': 'shell', - 'fish': 'shell', - - // Version Control - 'gitconfig': 'ini', - 'hgrc': 'ini', - 'svnconfig': 'ini', - - // Web Server - 'nginx': 'nginx', - - // Misc Config - 'properties': 'properties', - 'cfg': 'ini', - 'reg': 'ini' -}; - - -export function filenameToVscodeLanguage(filename: string): string | undefined { - - const ext = filename.toLowerCase().split('.').pop(); - if (!ext) return undefined; - - return fileExtensionToVscodeLanguage[ext]; -} diff --git a/src/vs/workbench/contrib/void/common/helpers/getLanguage.ts b/src/vs/workbench/contrib/void/common/helpers/getLanguage.ts new file mode 100644 index 00000000..da2c82d1 --- /dev/null +++ b/src/vs/workbench/contrib/void/common/helpers/getLanguage.ts @@ -0,0 +1,229 @@ +// /*-------------------------------------------------------------------------------------- +// * Copyright 2025 Glass Devtools, Inc. All rights reserved. +// * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. +// *--------------------------------------------------------------------------------------*/ + +import { URI } from '../../../../../base/common/uri.js'; +import { ILanguageService } from '../../../../../editor/common/languages/language.js'; +import { IModelService } from '../../../../../editor/common/services/model.js'; + +export const getFirstLine = (content: string): [string, string] | [string, undefined] => { + const newLineIdx = content.indexOf('\r\n') + if (newLineIdx !== -1) { + const A = content.substring(0, newLineIdx + 2) + const B = content.substring(newLineIdx + 2, Infinity); + return [A, B] + } + + const newLineIdx2 = content.indexOf('\n') + if (newLineIdx2 !== -1) { + const A = content.substring(0, newLineIdx2 + 1) + const B = content.substring(newLineIdx2 + 1, Infinity); + return [A, B] + } + + return [content, undefined] +} + + +export function getFullLanguage(languageService: ILanguageService, opts: { uri: URI | null, fileContents: string | undefined }) { + const firstLine = opts.fileContents ? getFirstLine(opts.fileContents)?.[0] : undefined + const fullLang = languageService.createByFilepathOrFirstLine(opts.uri, firstLine) + return fullLang +} + +export function getLanguage(languageService: ILanguageService, opts: { uri: URI | null, fileContents: string | undefined }): string { + return getFullLanguage(languageService, opts).languageId +} + +export function getLanguageFromModel(uri: URI, modelService: IModelService): string { + return modelService.getModel(uri)?.getLanguageId() || '' +} + + + +// conversions +const convertToVoidLanguage = (languageService: ILanguageService, language: string) => { + const { languageId } = languageService.createById(language) + return languageId +} + +export const convertToVscodeLang = (languageService: ILanguageService, markdownLang: string) => { + if (markdownLang in markdownLangToVscodeLang) + return markdownLangToVscodeLang[markdownLang] + return convertToVoidLanguage(languageService, markdownLang) +} + + + + +// // eg "bash" -> "shell" +const markdownLangToVscodeLang: { [key: string]: string } = { + // Web Technologies + 'html': 'html', + 'css': 'css', + 'scss': 'scss', + 'sass': 'scss', + 'less': 'less', + 'javascript': 'typescript', + 'js': 'typescript', // use more general renderer + 'jsx': 'typescript', + 'typescript': 'typescript', + 'ts': 'typescript', + 'tsx': 'typescript', + 'json': 'json', + 'jsonc': 'json', + + // Programming Languages + 'python': 'python', + 'py': 'python', + 'java': 'java', + 'cpp': 'cpp', + 'c++': 'cpp', + 'c': 'c', + 'csharp': 'csharp', + 'cs': 'csharp', + 'c#': 'csharp', + 'go': 'go', + 'golang': 'go', + 'rust': 'rust', + 'rs': 'rust', + 'ruby': 'ruby', + 'rb': 'ruby', + 'php': 'php', + 'shell': 'shellscript', // this is important + 'bash': 'shellscript', + 'sh': 'shellscript', + 'zsh': 'shellscript', + + // Markup and Config + 'markdown': 'markdown', + 'md': 'markdown', + 'xml': 'xml', + 'svg': 'xml', + 'yaml': 'yaml', + 'yml': 'yaml', + 'ini': 'ini', + 'toml': 'ini', + + // Database and Query Languages + 'sql': 'sql', + 'mysql': 'sql', + 'postgresql': 'sql', + 'graphql': 'graphql', + 'gql': 'graphql', + + // Others + 'dockerfile': 'dockerfile', + 'docker': 'dockerfile', + 'makefile': 'makefile', + 'plaintext': 'plaintext', + 'text': 'plaintext' +}; + +// // eg ".ts" -> "typescript" +// const fileExtensionToVscodeLanguage: { [key: string]: string } = { +// // Web +// 'html': 'html', +// 'htm': 'html', +// 'css': 'css', +// 'scss': 'scss', +// 'less': 'less', +// 'js': 'javascript', +// 'jsx': 'javascript', +// 'ts': 'typescript', +// 'tsx': 'typescript', +// 'json': 'json', +// 'jsonc': 'json', + +// // Programming Languages +// 'py': 'python', +// 'java': 'java', +// 'cpp': 'cpp', +// 'cc': 'cpp', +// 'c': 'c', +// 'h': 'cpp', +// 'hpp': 'cpp', +// 'cs': 'csharp', +// 'go': 'go', +// 'rs': 'rust', +// 'rb': 'ruby', +// 'php': 'php', +// 'sh': 'shell', +// 'bash': 'shell', +// 'zsh': 'shell', + +// // Markup/Config +// 'md': 'markdown', +// 'markdown': 'markdown', +// 'xml': 'xml', +// 'svg': 'xml', +// 'yaml': 'yaml', +// 'yml': 'yaml', +// 'ini': 'ini', +// 'toml': 'ini', + +// // Other +// 'sql': 'sql', +// 'graphql': 'graphql', +// 'gql': 'graphql', +// 'dockerfile': 'dockerfile', +// 'docker': 'dockerfile', +// 'mk': 'makefile', + +// // Config Files and Dot Files +// 'npmrc': 'ini', +// 'env': 'ini', +// 'gitignore': 'ignore', +// 'dockerignore': 'ignore', +// 'eslintrc': 'json', +// 'babelrc': 'json', +// 'prettierrc': 'json', +// 'stylelintrc': 'json', +// 'editorconfig': 'ini', +// 'htaccess': 'apacheconf', +// 'conf': 'ini', +// 'config': 'ini', + +// // Package Files +// 'package': 'json', +// 'package-lock': 'json', +// 'gemfile': 'ruby', +// 'podfile': 'ruby', +// 'rakefile': 'ruby', + +// // Build Systems +// 'cmake': 'cmake', +// 'makefile': 'makefile', +// 'gradle': 'groovy', + +// // Shell Scripts +// 'bashrc': 'shell', +// 'zshrc': 'shell', +// 'fish': 'shell', + +// // Version Control +// 'gitconfig': 'ini', +// 'hgrc': 'ini', +// 'svnconfig': 'ini', + +// // Web Server +// 'nginx': 'nginx', + +// // Misc Config +// 'properties': 'properties', +// 'cfg': 'ini', +// 'reg': 'ini' +// }; + + +// export function filenameToVscodeLanguage(filename: string): string | undefined { + + + + +// const ext = filename.toLowerCase().split('.').pop(); +// if (!ext) return undefined; + +// return fileExtensionToVscodeLanguage[ext]; +// } diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index 2d956379..806e8468 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -5,8 +5,6 @@ import { URI } from '../../../../../base/common/uri.js'; -import { filenameToVscodeLanguage } from '../helpers/detectLanguage.js'; -import { IModelService } from '../../../../../editor/common/services/model.js'; import { os } from '../helpers/systemInfo.js'; import { IVoidFileService } from '../voidFileService.js'; import { CodeSelection, FileSelection, StagingSelectionItem } from '../chatThreadServiceTypes.js'; @@ -17,8 +15,8 @@ export const tripleTick = ['```', '```'] export const editToolDesc_toolDescription = `\ A high level description of the change you'd like to make in the file. This description will be handed to a dumber, faster model that will quickly apply the change. \ -Typically the best description you can give here is a single code block of the form:\n${tripleTick[0]}\n// ... existing code ...\n{{change 1}}\n// ... existing code ...\n{{change2}}\n// ... existing code ...\n{{change 3}}\n...\n${tripleTick[1]}.\ -Do NOT output the whole file here if possible, and try to write as LITTLE as needed to describe the change.` +Typically the best description you can give here is a single code block of the form:\n${tripleTick[0]}\n// ... existing code ...\n{{change 1}}\n// ... existing code ...\n{{change2}}\n// ... existing code ...\n{{change 3}}\n...\n${tripleTick[1]}. \ +Do NOT output the whole file here if possible, and try to write as LITTLE code as needed to describe the change.` @@ -42,35 +40,37 @@ You will be given tools you can call. - NEVER modify a file outside one of the the user's workspaces without confirmation from the user.`} \ `: `\ -You're allowed to ask for more context. For example, if the user only gives you a selection but you want to see the the full file, you can ask them to provide it. -\ +You're allowed to ask for more context. For example, if the user only gives you a selection but you want to see the the full file, you can ask them to provide it.\ `} ${mode === 'agent' /* code blocks */ ? `\ -If you have a change to make, you should almost always use a tool to edit the file. Even if you don't (e.g. if the user asks you not to), you should still NEVER re-write the entire file for the user. Instead, you should write comments like "// ... existing code" to indicate how to change the existing code. +If you have a change to make, you should almost always use a tool to edit the file. Even if you don't (e.g. if the user asks you not to), you should still NEVER re-write the entire file for the user. Instead, you should write comments like "// ... existing code" to indicate how to change the existing code. \ `: `\ If you think it's appropriate to suggest an edit to a file, then you must describe your suggestion in CODE BLOCK(S) (wrapped in triple backticks). -- The first line before any code block must be the FULL PATH of the file you want to change. If the path does not already exist, it will be created. -- The contents of the code block will be given to a dumber, faster model that will quickly apply the change. +- The first line of the code block must be the FULL PATH of the file you want to change. If the path does not already exist, it will be created. +- The remaining contents of the code block will be given to a dumber, faster model that will quickly apply the change. - Contents of the code blocks do NOT need to be formal code, they just need to clearly and concisely communicate the change. -- Do NOT re-write the entire file in the code block(s). Instead, write comments like "// ... existing code" to indicate how to change the existing code. +- Do NOT re-write the entire file in the code block(s). Instead, write comments like "// ... existing code" to indicate how to change the existing code.`} + +Misc: +- Always wrap any code you produce in triple backticks. \ -`}` +` -type FileSelnLocal = { fileURI: URI, content: string } -const stringifyFileSelection = ({ fileURI, content }: FileSelnLocal) => { +type FileSelnLocal = { fileURI: URI, language: string, content: string } +const stringifyFileSelection = ({ fileURI, language, content }: FileSelnLocal) => { return `\ ${fileURI.fsPath} -${tripleTick[0]}${filenameToVscodeLanguage(fileURI.fsPath) ?? ''} +${tripleTick[0]}${language} ${content} ${tripleTick[1]} ` } -const stringifyCodeSelection = ({ fileURI, selectionStr, range }: CodeSelection) => { +const stringifyCodeSelection = ({ fileURI, language, selectionStr, range }: CodeSelection) => { return `\ ${fileURI.fsPath} (lines ${range.startLineNumber}:${range.endLineNumber}) -${tripleTick[0]}${filenameToVscodeLanguage(fileURI.fsPath) ?? ''} +${tripleTick[0]}${language} ${selectionStr} ${tripleTick[1]} ` @@ -85,14 +85,20 @@ const stringifyFileSelections = async (fileSelections: FileSelection[], voidFile })) return fileSlns.map(sel => stringifyFileSelection(sel)).join('\n') } + + const stringifyCodeSelections = (codeSelections: CodeSelection[]) => { - return codeSelections.map(sel => stringifyCodeSelection(sel)).join('\n') || null + return codeSelections.map(sel => { + stringifyCodeSelection(sel) + }).join('\n') || null } + const stringifySelectionNames = (currSelns: StagingSelectionItem[] | null): string => { if (!currSelns) return '' return currSelns.map(s => `${s.fileURI.fsPath}${s.range ? ` (lines ${s.range.startLineNumber}:${s.range.endLineNumber})` : ''}`).join('\n') } + export const chat_userMessageContent = async (instructions: string, currSelns: StagingSelectionItem[] | null) => { const selnsStr = stringifySelectionNames(currSelns) @@ -103,7 +109,10 @@ export const chat_userMessageContent = async (instructions: string, currSelns: S return str; }; -export const chat_selectionsString = async (prevSelns: StagingSelectionItem[] | null, currSelns: StagingSelectionItem[] | null, voidFileService: IVoidFileService) => { +export const chat_selectionsString = async ( + prevSelns: StagingSelectionItem[] | null, currSelns: StagingSelectionItem[] | null, + voidFileService: IVoidFileService, +) => { // ADD IN FILES AT TOP const allSelections = [...currSelns || [], ...prevSelns || []] @@ -158,9 +167,7 @@ Directions: -export const rewriteCode_userMessage = ({ originalCode, applyStr, uri }: { originalCode: string, applyStr: string, uri: URI }) => { - - const language = filenameToVscodeLanguage(uri.fsPath) ?? '' +export const rewriteCode_userMessage = ({ originalCode, applyStr, language }: { originalCode: string, applyStr: string, language: string }) => { return `\ ORIGINAL_FILE @@ -205,44 +212,44 @@ For example, if the user is asking you to "make this variable a better name", ma - Make sure you give enough context in the code block to apply the changes to the correct location in the code` -export const aiRegex_computeReplacementsForFile_userMessage = async ({ searchClause, replaceClause, fileURI, voidFileService }: { searchClause: string, replaceClause: string, fileURI: URI, modelService: IModelService, voidFileService: IVoidFileService }) => { +// export const aiRegex_computeReplacementsForFile_userMessage = async ({ searchClause, replaceClause, fileURI, voidFileService }: { searchClause: string, replaceClause: string, fileURI: URI, voidFileService: IVoidFileService }) => { - // we may want to do this in batches - const fileSelection: FileSelection = { type: 'File', fileURI, selectionStr: null, range: null, state: { isOpened: false } } +// // we may want to do this in batches +// const fileSelection: FileSelection = { type: 'File', fileURI, selectionStr: null, range: null, state: { isOpened: false } } - const file = await stringifyFileSelections([fileSelection], voidFileService) +// const file = await stringifyFileSelections([fileSelection], voidFileService) - return `\ -## FILE -${file} +// return `\ +// ## FILE +// ${file} -## SEARCH_CLAUSE -Here is what the user is searching for: -${searchClause} +// ## SEARCH_CLAUSE +// Here is what the user is searching for: +// ${searchClause} -## REPLACE_CLAUSE -Here is what the user wants to replace it with: -${replaceClause} +// ## REPLACE_CLAUSE +// Here is what the user wants to replace it with: +// ${replaceClause} -## INSTRUCTIONS -Please return the changes you want to make to the file in a codeblock, or return "no" if you do not want to make changes.` -} +// ## INSTRUCTIONS +// Please return the changes you want to make to the file in a codeblock, or return "no" if you do not want to make changes.` +// } -// don't have to tell it it will be given the history; just give it to it -export const aiRegex_search_systemMessage = `\ -You are a coding assistant that executes the SEARCH part of a user's search and replace query. +// // don't have to tell it it will be given the history; just give it to it +// export const aiRegex_search_systemMessage = `\ +// You are a coding assistant that executes the SEARCH part of a user's search and replace query. -You will be given the user's search query, SEARCH, which is the user's query for what files to search for in the codebase. You may also be given the user's REPLACE query for additional context. +// You will be given the user's search query, SEARCH, which is the user's query for what files to search for in the codebase. You may also be given the user's REPLACE query for additional context. -Output -- Regex query -- Files to Include (optional) -- Files to Exclude? (optional) +// Output +// - Regex query +// - Files to Include (optional) +// - Files to Exclude? (optional) -` +// ` diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts index 2821dfe1..c9388f08 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts @@ -396,6 +396,8 @@ export const prepareMessages = ({ const { messages: messages3, separateSystemMessageStr } = prepareMessages_systemMessage({ messages: messages2, aiInstructions, supportsSystemMessage }) const { messages: messages4 } = prepareMessages_tools({ messages: messages3, supportsTools }) const { messages: messages5 } = prepareMessages_noEmptyMessage({ messages: messages4 }) + + console.log('MESSAGES!!!', JSON.stringify(messages, null, 2)) return { messages: messages5 as any, separateSystemMessageStr 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 abb0bc17..a34535bf 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 @@ -7,7 +7,6 @@ import Anthropic from '@anthropic-ai/sdk'; import { Ollama } from 'ollama'; import OpenAI, { ClientOptions } from 'openai'; -import { Model as OpenAIModel } from 'openai/resources/models.js'; import { extractReasoningOnFinalMessage, extractReasoningOnTextWrapper } from '../../common/helpers/extractCodeFromResult.js'; import { LLMChatMessage, LLMFIMMessage, ModelListParams, OllamaModelResponse, OnError, OnFinalMessage, OnText } from '../../common/sendLLMMessageTypes.js'; import { defaultProviderSettings, displayInfoOfProviderName, ModelSelectionOptions, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js'; @@ -236,6 +235,13 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage } + +type OpenAIModel = { + id: string; + created: number; + object: 'model'; + owned_by: string; +} const _openaiCompatibleList = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider, providerName }: ListParams_Internal) => { const onSuccess = ({ models }: { models: OpenAIModel[] }) => { onSuccess_({ models }) From 8f3a7525624b09fbcdc052a288db4e532eb308a3 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 14 Mar 2025 00:41:17 -0700 Subject: [PATCH 059/173] sync fast apply to chat --- .../react/src/void-settings-tsx/Settings.tsx | 22 ++++++++++++++----- .../void/common/voidSettingsService.ts | 12 ++++++++++ .../contrib/void/common/voidSettingsTypes.ts | 3 ++- 3 files changed, 31 insertions(+), 6 deletions(-) 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 346d9143..3eb18fe6 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 @@ -444,11 +444,11 @@ export const FeaturesTab = () => {

    Feature Options

    -
    +

    {displayInfoOfFeatureName('Autocomplete')}

    Experimental. Only works with models that support FIM.
    -
    +
    { {voidSettingsState.globalSettings.enableAutocomplete ? 'Enabled' : 'Disabled'}
    -
    +

    {displayInfoOfFeatureName('Apply')}

    -
    We recommend using Claude 3.7 or GPT 4o.
    - +
    If you customize this, we recommend using Claude 3.7 or DeepSeek R1.
    +
    + voidSettingsService.setGlobalSetting('syncFastApplyToChat', !newVal)} + /> + {voidSettingsState.globalSettings.syncFastApplyToChat ? 'Sync with Chat' : 'Use Another Model'} +
    + +
    + +
    +
    diff --git a/src/vs/workbench/contrib/void/common/voidSettingsService.ts b/src/vs/workbench/contrib/void/common/voidSettingsService.ts index 278d63da..92940f92 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsService.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsService.ts @@ -273,6 +273,13 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { } + private _onUpdate_syncFastApplyToChat() { + // if sync is turned on, sync (call this whenever Chat model or !!sync changes) + if (this.state.globalSettings.syncFastApplyToChat) { + this.setModelSelectionOfFeature('Apply', deepClone(this.state.modelSelectionOfFeature['Chat'])) + } + } + setGlobalSetting: SetGlobalSettingFn = async (settingName, newVal) => { const newState: VoidSettingsState = { ...this.state, @@ -285,6 +292,8 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { await this._storeState() this._onDidChangeState.fire() + // hooks + this._onUpdate_syncFastApplyToChat() } @@ -301,6 +310,9 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { await this._storeState() this._onDidChangeState.fire() + + // hooks + if (featureName === 'Chat') { this._onUpdate_syncFastApplyToChat() } } diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index 639a3fae..01b07d65 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -385,13 +385,14 @@ export type GlobalSettings = { autoRefreshModels: boolean; aiInstructions: string; enableAutocomplete: boolean; + syncFastApplyToChat: boolean; } export const defaultGlobalSettings: GlobalSettings = { autoRefreshModels: true, aiInstructions: '', enableAutocomplete: false, - + syncFastApplyToChat: true, } export type GlobalSettingName = keyof GlobalSettings From dbdd40bd3d1c56d0ed183bd6996f828ecfd6c193 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 14 Mar 2025 02:23:58 -0700 Subject: [PATCH 060/173] add better settings --- .../contrib/void/browser/editCodeService.ts | 17 ++-- .../void/browser/editCodeServiceInterface.ts | 2 - .../src/markdown/ApplyBlockHoverButtons.tsx | 1 - .../src/quick-edit-tsx/QuickEditChat.tsx | 1 - .../react/src/sidebar-tsx/SidebarChat.tsx | 2 +- .../void/browser/react/src/util/inputs.tsx | 6 +- .../src/void-settings-tsx/ModelDropdown.tsx | 12 +-- .../react/src/void-settings-tsx/Settings.tsx | 90 ++++++++++++++----- .../contrib/void/browser/toolsService.ts | 1 - .../void/common/voidSettingsService.ts | 13 +-- .../contrib/void/common/voidSettingsTypes.ts | 8 +- 11 files changed, 101 insertions(+), 52 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index c4b23325..978016f7 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -1224,13 +1224,16 @@ class EditCodeService extends Disposable implements IEditCodeService { public async startApplying(opts: StartApplyingOpts): Promise<[URI, Promise] | null> { let res: [DiffZone, Promise] | undefined = undefined - if (Math.random() > 0) { - console.log('writeover....') - res = await this._initializeWriteoverStream(opts) - } else { - - if (opts.type === 'rewrite') res = await this._initializeWriteoverStream(opts) - else if (opts.type === 'searchReplace') res = await this._initializeSearchAndReplaceStream(opts) + if (opts.from === 'QuickEdit') { + res = await this._initializeWriteoverStream(opts) // rewrite + } + else if (opts.from === 'ClickApply') { + if (this._settingsService.state.globalSettings.enableFastApply) { + res = await this._initializeSearchAndReplaceStream(opts) // fast apply + } + else { + res = await this._initializeWriteoverStream(opts) // rewrite + } } if (!res) return null diff --git a/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts b/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts index bdcdc05e..e9b1f49e 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts @@ -13,11 +13,9 @@ import { createDecorator } from '../../../../platform/instantiation/common/insta export type StartApplyingOpts = ({ from: 'QuickEdit'; - type: 'rewrite'; diffareaid: number; // id of the CtrlK area (contains text selection) } | { from: 'ClickApply'; - type: 'searchReplace' | 'rewrite'; applyStr: string; uri: 'current' | URI; startBehavior: 'accept-conflicts' | 'reject-conflicts'; 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 a0e0ec4d..eb00ff52 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 @@ -123,7 +123,6 @@ export const useApplyButtonHTML = ({ codeStr, applyBoxId }: { codeStr: string, a if (getStreamState() === 'streaming') return const [newApplyingUri, _] = await editCodeService.startApplying({ from: 'ClickApply', - type: 'searchReplace', applyStr: codeStr, uri: 'current', startBehavior: 'reject-conflicts', diff --git a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx index e42755c1..01a56226 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx @@ -62,7 +62,6 @@ export const QuickEditChat = ({ editCodeService.startApplying({ from: 'QuickEdit', - type: 'rewrite', diffareaid, }) }, [isStreamingRef, isDisabled, editCodeService, diffareaid]) 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 18d3cead..2427634c 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 @@ -363,7 +363,7 @@ export const VoidChatArea: React.FC = ({ {showModelDropdown && (
    - +
    )} 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 069ed703..770df495 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 @@ -460,7 +460,7 @@ export const VoidCheckBox = ({ label, value, onClick, className }: { label: stri -export const VoidCustomDropdownBox = ({ +export const VoidCustomDropdownBox = >({ options, selectedOption, onChangeOption, @@ -537,7 +537,7 @@ export const VoidCustomDropdownBox = ({ // if the selected option is null, set the selection to the 0th option useEffect(() => { if (options.length === 0) return - if (selectedOption) return + if (selectedOption !== undefined) return onChangeOption(options[0]) }, [selectedOption, onChangeOption, options]) @@ -566,7 +566,7 @@ export const VoidCustomDropdownBox = ({ return () => document.removeEventListener('mousedown', handleClickOutside); }, [isOpen, refs.floating, refs.reference]); - if (!selectedOption) + if (selectedOption === undefined) return null return ( diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/ModelDropdown.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/ModelDropdown.tsx index f4943936..42f4de23 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/ModelDropdown.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/ModelDropdown.tsx @@ -21,7 +21,7 @@ const optionsEqual = (m1: ModelOption[], m2: ModelOption[]) => { return true } -const ModelSelectBox = ({ options, featureName }: { options: ModelOption[], featureName: FeatureName }) => { +const ModelSelectBox = ({ options, featureName, className }: { options: ModelOption[], featureName: FeatureName, className: string }) => { const accessor = useAccessor() const voidSettingsService = accessor.get('IVoidSettingsService') @@ -40,7 +40,7 @@ const ModelSelectBox = ({ options, featureName }: { options: ModelOption[], feat getOptionDropdownName={(option) => option.selection.modelName} getOptionDropdownDetail={(option) => option.selection.providerName} getOptionsEqual={(a, b) => optionsEqual([a], [b])} - className='text-xs text-void-fg-3' + className={className} matchInputWidth={false} /> } @@ -77,7 +77,7 @@ const ModelSelectBox = ({ options, featureName }: { options: ModelOption[], feat -const MemoizedModelDropdown = ({ featureName }: { featureName: FeatureName }) => { +const MemoizedModelDropdown = ({ featureName, className }: { featureName: FeatureName, className: string }) => { const settingsState = useSettingsState() const oldOptionsRef = useRef([]) const [memoizedOptions, setMemoizedOptions] = useState(oldOptionsRef.current) @@ -98,11 +98,11 @@ const MemoizedModelDropdown = ({ featureName }: { featureName: FeatureName }) => return } - return + return } -export const ModelDropdown = ({ featureName }: { featureName: FeatureName }) => { +export const ModelDropdown = ({ featureName, className }: { featureName: FeatureName, className: string }) => { const settingsState = useSettingsState() const accessor = useAccessor() @@ -123,5 +123,5 @@ export const ModelDropdown = ({ featureName }: { featureName: FeatureName }) => : 'Provider required' } /> - return + return } 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 3eb18fe6..58965f9f 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 @@ -398,6 +398,30 @@ export const AIInstructionsBox = () => { /> } +const FastApplyMethodDropdown = () => { + const accessor = useAccessor() + const voidSettingsService = accessor.get('IVoidSettingsService') + + const options = useMemo(() => [true, false], []) + + const onChangeOption = useCallback((newVal: boolean) => { + voidSettingsService.setGlobalSetting('enableFastApply', newVal) + }, [voidSettingsService]) + + return val ? 'Fast Apply' : 'Slow Apply'} + getOptionDropdownName={(val) => val ? 'Fast Apply' : 'Slow Apply'} + getOptionDropdownDetail={(val) => val ? 'Output Search/Replace blocks' : 'Rewrite whole files'} + getOptionsEqual={(a, b) => a === b} + /> + +} + + export const FeaturesTab = () => { const voidSettingsState = useSettingsState() const accessor = useAccessor() @@ -445,42 +469,66 @@ export const FeaturesTab = () => {

    Feature Options

    + {/* FIM */}

    {displayInfoOfFeatureName('Autocomplete')}

    -
    Experimental. Only works with models that support FIM.
    -
    - voidSettingsService.setGlobalSetting('enableAutocomplete', newVal)} - /> - {voidSettingsState.globalSettings.enableAutocomplete ? 'Enabled' : 'Disabled'} +
    Experimental. Only works with models that support FIM.
    + +
    + {/* Enable Switch */} +
    + voidSettingsService.setGlobalSetting('enableAutocomplete', newVal)} + /> + {voidSettingsState.globalSettings.enableAutocomplete ? 'Enabled' : 'Disabled'} +
    + {/* Model Dropdown */} +
    + +
    -
    - -
    + {/* Apply */}

    {displayInfoOfFeatureName('Apply')}

    -
    If you customize this, we recommend using Claude 3.7 or DeepSeek R1.
    -
    - voidSettingsService.setGlobalSetting('syncFastApplyToChat', !newVal)} - /> - {voidSettingsState.globalSettings.syncFastApplyToChat ? 'Sync with Chat' : 'Use Another Model'} +
    Settings that control the behavior of the Apply button and the Edit tool.
    + +
    + {/* Sync to Chat Switch */} +
    + voidSettingsService.setGlobalSetting('syncApplyToChat', newVal)} + /> + {voidSettingsState.globalSettings.syncApplyToChat ? 'Sync with Chat model' : 'Use another model'} +
    + + {/* Model Dropdown */} +
    + +
    -
    - + +
    + {/* Fast Apply Method Dropdown */} +
    + +
    + +
    +
    + diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index fb061bc1..d740a26d 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -330,7 +330,6 @@ export class ToolsService implements IToolsService { uri, applyStr: changeDescription, from: 'ClickApply', - type: 'searchReplace', startBehavior: 'accept-conflicts', }) ?? [] await applyDonePromise diff --git a/src/vs/workbench/contrib/void/common/voidSettingsService.ts b/src/vs/workbench/contrib/void/common/voidSettingsService.ts index 92940f92..fd0f973b 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsService.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsService.ts @@ -273,11 +273,10 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { } - private _onUpdate_syncFastApplyToChat() { + private _onUpdate_syncApplyToChat() { // if sync is turned on, sync (call this whenever Chat model or !!sync changes) - if (this.state.globalSettings.syncFastApplyToChat) { - this.setModelSelectionOfFeature('Apply', deepClone(this.state.modelSelectionOfFeature['Chat'])) - } + this.setModelSelectionOfFeature('Apply', deepClone(this.state.modelSelectionOfFeature['Chat'])) + } setGlobalSetting: SetGlobalSettingFn = async (settingName, newVal) => { @@ -293,7 +292,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { this._onDidChangeState.fire() // hooks - this._onUpdate_syncFastApplyToChat() + if (this.state.globalSettings.syncApplyToChat) this._onUpdate_syncApplyToChat() } @@ -312,7 +311,9 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { this._onDidChangeState.fire() // hooks - if (featureName === 'Chat') { this._onUpdate_syncFastApplyToChat() } + if (featureName === 'Chat') { + if (this.state.globalSettings.syncApplyToChat) this._onUpdate_syncApplyToChat() + } } diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index 01b07d65..f5516ff3 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -324,7 +324,7 @@ export const displayInfoOfFeatureName = (featureName: FeatureName) => { else if (featureName === 'Chat') return 'Chat' else if (featureName === 'Apply') - return 'Fast Apply' + return 'Apply' else throw new Error(`Feature Name ${featureName} not allowed`) } @@ -385,14 +385,16 @@ export type GlobalSettings = { autoRefreshModels: boolean; aiInstructions: string; enableAutocomplete: boolean; - syncFastApplyToChat: boolean; + syncApplyToChat: boolean; + enableFastApply: boolean; } export const defaultGlobalSettings: GlobalSettings = { autoRefreshModels: true, aiInstructions: '', enableAutocomplete: false, - syncFastApplyToChat: true, + syncApplyToChat: true, + enableFastApply: true, } export type GlobalSettingName = keyof GlobalSettings From 136840cca4b65e1fc1ac7dcb152bfb0ee12ca357 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Commaret?= Date: Fri, 14 Mar 2025 11:54:43 +0100 Subject: [PATCH 061/173] =?UTF-8?q?=F0=9F=98=BB=20:=20Miaou=20!?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../llmMessage/sendLLMMessage.impl.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) 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 0c10e5ac..8694795a 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 @@ -121,7 +121,7 @@ const newOpenAICompatibleSDK = ({ settingsOfProvider, providerName, includeInPay } -const _sendOpenAICompatibleFIM = ({ messages: messages_, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, aiInstructions, }: SendFIMParams_Internal) => { +const _sendOpenAICompatibleFIM = ({ messages: messages_, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, aiInstructions, modelSelectionOptions, }: SendFIMParams_Internal) => { const { modelName, supportsFIM } = getModelCapabilities(providerName, modelName_) if (!supportsFIM) { if (modelName === modelName_) @@ -155,7 +155,7 @@ const _sendOpenAICompatibleFIM = ({ messages: messages_, onFinalMessage, onError -const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, aiInstructions, tools: tools_ }: SendChatParams_Internal) => { +const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, aiInstructions, modelSelectionOptions, tools: tools_ }: SendChatParams_Internal) => { const { modelName, supportsReasoning, @@ -469,7 +469,7 @@ const sendOllamaFIM = ({ messages: messages_, onFinalMessage, onError, settingsO } //////// MISTRAL //////// -const sendMistralChat = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, aiInstructions }: SendChatParams_Internal) => { +const sendMistralChat = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, aiInstructions, modelSelectionOptions }: SendChatParams_Internal) => { _sendOpenAICompatibleChat({ messages: messages_, onText, @@ -479,11 +479,12 @@ const sendMistralChat = ({ messages: messages_, onText, onFinalMessage, onError, modelName: modelName_, _setAborter, providerName, - aiInstructions + aiInstructions, + modelSelectionOptions }); } -const sendMistralFIM = ({ messages: messages_, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, aiInstructions }: SendFIMParams_Internal) => { +const sendMistralFIM = ({ messages: messages_, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, aiInstructions, modelSelectionOptions }: SendFIMParams_Internal) => { const { modelName, supportsFIM } = getModelCapabilities(providerName, modelName_) if (!supportsFIM) { if (modelName === modelName_) @@ -492,7 +493,8 @@ const sendMistralFIM = ({ messages: messages_, onFinalMessage, onError, settings onError({ message: `Model ${modelName_} (${modelName}) does not support FIM.`, fullError: null }) return } - const messages = prepareFIMMessage({ messages: messages_, aiInstructions }) + + prepareFIMMessage({ messages: messages_, aiInstructions }) _sendOpenAICompatibleFIM({ messages: messages_, @@ -502,7 +504,9 @@ const sendMistralFIM = ({ messages: messages_, onFinalMessage, onError, settings modelName: modelName_, _setAborter, providerName, - aiInstructions + aiInstructions, + modelSelectionOptions, + onText: () => { } }); } From 8a6b75b6bbebcd56ae99bbe3a798f7444d0da9f8 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sat, 15 Mar 2025 01:17:13 -0700 Subject: [PATCH 062/173] style updates progress --- .../contrib/void/browser/chatThreadService.ts | 223 +++++++++++++++++- .../contrib/void/browser/media/void.css | 2 +- .../src/markdown/ApplyBlockHoverButtons.tsx | 95 ++++++-- .../browser/react/src/markdown/BlockCode.tsx | 48 +--- .../react/src/markdown/ChatMarkdownRender.tsx | 63 ++--- .../react/src/sidebar-tsx/SidebarChat.tsx | 207 ++++++++-------- .../contrib/void/browser/toolsService.ts | 4 +- .../contrib/void/common/prompt/prompts.ts | 24 +- 8 files changed, 458 insertions(+), 208 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 6cbff796..5cb20f8e 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -237,7 +237,228 @@ class ChatThreadService extends Disposable implements IChatThreadService { if (!threadsStr) { return null } - return this._convertThreadDataFromStorage(threadsStr); + const threads = this._convertThreadDataFromStorage(threadsStr); + + // threads['abc'] = { + // id: 'abc', + // createdAt: new Date().toISOString(), + // lastModified: new Date().toISOString(), + // messages: [ + // { + // role: 'tool', + // name: 'pathname_search', + // id: 'tool-1', + // paramsStr: '{"query": "hello", "pageNumber": 0}', + // content: '/users/andrew/void/Desktop/etc/abc.txt', + // result: { type: 'success', params: { queryStr: 'hello', pageNumber: 0 }, value: { uris: [URI.file('/Users/username/Downloads/helloooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo.txt'), URI.file('/Users/username/Downloads/hello1.txt'), URI.file('/Users/username/Downloads/hello2.txt'), URI.file('/Users/username/Downloads/hello3.txt'), URI.file('/Users/username/hello.txt')], hasNextPage: true } }, + // } satisfies ToolMessage<'pathname_search'>, + // { + // role: 'tool', + // name: 'pathname_search', + // id: 'tool-1', + // paramsStr: '{"query": "hello", "pageNumber": 0}', + // content: '/users/andrew/void/Desktop/etc/abc.txt', + // result: { type: 'success', params: { queryStr: 'hello', pageNumber: 0 }, value: { uris: [], hasNextPage: false } }, + // } satisfies ToolMessage<'pathname_search'>, + + // // { + // // role: 'tool_request', + // // name: 'pathname_search', + // // params: { queryStr: 'hello', pageNumber: 0 }, + // // paramsStr: '{"query": "hello", "pageNumber": 0}', + // // voidToolId: 'request-1', + // // } satisfies ToolRequestApproval<'pathname_search'>, + + // { + // role: 'tool', + // name: 'list_dir', + // id: 'tool-2', + // paramsStr: '{"uri": "/Users/username/Documents"}', + // content: 'Directory listing of /Users/username/Documents', + // result: { + // type: 'success', + // params: { rootURI: URI.file('/Users/username/Documents'), pageNumber: 1, }, + // value: { + // children: [ + // { uri: URI.file('/Users/username/Documents/file1.txt'), name: 'file1.txt', isDirectory: false, isSymbolicLink: false }, + // { uri: URI.file('/Users/username/Documents/folder1'), name: 'folder1', isDirectory: true, isSymbolicLink: false } + // ], + // hasNextPage: true, + // hasPrevPage: true, + // itemsRemaining: 5, + // } + // }, + // } satisfies ToolMessage<'list_dir'>, + + // // { + // // role: 'tool_request', + // // name: 'list_dir', + // // params: { rootURI: URI.file('/Users/username/Documents'), pageNumber: 0 }, + // // paramsStr: '{"uri": "/Users/username/Documents"}', + // // voidToolId: 'request-2', + // // } satisfies ToolRequestApproval<'list_dir'>, + + // { + // role: 'tool', + // name: 'read_file', + // id: 'tool-3', + // paramsStr: '{"uri": "/Users/username/Documents/file1.txt"}', + // content: 'Content of file1.txt\nThis is a sample file.\nHello world!', + // result: { + // type: 'success', + // params: { uri: URI.file('/Users/username/Documents/file1.txt'), pageNumber: 0 }, + // value: { fileContents: 'Content of file1.txt\nThis is a sample file.\nHello world!', hasNextPage: false } + // }, + // } satisfies ToolMessage<'read_file'>, + + // // { + // // role: 'tool_request', + // // name: 'read_file', + // // params: { uri: URI.file('/Users/username/Documents/file1.txt'), pageNumber: 0 }, + // // paramsStr: '{"uri": "/Users/username/Documents/file1.txt"}', + // // voidToolId: 'request-3', + // // } satisfies ToolRequestApproval<'read_file'>, + + // { + // role: 'tool', + // name: 'search', + // id: 'tool-4', + // paramsStr: '{"query": "function main"}', + // content: 'Found matches in 3 files', + // result: { + // type: 'success', + // params: { queryStr: 'function main', pageNumber: 0 }, + // value: { + // uris: [ + // URI.file('/Users/username/Project/main.js'), + // URI.file('/Users/username/Project/src/app.js'), + // URI.file('/Users/username/Project/test/test.js') + // ], + // hasNextPage: false + // } + // }, + // } satisfies ToolMessage<'search'>, + + // // { + // // role: 'tool_request', + // // name: 'search', + // // params: { queryStr: 'function main', pageNumber: 0 }, + // // paramsStr: '{"query": "function main"}', + // // voidToolId: 'request-4', + // // } satisfies ToolRequestApproval<'search'>, + + // // --- + + // { + // role: 'tool', + // name: 'edit', + // id: 'tool-5', + // paramsStr: '{"uri": "/Users/username/Project/main.js", "changeDescription": "Add console.log statement"}', + // content: 'Successfully edited the file at /Users/username/Project/main.js', + // result: { + // type: 'success', + // params: { uri: URI.file('/Users/username/Project/main.js'), changeDescription: 'Add console.log statement' }, + // value: {} + // }, + // } satisfies ToolMessage<'edit'>, + // { + // role: 'tool_request', + // name: 'edit', + // params: { uri: URI.file('/Users/username/Project/main.js'), changeDescription: 'Add console.log statement' }, + // paramsStr: '{"uri": "/Users/username/Project/main.js", "changeDescription": "Add console.log statement"}', + // voidToolId: 'request-5', + // } satisfies ToolRequestApproval<'edit'>, + + // { + // role: 'tool', + // name: 'create_uri', + // id: 'tool-6', + // paramsStr: '{"uri": "/Users/username/Project/new-file.js"}', + // content: 'Successfully created file at /Users/username/Project/new-file.js', + // result: { + // type: 'success', + // params: { uri: URI.file('/Users/username/Project/new-file.js'), isFolder: false }, + // value: {} + // }, + // } satisfies ToolMessage<'create_uri'>, + // { + // role: 'tool_request', + // name: 'create_uri', + // params: { uri: URI.file('/Users/username/Project/new-file.js'), isFolder: false }, + // paramsStr: '{"uri": "/Users/username/Project/new-file.js"}', + // voidToolId: 'request-6', + // } satisfies ToolRequestApproval<'create_uri'>, + + // { + // role: 'tool', + // name: 'delete_uri', + // id: 'tool-7', + // paramsStr: '{"uri": "/Users/username/Project/old-file.js", "params": ""}', + // content: 'Successfully deleted file at /Users/username/Project/old-file.js', + // result: { + // type: 'success', + // params: { uri: URI.file('/Users/username/Project/old-file.js'), isRecursive: false, isFolder: false }, + // value: {} + // }, + // } satisfies ToolMessage<'delete_uri'>, + // { + // role: 'tool_request', + // name: 'delete_uri', + // params: { uri: URI.file('/Users/username/Project/old-file.js'), isRecursive: false, isFolder: false }, + // paramsStr: '{"uri": "/Users/username/Project/old-file.js", "params": ""}', + // voidToolId: 'request-7', + // } satisfies ToolRequestApproval<'delete_uri'>, + + // { + // role: 'tool', + // name: 'terminal_command', + // id: 'tool-8', + // paramsStr: '{"command": "npm install", "waitForCompletion": "true"}', + // content: 'Command executed: npm install\nAdded 123 packages in 3.5s', + // result: { + // type: 'success', + // params: { command: 'npm install', proposedTerminalId: '1', waitForCompletion: true }, + // value: { + // terminalId: '1', + // didCreateTerminal: false, + // result: 'Added 123 packages in 3.5s', + // resolveReason: { type: 'done', exitCode: 0 } + // } + // }, + // } satisfies ToolMessage<'terminal_command'>, + // { + // role: 'tool_request', + // name: 'terminal_command', + // params: { command: 'npm install', proposedTerminalId: '1', waitForCompletion: true }, + // paramsStr: '{"command": "npm install", "waitForCompletion": "true"}', + // voidToolId: 'request-8', + // } satisfies ToolRequestApproval<'terminal_command'>, + + + + // // Examples of error and rejected states + // { + // role: 'tool', + // name: 'pathname_search', + // id: 'tool-error', + // paramsStr: '{"query": "invalid**query"}', + // content: 'Error: Invalid search pattern', + // result: { type: 'error', params: { queryStr: 'invalid**query', pageNumber: 0 }, value: 'Error: Invalid search pattern' }, + // } satisfies ToolMessage<'pathname_search'>, + + // { + // role: 'tool', + // name: 'pathname_search', + // id: 'tool-rejected', + // paramsStr: '{"query": "sensitive-data"}', + // content: 'Tool call was rejected by the user.', + // result: { type: 'rejected', params: { queryStr: 'sensitive-data', pageNumber: 0 } }, + // } satisfies ToolMessage<'pathname_search'>, + // ], + // state: defaultThreadState, + // } + + return threads } private _storeAllThreads(threads: ChatThreads) { diff --git a/src/vs/workbench/contrib/void/browser/media/void.css b/src/vs/workbench/contrib/void/browser/media/void.css index e5e9793e..3a420737 100644 --- a/src/vs/workbench/contrib/void/browser/media/void.css +++ b/src/vs/workbench/contrib/void/browser/media/void.css @@ -83,7 +83,7 @@ .void-scrollable-element::-webkit-scrollbar, .void-scrollable-element *::-webkit-scrollbar { width: 14px !important; - height: 14px !important; + height: 4px !important; } .void-scrollable-element::-webkit-scrollbar-track, 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 eb00ff52..8d75d801 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 @@ -5,6 +5,8 @@ import { isFeatureNameDisabled } from '../../../../common/voidSettingsTypes.js' import { URI } from '../../../../../../../base/common/uri.js' import { LucideIcon, RotateCw } from 'lucide-react' import { Check, X, Square, Copy, Play, } from 'lucide-react' +import { ToolContentsWrapper } from '../sidebar-tsx/SidebarChat.js' +import { ChatMarkdownRender } from './ChatMarkdownRender.js' enum CopyButtonText { Idle = 'Copy', @@ -27,7 +29,8 @@ export const IconShell1 = ({ onClick, title, Icon, disabled, className }: IconBu disabled={disabled} onClick={onClick} className={` - size-[24px] + size-[22px] + p-[4px] flex items-center justify-center text-sm bg-void-bg-3 text-void-fg-1 hover:brightness-110 @@ -36,28 +39,28 @@ export const IconShell1 = ({ onClick, title, Icon, disabled, className }: IconBu ${className} `} > - + ) -export const IconShell2 = ({ onClick, title, Icon, disabled, className }: IconButtonProps) => ( - -) +// export const IconShell2 = ({ onClick, title, Icon, disabled, className }: IconButtonProps) => ( +// +// ) const COPY_FEEDBACK_TIMEOUT = 1000 // amount of time to say 'Copied!' @@ -230,17 +233,16 @@ export const useApplyButtonHTML = ({ codeStr, applyBoxId }: { codeStr: string, a } - const statusIndicatorHTML =
    + const statusIndicatorHTML =
    -
    + />
    return { @@ -248,5 +250,52 @@ export const useApplyButtonHTML = ({ codeStr, applyBoxId }: { codeStr: string, a buttonsHTML } +} + + + + + + +export const BlockCodeApplyWrapper = ({ + children, + initValue, + applyBoxId, + language, + canApply, + +}: { + initValue: string; + children: React.ReactNode; + applyBoxId: string; + canApply: boolean; + language: string; +}) => { + + + const { statusIndicatorHTML, buttonsHTML } = useApplyButtonHTML({ codeStr: initValue, applyBoxId }) + + return
    + + {/* header */} +
    +
    + {statusIndicatorHTML} + + {language || 'text'} + +
    +
    + {buttonsHTML} +
    +
    + + {/* contents */} + + {children} + +
    } diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/BlockCode.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/BlockCode.tsx index c1cd1de2..5a403ad5 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/BlockCode.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/BlockCode.tsx @@ -4,54 +4,10 @@ *--------------------------------------------------------------------------------------*/ import { VoidCodeEditor, VoidCodeEditorProps } from '../util/inputs.js'; -import { useApplyButtonHTML } from './ApplyBlockHoverButtons.js'; - -export const BlockCodeWithApply = ({ initValue, language, applyBoxId }: { initValue: string, language?: string, applyBoxId: string }) => { - - const { statusIndicatorHTML, buttonsHTML } = useApplyButtonHTML({ codeStr: initValue, applyBoxId }) - - return ( -
    -
    -
    -
    {language || 'text'}
    - {statusIndicatorHTML} -
    -
    - {buttonsHTML} -
    -
    - - - -
    - ) -} +import { BlockCodeApplyWrapper, useApplyButtonHTML } from './ApplyBlockHoverButtons.js'; export const BlockCode = ({ ...codeEditorProps }: VoidCodeEditorProps) => { - const isSingleLine = !codeEditorProps.initValue.includes('\n') - - return ( - <> - - - {/*
    - {buttonsOnHover === null ? null : ( -
    -
    - {buttonsOnHover} -
    -
    - )} - - -
    */} - - - ) + return } diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index 521b552d..9823f62a 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -5,12 +5,10 @@ import React, { JSX, useState } from 'react' import { marked, MarkedToken, Token } from 'marked' -import { BlockCode, BlockCodeWithApply } from './BlockCode.js' +import { BlockCode } from './BlockCode.js' import { convertToVscodeLang, getFirstLine, getLanguage } from '../../../../common/helpers/getLanguage.js' -import { useApplyButtonHTML } from './ApplyBlockHoverButtons.js' -import { useAccessor, useChatThreadsState } from '../util/services.js' -import { Range } from '../../../../../../services/search/common/searchExtTypes.js' -import { IRange } from '../../../../../../../base/common/range.js' +import { BlockCodeApplyWrapper, useApplyButtonHTML } from './ApplyBlockHoverButtons.js' +import { useAccessor } from '../util/services.js' import { ScrollType } from '../../../../../../../editor/common/editorCommon.js' import { URI } from '../../../../../../../base/common/uri.js' @@ -102,7 +100,7 @@ const CodespanWithLink = ({ text, rawText, chatMessageLocation }: { text: string export type RenderTokenOptions = { isApplyEnabled?: boolean, isLinkDetectionEnabled?: boolean } -const RenderToken = ({ token, inPTag, chatMessageLocation, tokenIdx, ...options }: { token: Token | string, inPTag?: boolean, chatMessageLocation?: ChatMessageLocation, tokenIdx: string, } & RenderTokenOptions): JSX.Element => { +const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ...options }: { token: Token | string, inPTag?: boolean, codeURI?: URI, chatMessageLocation?: ChatMessageLocation, tokenIdx: string, } & RenderTokenOptions): JSX.Element => { const accessor = useAccessor() const languageService = accessor.get('ILanguageService') @@ -120,36 +118,45 @@ const RenderToken = ({ token, inPTag, chatMessageLocation, tokenIdx, ...options if (t.type === "code") { const [firstLine, remainingContents] = getFirstLine(t.text) const firstLineIsURI = URI.isUri(firstLine) - - let language: string | undefined = undefined - if (t.lang !== undefined) { - // convert markdown language to language that vscode recognizes (eg markdown doesn't know bash but it does know shell) - language = convertToVscodeLang(languageService, t.lang) - } - - else if (!language) { // if still no lang - if (firstLineIsURI) { // get lang from the uri - const uri = URI.file(firstLine) - language = getLanguage(languageService, { uri, fileContents: remainingContents ?? undefined }) - } - else { // get lang from the contents - language = getLanguage(languageService, { uri: null, fileContents: remainingContents ?? undefined }) - } - } const contents = firstLineIsURI ? (remainingContents || '') : t.text // exclude first-line URI from contents - // TODO!!! user should only be able to apply this when the code has been closed (t.raw ends with "```") + // figure out langauge + let language: string | undefined = undefined + let uri: URI | undefined = undefined + if (t.lang) { // a language was provided. empty string is common so check truthy, not just undefined + uri = codeURI + language = convertToVscodeLang(languageService, t.lang) // convert markdown language to language that vscode recognizes (eg markdown doesn't know bash but it does know shell) + } + else { // no language provided - fallback + if (firstLineIsURI) { // get lang from the uri in the markdown + uri = codeURI ?? URI.file(firstLine) + language = getLanguage(languageService, { uri, fileContents: remainingContents ?? undefined }) + } + else { // get lang from the given URI and contents + uri = codeURI + language = getLanguage(languageService, { uri: codeURI ?? null, fileContents: remainingContents ?? undefined }) + } + } + if (options.isApplyEnabled && chatMessageLocation) { + const isCodeblockClosed = t.raw.trimEnd().endsWith('```') // user should only be able to Apply when the code has been closed (t.raw ends with "```") + const applyBoxId = getApplyBoxId({ threadId: chatMessageLocation.threadId, messageIdx: chatMessageLocation.messageIdx, tokenIdx: tokenIdx, }) - return + > + + } return - + } @@ -361,7 +368,7 @@ const RenderToken = ({ token, inPTag, chatMessageLocation, tokenIdx, ...options } -export const ChatMarkdownRender = ({ string, inPTag = false, chatMessageLocation, ...options }: { string: string, inPTag?: boolean, chatMessageLocation: ChatMessageLocation | undefined } & RenderTokenOptions) => { +export const ChatMarkdownRender = ({ string, inPTag = false, chatMessageLocation, ...options }: { string: string, inPTag?: boolean, codeURI?: URI, chatMessageLocation: ChatMessageLocation | undefined } & RenderTokenOptions) => { const tokens = marked.lexer(string); // https://marked.js.org/using_pro#renderer return ( <> 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 2427634c..f8a2cad6 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 @@ -696,8 +696,7 @@ const ToolHeaderComponent = ({ const isClickable = !!(isDropdown || onClick) return (
    -
    - +
    {/* header */}
    )} -
    -
    - {title} - {desc1} +
    +
    + {title} + + {/* Fixed description with proper ellipsis */} + {desc1}
    -
    +
    {desc2 && {desc2} } @@ -731,11 +732,10 @@ const ToolHeaderComponent = ({
    {/* children */} {
    - {children || '(no results)'} + {children}
    }
    @@ -938,6 +938,29 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isLoading }: ChatBubble } + +export const ToolContentsWrapper = ({ children, className }: { children: React.ReactNode, className?: string }) => { + return
    +
    + {children} +
    +
    +} +const ListableToolItem = ({ name, onClick, isSmall, className }: { name: string, onClick?: () => void, isSmall?: boolean, className?: string }) => { + return
    +
    +
    {name}
    +
    +} + + const AssistantMessageComponent = ({ chatMessage, isLoading, messageIdx, isLast }: ChatBubbleProps & { chatMessage: ChatMessage & { role: 'assistant' } }) => { const accessor = useAccessor() @@ -963,7 +986,6 @@ const AssistantMessageComponent = ({ chatMessage, isLoading, messageIdx, isLast className=' text-void-fg-2 - prose prose-sm break-words @@ -1014,15 +1036,15 @@ const AssistantMessageComponent = ({ chatMessage, isLoading, messageIdx, isLast // 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 toolNameToTitle: Record = { - 'read_file': 'Read file', // past tense - 'list_dir': 'Inspected folder', // past tense - 'pathname_search': 'Searched by file name', // past tense - 'search': 'Searched', // past tense - 'create_uri': 'Created file', // past tense - 'delete_uri': 'Deleted file', // past tense - 'edit': 'Edited file', // past tense - 'terminal_command': 'Ran terminal command' // past tense +const toolNameToTitle: Record = { + 'read_file': { past: 'Read file', current: 'Reading file', proposed: 'Read file' }, + 'list_dir': { past: 'Inspected folder', current: 'Inspecting folder', proposed: 'Inspect folder' }, + 'pathname_search': { past: 'Searched by file name', current: 'Searching by file name', proposed: 'Search by file name' }, + 'search': { past: 'Searched', current: 'Searching', proposed: 'Search' }, + 'create_uri': { past: 'Created file', current: 'Creating file', proposed: 'Create file' }, + 'delete_uri': { past: 'Deleted file', current: 'Deleting file', proposed: 'Delete file' }, + 'edit': { past: 'Edited file', current: 'Editing file', proposed: 'Edit file' }, + 'terminal_command': { past: 'Ran terminal command', current: 'Running terminal command', proposed: 'Run terminal command' } } const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName] | undefined): string => { @@ -1128,17 +1150,17 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') - const title = toolNameToTitle[toolMessage.name] + const title = toolNameToTitle[toolMessage.name].past const { uri } = toolMessage.result.params ?? {} const desc1 = uri ? getBasename(uri.fsPath) : ''; const icon = null - if (toolMessage.result.type === 'rejected') return null + if (toolMessage.result.type === 'rejected') return null // will never happen, not rejectable const isError = toolMessage.result.type === 'error' const componentParams: ToolHeaderParams = { title, desc1, isError, icon } - if (toolMessage.result.type !== 'error') { + if (toolMessage.result.type === 'success') { const { value, params } = toolMessage.result componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } if (toolMessage.result.value.hasNextPage) componentParams.desc2 = `(AI can scroll for more)` @@ -1158,38 +1180,34 @@ const toolNameToComponent: { [T in ToolName]: { const accessor = useAccessor() const commandService = accessor.get('ICommandService') const explorerService = accessor.get('IExplorerService') - const title = toolNameToTitle[toolMessage.name] + const title = toolNameToTitle[toolMessage.name].past const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params) const icon = null - if (toolMessage.result.type === 'rejected') return null + if (toolMessage.result.type === 'rejected') return null // will never happen, not rejectable const isError = toolMessage.result.type === 'error' const componentParams: ToolHeaderParams = { title, desc1, isError, icon } - if (toolMessage.result.type !== 'error') { + if (toolMessage.result.type === 'success') { const { value, params } = toolMessage.result componentParams.numResults = value.children?.length - componentParams.children = (value.children?.length ?? 0) === 0 ? null : <> - {value.children?.map((child, i) => ( -
    + {!value.children || (value.children.length ?? 0) === 0 ? <> + + : <> + {value.children.map((child, i) => ( { commandService.executeCommand('workbench.view.explorer'); explorerService.select(child.uri, true); }} - > -
    - {`${child.name}${child.isDirectory ? '/' : ''}`} -
    - ))} - {value.hasNextPage && ( -
    - {value.itemsRemaining} more items... -
    - )} - + />))} + {value.hasNextPage && + + } + } + } else { componentParams.children = <> @@ -1205,37 +1223,31 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') - const title = toolNameToTitle[toolMessage.name] + const title = toolNameToTitle[toolMessage.name].past const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params) const icon = null - if (toolMessage.result.type === 'rejected') return null + if (toolMessage.result.type === 'rejected') return null // will never happen, not rejectable const isError = toolMessage.result.type === 'error' const componentParams: ToolHeaderParams = { title, desc1, isError, icon } - if (toolMessage.result.type !== 'error') { + if (toolMessage.result.type === 'success') { const { value, params } = toolMessage.result componentParams.numResults = value.uris.length - componentParams.children = value.uris.length === 0 ? null : <> - {value.uris.map((uri, i) => ( -
    { - commandService.executeCommand('vscode.open', uri, { preview: true }) - }} - > -
    - {uri.fsPath.split('/').pop()} -
    - ))} - {value.hasNextPage && ( -
    - More results available... -
    - )} - + componentParams.children = + {value.uris.length === 0 ? <> + + : <> + {value.uris.map((uri, i) => ( { commandService.executeCommand('vscode.open', uri, { preview: true }) }} + />))} + {value.hasNextPage && + + } + } + } else { componentParams.children = <> @@ -1251,30 +1263,31 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') - const title = toolNameToTitle[toolMessage.name] + const title = toolNameToTitle[toolMessage.name].past const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params) const icon = null - if (toolMessage.result.type === 'rejected') return null + if (toolMessage.result.type === 'rejected') return null // will never happen, not rejectable const isError = toolMessage.result.type === 'error' const componentParams: ToolHeaderParams = { title, desc1, isError, icon } - if (toolMessage.result.type !== 'error') { + if (toolMessage.result.type === 'success') { const { value, params } = toolMessage.result componentParams.numResults = value.uris.length - componentParams.children = value.uris.length === 0 ? null : <> - {value.uris.map((uri, i) => ( -
    + {value.uris.length === 0 ? <> + + : <> + {value.uris.map((uri, i) => ( { commandService.executeCommand('vscode.open', uri, { preview: true }) }} - > -
    - {uri.fsPath.split('/').pop()} -
    - ))} - {value.hasNextPage && (
    More results available...
    )} - + />))} + {value.hasNextPage && + + } + } + } else { componentParams.children = <> @@ -1291,7 +1304,7 @@ const toolNameToComponent: { [T in ToolName]: { requestWrapper: ({ toolRequest }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') - const title = toolNameToTitle[toolRequest.name] + const title = toolNameToTitle[toolRequest.name].proposed const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params) const icon = null @@ -1306,7 +1319,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') - const title = toolNameToTitle[toolMessage.name] + const title = toolNameToTitle[toolMessage.name].past const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params) const icon = null @@ -1335,7 +1348,7 @@ const toolNameToComponent: { [T in ToolName]: { requestWrapper: ({ toolRequest, }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') - const title = toolNameToTitle[toolRequest.name] + const title = toolNameToTitle[toolRequest.name].proposed const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params) const icon = null @@ -1350,7 +1363,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') - const title = toolNameToTitle[toolMessage.name] + const title = toolNameToTitle[toolMessage.name].past const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params) const icon = null @@ -1378,7 +1391,7 @@ const toolNameToComponent: { [T in ToolName]: { requestWrapper: ({ toolRequest, }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') - const title = toolNameToTitle[toolRequest.name] + const title = toolNameToTitle[toolRequest.name].proposed const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params) const icon = null @@ -1386,21 +1399,22 @@ const toolNameToComponent: { [T in ToolName]: { const componentParams: ToolHeaderParams = { title, desc1, isError, icon, } const { params } = toolRequest - componentParams.children =
    -
    { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }}> - {getBasename(params.uri.fsPath)} + componentParams.children = + { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }} + /> +
    +
    - -
    + return }, resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') - const title = toolNameToTitle[toolMessage.name] + const title = toolNameToTitle[toolMessage.name].past const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params) const icon = null @@ -1409,12 +1423,12 @@ const toolNameToComponent: { [T in ToolName]: { if (toolMessage.result.type === 'success') { const { params } = toolMessage.result - componentParams.children = + componentParams.children = componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } } else if (toolMessage.result.type === 'rejected') { const { params } = toolMessage.result - componentParams.children = + componentParams.children = componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } } else if (toolMessage.result.type === 'error') { @@ -1431,7 +1445,7 @@ const toolNameToComponent: { [T in ToolName]: { const accessor = useAccessor() const commandService = accessor.get('ICommandService') const terminalToolsService = accessor.get('ITerminalToolService') - const title = toolNameToTitle[toolRequest.name] + const title = toolNameToTitle[toolRequest.name].proposed const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params) const icon = null @@ -1451,7 +1465,7 @@ const toolNameToComponent: { [T in ToolName]: { const accessor = useAccessor() const commandService = accessor.get('ICommandService') const terminalToolsService = accessor.get('ITerminalToolService') - const title = toolNameToTitle[toolMessage.name] + const title = toolNameToTitle[toolMessage.name].past const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params) const icon = null @@ -1529,7 +1543,8 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx, isLast }: ChatBubblePr } else if (role === 'tool_request') { const ToolRequestWrapper = toolNameToComponent[chatMessage.name].requestWrapper as React.FC<{ toolRequest: any }> // ts isnt smart enough... - if (!isLast) return null + // if (!isLast) return null + if (!ToolRequestWrapper) return null return <> diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index d740a26d..c6c8a7f8 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -75,7 +75,7 @@ const directoryResultToString = (params: ToolCallParams['list_dir'], result: Too let output = ''; const entries = result.children; - if (!result.hasPrevPage) { + if (!result.hasPrevPage) { // is first page output += `${params.rootURI}\n`; } @@ -351,7 +351,7 @@ export class ToolsService implements IToolsService { }, list_dir: (params, result) => { const dirTreeStr = directoryResultToString(params, result) - return dirTreeStr + nextPageStr(result.hasNextPage) + return dirTreeStr // + nextPageStr(result.hasNextPage) // already handles num results remaining }, pathname_search: (params, result) => { return result.uris.map(uri => uri.fsPath).join('\n') + nextPageStr(result.hasNextPage) diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index 806e8468..067fc649 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -14,8 +14,9 @@ import { CodeSelection, FileSelection, StagingSelectionItem } from '../chatThrea export const tripleTick = ['```', '```'] export const editToolDesc_toolDescription = `\ -A high level description of the change you'd like to make in the file. This description will be handed to a dumber, faster model that will quickly apply the change. \ -Typically the best description you can give here is a single code block of the form:\n${tripleTick[0]}\n// ... existing code ...\n{{change 1}}\n// ... existing code ...\n{{change2}}\n// ... existing code ...\n{{change 3}}\n...\n${tripleTick[1]}. \ +A high level description of the change you'd like to make in the file. This description will be handed to a dumber, faster model that will quickly apply the change.\ +The model does not have ANY context except the file content and this description, so make sure to include all necessary information to make the change here.\ +Typically the best description you can give is a code block of the form:\n${tripleTick[0]}\n// ... existing code ...\n{{change 1}}\n// ... existing code ...\n{{change2}}\n// ... existing code ...\n{{change 3}}\n...\n${tripleTick[1]}. \ Do NOT output the whole file here if possible, and try to write as LITTLE code as needed to describe the change.` @@ -29,22 +30,23 @@ The user's system information is as follows: - ${os} - Open workspace(s): ${workspaces.join(', ') || 'NO WORKSPACE OPEN'} ${(mode === 'agent' || mode === 'gather') && runningTerminalIds.length !== 0 ? `\ -- Running terminal IDs: ${runningTerminalIds.join(', ')} +- Existing terminal IDs: ${runningTerminalIds.join(', ')} `: '\n'} ${mode === 'agent' || mode === 'gather' /* tool use */ ? `\ You will be given tools you can call. -- Only use tools if they help you accomplish the user's goal. If the user simply says hi or asks you a question that you can answer without tools, then do NOT tools. -- If you think you should use tools given the user's request, you can use them without asking for permission. Feel free to use tools to gather context, understand the codebase, ${mode === 'agent' ? 'edit files, ' : ''}etc. -- NEVER refer to a tool by name when speaking with the user. For example, do NOT say to the user "I'm going to use \`list_dir\`". Instead, say "I'm going to list all files in ___ directory", etc. Do not refer to "pages" of results, just say you're getting more results. -- Some tools only work if the user has a workspace open. ${mode === 'gather' ? '' : ` -- NEVER modify a file outside one of the the user's workspaces without confirmation from the user.`} +- Only use tools if they help you accomplish the user's goal. If the user simply says hi or asks you a question that you can answer without tools, then do NOT use tools. +- If you think you should use tools, you do not need to ask for permission. Feel free to call tools whenever you'd like. You can use them to understand the codebase, ${mode === 'agent' ? 'run terminal commands, edit files, ' : 'gather relevant files and information, '}etc. +- NEVER refer to a tool by name when speaking with the user (NEVER say something like "I'm going to use \`tool_name\`"). Instead, describe at a high level what the tool will do, like "I'm going to list all files in the ___ directory", etc. Also do not refer to "pages" of results, just say you're getting more results. +- Some tools only work if the user has a workspace open.${mode === 'agent' ? ` +- NEVER modify a file outside the user's workspace(s) without permission from the user.` : ''} \ `: `\ You're allowed to ask for more context. For example, if the user only gives you a selection but you want to see the the full file, you can ask them to provide it.\ `} ${mode === 'agent' /* code blocks */ ? `\ -If you have a change to make, you should almost always use a tool to edit the file. Even if you don't (e.g. if the user asks you not to), you should still NEVER re-write the entire file for the user. Instead, you should write comments like "// ... existing code" to indicate how to change the existing code. \ +- Prioritize editing files and running commands over simply making suggestions. +- Prioritize taking as many steps as you need to complete your request over stopping early.\ `: `\ If you think it's appropriate to suggest an edit to a file, then you must describe your suggestion in CODE BLOCK(S) (wrapped in triple backticks). - The first line of the code block must be the FULL PATH of the file you want to change. If the path does not already exist, it will be created. @@ -53,8 +55,8 @@ If you think it's appropriate to suggest an edit to a file, then you must descri - Do NOT re-write the entire file in the code block(s). Instead, write comments like "// ... existing code" to indicate how to change the existing code.`} Misc: -- Always wrap any code you produce in triple backticks. -\ +- Do not make things up. +- Always wrap any code you produce in triple backticks, and specify a language if possible. For example, ${tripleTick[0]}typescript\n...\n${tripleTick[1]}.\ ` From bf2843b1bd54d5daaed946d14364ec70f8607185 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sat, 15 Mar 2025 02:59:32 -0700 Subject: [PATCH 063/173] edit state and apply state should work, edit UI --- .../contrib/void/browser/chatThreadService.ts | 406 +++++++++--------- .../src/markdown/ApplyBlockHoverButtons.tsx | 24 +- .../browser/react/src/markdown/BlockCode.tsx | 1 - .../react/src/markdown/ChatMarkdownRender.tsx | 5 +- .../react/src/sidebar-tsx/SidebarChat.tsx | 202 +++++---- .../void/common/helpers/getLanguage.ts | 4 +- 6 files changed, 336 insertions(+), 306 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 5cb20f8e..caad3b73 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -23,7 +23,7 @@ import { IToolsService } from './toolsService.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js'; import { ITextModelService } from '../../../../editor/common/services/resolverService.js'; -import { ChatMessage, CodespanLocationLink, StagingSelectionItem, ToolRequestApproval } from '../common/chatThreadServiceTypes.js'; +import { ChatMessage, CodespanLocationLink, StagingSelectionItem, ToolMessage, ToolRequestApproval } from '../common/chatThreadServiceTypes.js'; import { Position } from '../../../../editor/common/core/position.js'; import { ITerminalToolService } from './terminalToolService.js'; @@ -239,224 +239,224 @@ class ChatThreadService extends Disposable implements IChatThreadService { } const threads = this._convertThreadDataFromStorage(threadsStr); - // threads['abc'] = { - // id: 'abc', - // createdAt: new Date().toISOString(), - // lastModified: new Date().toISOString(), - // messages: [ - // { - // role: 'tool', - // name: 'pathname_search', - // id: 'tool-1', - // paramsStr: '{"query": "hello", "pageNumber": 0}', - // content: '/users/andrew/void/Desktop/etc/abc.txt', - // result: { type: 'success', params: { queryStr: 'hello', pageNumber: 0 }, value: { uris: [URI.file('/Users/username/Downloads/helloooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo.txt'), URI.file('/Users/username/Downloads/hello1.txt'), URI.file('/Users/username/Downloads/hello2.txt'), URI.file('/Users/username/Downloads/hello3.txt'), URI.file('/Users/username/hello.txt')], hasNextPage: true } }, - // } satisfies ToolMessage<'pathname_search'>, - // { - // role: 'tool', - // name: 'pathname_search', - // id: 'tool-1', - // paramsStr: '{"query": "hello", "pageNumber": 0}', - // content: '/users/andrew/void/Desktop/etc/abc.txt', - // result: { type: 'success', params: { queryStr: 'hello', pageNumber: 0 }, value: { uris: [], hasNextPage: false } }, - // } satisfies ToolMessage<'pathname_search'>, + threads['abc'] = { + id: 'abc', + createdAt: new Date().toISOString(), + lastModified: new Date().toISOString(), + messages: [ + { + role: 'tool', + name: 'pathname_search', + id: 'tool-1', + paramsStr: '{"query": "hello", "pageNumber": 0}', + content: '/users/andrew/void/Desktop/etc/abc.txt', + result: { type: 'success', params: { queryStr: 'hello', pageNumber: 0 }, value: { uris: [URI.file('/Users/username/Downloads/helloooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo.txt'), URI.file('/Users/username/Downloads/hello1.txt'), URI.file('/Users/username/Downloads/hello2.txt'), URI.file('/Users/username/Downloads/hello3.txt'), URI.file('/Users/username/hello.txt')], hasNextPage: true } }, + } satisfies ToolMessage<'pathname_search'>, + { + role: 'tool', + name: 'pathname_search', + id: 'tool-1', + paramsStr: '{"query": "hello", "pageNumber": 0}', + content: '/users/andrew/void/Desktop/etc/abc.txt', + result: { type: 'success', params: { queryStr: 'hello', pageNumber: 0 }, value: { uris: [], hasNextPage: false } }, + } satisfies ToolMessage<'pathname_search'>, - // // { - // // role: 'tool_request', - // // name: 'pathname_search', - // // params: { queryStr: 'hello', pageNumber: 0 }, - // // paramsStr: '{"query": "hello", "pageNumber": 0}', - // // voidToolId: 'request-1', - // // } satisfies ToolRequestApproval<'pathname_search'>, + // { + // role: 'tool_request', + // name: 'pathname_search', + // params: { queryStr: 'hello', pageNumber: 0 }, + // paramsStr: '{"query": "hello", "pageNumber": 0}', + // voidToolId: 'request-1', + // } satisfies ToolRequestApproval<'pathname_search'>, - // { - // role: 'tool', - // name: 'list_dir', - // id: 'tool-2', - // paramsStr: '{"uri": "/Users/username/Documents"}', - // content: 'Directory listing of /Users/username/Documents', - // result: { - // type: 'success', - // params: { rootURI: URI.file('/Users/username/Documents'), pageNumber: 1, }, - // value: { - // children: [ - // { uri: URI.file('/Users/username/Documents/file1.txt'), name: 'file1.txt', isDirectory: false, isSymbolicLink: false }, - // { uri: URI.file('/Users/username/Documents/folder1'), name: 'folder1', isDirectory: true, isSymbolicLink: false } - // ], - // hasNextPage: true, - // hasPrevPage: true, - // itemsRemaining: 5, - // } - // }, - // } satisfies ToolMessage<'list_dir'>, + { + role: 'tool', + name: 'list_dir', + id: 'tool-2', + paramsStr: '{"uri": "/Users/username/Documents"}', + content: 'Directory listing of /Users/username/Documents', + result: { + type: 'success', + params: { rootURI: URI.file('/Users/username/Documents'), pageNumber: 1, }, + value: { + children: [ + { uri: URI.file('/Users/username/Documents/file1.txt'), name: 'file1.txt', isDirectory: false, isSymbolicLink: false }, + { uri: URI.file('/Users/username/Documents/folder1'), name: 'folder1', isDirectory: true, isSymbolicLink: false } + ], + hasNextPage: true, + hasPrevPage: true, + itemsRemaining: 5, + } + }, + } satisfies ToolMessage<'list_dir'>, - // // { - // // role: 'tool_request', - // // name: 'list_dir', - // // params: { rootURI: URI.file('/Users/username/Documents'), pageNumber: 0 }, - // // paramsStr: '{"uri": "/Users/username/Documents"}', - // // voidToolId: 'request-2', - // // } satisfies ToolRequestApproval<'list_dir'>, + // { + // role: 'tool_request', + // name: 'list_dir', + // params: { rootURI: URI.file('/Users/username/Documents'), pageNumber: 0 }, + // paramsStr: '{"uri": "/Users/username/Documents"}', + // voidToolId: 'request-2', + // } satisfies ToolRequestApproval<'list_dir'>, - // { - // role: 'tool', - // name: 'read_file', - // id: 'tool-3', - // paramsStr: '{"uri": "/Users/username/Documents/file1.txt"}', - // content: 'Content of file1.txt\nThis is a sample file.\nHello world!', - // result: { - // type: 'success', - // params: { uri: URI.file('/Users/username/Documents/file1.txt'), pageNumber: 0 }, - // value: { fileContents: 'Content of file1.txt\nThis is a sample file.\nHello world!', hasNextPage: false } - // }, - // } satisfies ToolMessage<'read_file'>, + { + role: 'tool', + name: 'read_file', + id: 'tool-3', + paramsStr: '{"uri": "/Users/username/Documents/file1.txt"}', + content: 'Content of file1.txt\nThis is a sample file.\nHello world!', + result: { + type: 'success', + params: { uri: URI.file('/src/vs/workbench/hi'), pageNumber: 0 }, + value: { fileContents: 'Content of file1.txt\nThis is a sample file.\nHello world!', hasNextPage: false } + }, + } satisfies ToolMessage<'read_file'>, - // // { - // // role: 'tool_request', - // // name: 'read_file', - // // params: { uri: URI.file('/Users/username/Documents/file1.txt'), pageNumber: 0 }, - // // paramsStr: '{"uri": "/Users/username/Documents/file1.txt"}', - // // voidToolId: 'request-3', - // // } satisfies ToolRequestApproval<'read_file'>, + // { + // role: 'tool_request', + // name: 'read_file', + // params: { uri: URI.file('/Users/username/Documents/file1.txt'), pageNumber: 0 }, + // paramsStr: '{"uri": "/Users/username/Documents/file1.txt"}', + // voidToolId: 'request-3', + // } satisfies ToolRequestApproval<'read_file'>, - // { - // role: 'tool', - // name: 'search', - // id: 'tool-4', - // paramsStr: '{"query": "function main"}', - // content: 'Found matches in 3 files', - // result: { - // type: 'success', - // params: { queryStr: 'function main', pageNumber: 0 }, - // value: { - // uris: [ - // URI.file('/Users/username/Project/main.js'), - // URI.file('/Users/username/Project/src/app.js'), - // URI.file('/Users/username/Project/test/test.js') - // ], - // hasNextPage: false - // } - // }, - // } satisfies ToolMessage<'search'>, + { + role: 'tool', + name: 'search', + id: 'tool-4', + paramsStr: '{"query": "function main"}', + content: 'Found matches in 3 files', + result: { + type: 'success', + params: { queryStr: 'function main', pageNumber: 0 }, + value: { + uris: [ + URI.file('/Users/username/Project/main.js'), + URI.file('/Users/username/Project/src/app.js'), + URI.file('/Users/username/Project/test/test.js') + ], + hasNextPage: false + } + }, + } satisfies ToolMessage<'search'>, - // // { - // // role: 'tool_request', - // // name: 'search', - // // params: { queryStr: 'function main', pageNumber: 0 }, - // // paramsStr: '{"query": "function main"}', - // // voidToolId: 'request-4', - // // } satisfies ToolRequestApproval<'search'>, + // { + // role: 'tool_request', + // name: 'search', + // params: { queryStr: 'function main', pageNumber: 0 }, + // paramsStr: '{"query": "function main"}', + // voidToolId: 'request-4', + // } satisfies ToolRequestApproval<'search'>, - // // --- + // --- - // { - // role: 'tool', - // name: 'edit', - // id: 'tool-5', - // paramsStr: '{"uri": "/Users/username/Project/main.js", "changeDescription": "Add console.log statement"}', - // content: 'Successfully edited the file at /Users/username/Project/main.js', - // result: { - // type: 'success', - // params: { uri: URI.file('/Users/username/Project/main.js'), changeDescription: 'Add console.log statement' }, - // value: {} - // }, - // } satisfies ToolMessage<'edit'>, - // { - // role: 'tool_request', - // name: 'edit', - // params: { uri: URI.file('/Users/username/Project/main.js'), changeDescription: 'Add console.log statement' }, - // paramsStr: '{"uri": "/Users/username/Project/main.js", "changeDescription": "Add console.log statement"}', - // voidToolId: 'request-5', - // } satisfies ToolRequestApproval<'edit'>, + { + role: 'tool', + name: 'edit', + id: 'tool-5', + paramsStr: '{"uri": "/Users/username/Project/main.js", "changeDescription": "Add console.log statement"}', + content: 'Successfully edited the file at /Users/username/Project/main.js', + result: { + type: 'success', + params: { uri: URI.file('/Users/username/Project/main.js'), changeDescription: 'Add console.log statement' }, + value: {} + }, + } satisfies ToolMessage<'edit'>, + { + role: 'tool_request', + name: 'edit', + params: { uri: URI.file('/Users/username/Project/main.js'), changeDescription: 'Add console.log statement' }, + paramsStr: '{"uri": "/Users/username/Project/main.js", "changeDescription": "Add console.log statement"}', + voidToolId: 'request-5', + } satisfies ToolRequestApproval<'edit'>, - // { - // role: 'tool', - // name: 'create_uri', - // id: 'tool-6', - // paramsStr: '{"uri": "/Users/username/Project/new-file.js"}', - // content: 'Successfully created file at /Users/username/Project/new-file.js', - // result: { - // type: 'success', - // params: { uri: URI.file('/Users/username/Project/new-file.js'), isFolder: false }, - // value: {} - // }, - // } satisfies ToolMessage<'create_uri'>, - // { - // role: 'tool_request', - // name: 'create_uri', - // params: { uri: URI.file('/Users/username/Project/new-file.js'), isFolder: false }, - // paramsStr: '{"uri": "/Users/username/Project/new-file.js"}', - // voidToolId: 'request-6', - // } satisfies ToolRequestApproval<'create_uri'>, + { + role: 'tool', + name: 'create_uri', + id: 'tool-6', + paramsStr: '{"uri": "/Users/username/Project/new-file.js"}', + content: 'Successfully created file at /Users/username/Project/new-file.js', + result: { + type: 'success', + params: { uri: URI.file('Users/andrew/Desktop/void/src/vs/workbench/hi'), isFolder: false }, + value: {} + }, + } satisfies ToolMessage<'create_uri'>, + { + role: 'tool_request', + name: 'create_uri', + params: { uri: URI.file('/Users/username/Project/new-file.js'), isFolder: false }, + paramsStr: '{"uri": "/Users/username/Project/new-file.js"}', + voidToolId: 'request-6', + } satisfies ToolRequestApproval<'create_uri'>, - // { - // role: 'tool', - // name: 'delete_uri', - // id: 'tool-7', - // paramsStr: '{"uri": "/Users/username/Project/old-file.js", "params": ""}', - // content: 'Successfully deleted file at /Users/username/Project/old-file.js', - // result: { - // type: 'success', - // params: { uri: URI.file('/Users/username/Project/old-file.js'), isRecursive: false, isFolder: false }, - // value: {} - // }, - // } satisfies ToolMessage<'delete_uri'>, - // { - // role: 'tool_request', - // name: 'delete_uri', - // params: { uri: URI.file('/Users/username/Project/old-file.js'), isRecursive: false, isFolder: false }, - // paramsStr: '{"uri": "/Users/username/Project/old-file.js", "params": ""}', - // voidToolId: 'request-7', - // } satisfies ToolRequestApproval<'delete_uri'>, + { + role: 'tool', + name: 'delete_uri', + id: 'tool-7', + paramsStr: '{"uri": "/Users/username/Project/old-file.js", "params": ""}', + content: 'Successfully deleted file at /Users/username/Project/old-file.js', + result: { + type: 'success', + params: { uri: URI.file('/Users/username/Project/old-file.js'), isRecursive: false, isFolder: false }, + value: {} + }, + } satisfies ToolMessage<'delete_uri'>, + { + role: 'tool_request', + name: 'delete_uri', + params: { uri: URI.file('/Users/username/Project/old-file.js'), isRecursive: false, isFolder: false }, + paramsStr: '{"uri": "/Users/username/Project/old-file.js", "params": ""}', + voidToolId: 'request-7', + } satisfies ToolRequestApproval<'delete_uri'>, - // { - // role: 'tool', - // name: 'terminal_command', - // id: 'tool-8', - // paramsStr: '{"command": "npm install", "waitForCompletion": "true"}', - // content: 'Command executed: npm install\nAdded 123 packages in 3.5s', - // result: { - // type: 'success', - // params: { command: 'npm install', proposedTerminalId: '1', waitForCompletion: true }, - // value: { - // terminalId: '1', - // didCreateTerminal: false, - // result: 'Added 123 packages in 3.5s', - // resolveReason: { type: 'done', exitCode: 0 } - // } - // }, - // } satisfies ToolMessage<'terminal_command'>, - // { - // role: 'tool_request', - // name: 'terminal_command', - // params: { command: 'npm install', proposedTerminalId: '1', waitForCompletion: true }, - // paramsStr: '{"command": "npm install", "waitForCompletion": "true"}', - // voidToolId: 'request-8', - // } satisfies ToolRequestApproval<'terminal_command'>, + { + role: 'tool', + name: 'terminal_command', + id: 'tool-8', + paramsStr: '{"command": "npm install", "waitForCompletion": "true"}', + content: 'Command executed: npm install\nAdded 123 packages in 3.5s', + result: { + type: 'success', + params: { command: 'npm install', proposedTerminalId: '1', waitForCompletion: true }, + value: { + terminalId: '1', + didCreateTerminal: false, + result: 'Added 123 packages in 3.5s', + resolveReason: { type: 'done', exitCode: 0 } + } + }, + } satisfies ToolMessage<'terminal_command'>, + { + role: 'tool_request', + name: 'terminal_command', + params: { command: 'npm install', proposedTerminalId: '1', waitForCompletion: true }, + paramsStr: '{"command": "npm install", "waitForCompletion": "true"}', + voidToolId: 'request-8', + } satisfies ToolRequestApproval<'terminal_command'>, - // // Examples of error and rejected states - // { - // role: 'tool', - // name: 'pathname_search', - // id: 'tool-error', - // paramsStr: '{"query": "invalid**query"}', - // content: 'Error: Invalid search pattern', - // result: { type: 'error', params: { queryStr: 'invalid**query', pageNumber: 0 }, value: 'Error: Invalid search pattern' }, - // } satisfies ToolMessage<'pathname_search'>, + // Examples of error and rejected states + { + role: 'tool', + name: 'pathname_search', + id: 'tool-error', + paramsStr: '{"query": "invalid**query"}', + content: 'Error: Invalid search pattern', + result: { type: 'error', params: { queryStr: 'invalid**query', pageNumber: 0 }, value: 'Error: Invalid search pattern' }, + } satisfies ToolMessage<'pathname_search'>, - // { - // role: 'tool', - // name: 'pathname_search', - // id: 'tool-rejected', - // paramsStr: '{"query": "sensitive-data"}', - // content: 'Tool call was rejected by the user.', - // result: { type: 'rejected', params: { queryStr: 'sensitive-data', pageNumber: 0 } }, - // } satisfies ToolMessage<'pathname_search'>, - // ], - // state: defaultThreadState, - // } + { + role: 'tool', + name: 'pathname_search', + id: 'tool-rejected', + paramsStr: '{"query": "sensitive-data"}', + content: 'Tool call was rejected by the user.', + result: { type: 'rejected', params: { queryStr: 'sensitive-data', pageNumber: 0 } }, + } satisfies ToolMessage<'pathname_search'>, + ], + state: defaultThreadState, + } return threads } 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 8d75d801..37977c40 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 @@ -98,7 +98,7 @@ const CopyButton = ({ codeStr }: { codeStr: string }) => { const applyingURIOfApplyBoxIdRef: { current: { [applyBoxId: string]: URI | undefined } } = { current: {} } -export const useApplyButtonHTML = ({ codeStr, applyBoxId }: { codeStr: string, applyBoxId: string }) => { +export const useApplyButtonHTML = ({ codeStr, applyBoxId, uri }: { codeStr: string, applyBoxId: string, uri: URI | 'current' }) => { const settingsState = useSettingsState() const isDisabled = !!isFeatureNameDisabled('Apply', settingsState) || !applyBoxId @@ -112,13 +112,16 @@ export const useApplyButtonHTML = ({ codeStr, applyBoxId }: { codeStr: string, a const getUriBeingApplied = useCallback(() => applyingURIOfApplyBoxIdRef.current[applyBoxId] ?? null, [applyBoxId]) const getStreamState = useCallback(() => editCodeService.getURIStreamState({ uri: getUriBeingApplied() }), [editCodeService, getUriBeingApplied]) - // listen for stream updates + // listen for stream updates on this box useURIStreamState( - useCallback((uri, newStreamState) => { - const shouldUpdate = getUriBeingApplied()?.fsPath === uri.fsPath + useCallback((uri_, newStreamState) => { + const shouldUpdate = ( + getUriBeingApplied()?.fsPath === uri_.fsPath + || (uri === 'current' ? false : uri.fsPath === uri_.fsPath) + ) if (!shouldUpdate) return rerender(c => c + 1) - }, [applyBoxId, editCodeService, getUriBeingApplied]) + }, [applyBoxId, editCodeService, getUriBeingApplied, uri]) ) const onClickSubmit = useCallback(async () => { @@ -127,14 +130,14 @@ export const useApplyButtonHTML = ({ codeStr, applyBoxId }: { codeStr: string, a const [newApplyingUri, _] = await editCodeService.startApplying({ from: 'ClickApply', applyStr: codeStr, - uri: 'current', + uri: uri, startBehavior: 'reject-conflicts', }) ?? [] applyingURIOfApplyBoxIdRef.current[applyBoxId] = newApplyingUri ?? undefined rerender(c => c + 1) metricsService.capture('Apply Code', { length: codeStr.length }) // capture the length only - }, [isDisabled, getStreamState, editCodeService, codeStr, applyBoxId, metricsService]) + }, [isDisabled, getStreamState, editCodeService, codeStr, uri, applyBoxId, metricsService]) const onInterrupt = useCallback(() => { @@ -263,17 +266,18 @@ export const BlockCodeApplyWrapper = ({ applyBoxId, language, canApply, - + uri, }: { initValue: string; children: React.ReactNode; applyBoxId: string; canApply: boolean; language: string; + uri: URI | 'current', }) => { - const { statusIndicatorHTML, buttonsHTML } = useApplyButtonHTML({ codeStr: initValue, applyBoxId }) + const { statusIndicatorHTML, buttonsHTML } = useApplyButtonHTML({ codeStr: initValue, applyBoxId, uri }) return
    -
    +
    {buttonsHTML}
    diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/BlockCode.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/BlockCode.tsx index 5a403ad5..114143f0 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/BlockCode.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/BlockCode.tsx @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------*/ import { VoidCodeEditor, VoidCodeEditorProps } from '../util/inputs.js'; -import { BlockCodeApplyWrapper, useApplyButtonHTML } from './ApplyBlockHoverButtons.js'; export const BlockCode = ({ ...codeEditorProps }: VoidCodeEditorProps) => { diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index 9823f62a..f21bef23 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -20,7 +20,7 @@ export type ChatMessageLocation = { type ApplyBoxLocation = ChatMessageLocation & { tokenIdx: string } -const getApplyBoxId = ({ threadId, messageIdx, tokenIdx }: ApplyBoxLocation) => { +export const getApplyBoxId = ({ threadId, messageIdx, tokenIdx }: ApplyBoxLocation) => { return `${threadId}-${messageIdx}-${tokenIdx}` } @@ -120,7 +120,7 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, .. const firstLineIsURI = URI.isUri(firstLine) const contents = firstLineIsURI ? (remainingContents || '') : t.text // exclude first-line URI from contents - // figure out langauge + // figure out langauge and URI let language: string | undefined = undefined let uri: URI | undefined = undefined if (t.lang) { // a language was provided. empty string is common so check truthy, not just undefined @@ -151,6 +151,7 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, .. applyBoxId={applyBoxId} initValue={contents} language={language} + uri={uri || 'current'} > { // '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] } @@ -679,7 +681,7 @@ type ToolHeaderParams = { onClick?: () => void; } -const ToolHeaderComponent = ({ +const ToolHeaderWrapper = ({ icon, title, desc1, @@ -696,7 +698,7 @@ const ToolHeaderComponent = ({ const isClickable = !!(isDropdown || onClick) return (
    -
    +
    {/* header */}
    )} -
    +
    + {/* left */}
    {title} - - {/* Fixed description with proper ellipsis */} {desc1}
    + + {/* right */}
    {desc2 && {desc2} @@ -733,13 +736,13 @@ const ToolHeaderComponent = ({ {/* children */} {
    {children}
    }
    -
    - ); +
    ); }; @@ -939,28 +942,6 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isLoading }: ChatBubble -export const ToolContentsWrapper = ({ children, className }: { children: React.ReactNode, className?: string }) => { - return
    -
    - {children} -
    -
    -} -const ListableToolItem = ({ name, onClick, isSmall, className }: { name: string, onClick?: () => void, isSmall?: boolean, className?: string }) => { - return
    -
    -
    {name}
    -
    -} - - const AssistantMessageComponent = ({ chatMessage, isLoading, messageIdx, isLast }: ChatBubbleProps & { chatMessage: ChatMessage & { role: 'assistant' } }) => { const accessor = useAccessor() @@ -1141,9 +1122,54 @@ const ToolRequestAcceptRejectButtons = ({ voidToolId }: { voidToolId: string })
    } +export const ToolContentsWrapper = ({ children, className }: { children: React.ReactNode, className?: string }) => { + return
    +
    + {children} +
    +
    +} +const ListableToolItem = ({ name, onClick, isSmall, className, showDot }: { name: React.ReactNode, onClick?: () => void, isSmall?: boolean, className?: string, showDot?: boolean }) => { + return
    + {showDot === false ? null :
    } +
    {name}
    +
    +} + +const EditToolChildren = ({ uri, changeDescription }: { uri: URI, changeDescription: string }) => { + const accessor = useAccessor() + const commandService = accessor.get('ICommandService') + return + { commandService.executeCommand('vscode.open', uri, { preview: true }) }} + /> +
    + +
    +
    +} +const EditToolApplyButton = ({ changeDescription, applyBoxId, uri }: { changeDescription: string, applyBoxId: string, uri: URI }) => { + const { statusIndicatorHTML, buttonsHTML } = useApplyButtonHTML({ codeStr: changeDescription, applyBoxId, uri }) + return
    + {statusIndicatorHTML} + {buttonsHTML} +
    +} + + const toolNameToComponent: { [T in ToolName]: { requestWrapper: T extends ToolNameWithApproval ? ((props: { toolRequest: ToolRequestApproval }) => React.ReactNode) : null, - resultWrapper: (props: { toolMessage: ToolMessage }) => React.ReactNode, + resultWrapper: (props: { toolMessage: ToolMessage, messageIdx: number }) => React.ReactNode, } } = { 'read_file': { requestWrapper: null, @@ -1171,7 +1197,7 @@ const toolNameToComponent: { [T in ToolName]: { } - return + return }, }, 'list_dir': { @@ -1192,10 +1218,8 @@ const toolNameToComponent: { [T in ToolName]: { if (toolMessage.result.type === 'success') { const { value, params } = toolMessage.result componentParams.numResults = value.children?.length - componentParams.children = - {!value.children || (value.children.length ?? 0) === 0 ? <> - - : <> + componentParams.children = !value.children || (value.children.length ?? 0) === 0 ? undefined + : {value.children.map((child, i) => ( { @@ -1206,8 +1230,7 @@ const toolNameToComponent: { [T in ToolName]: { {value.hasNextPage && } - } - + } else { componentParams.children = <> @@ -1215,7 +1238,7 @@ const toolNameToComponent: { [T in ToolName]: { } - return + return } }, 'pathname_search': { @@ -1235,10 +1258,8 @@ const toolNameToComponent: { [T in ToolName]: { if (toolMessage.result.type === 'success') { const { value, params } = toolMessage.result componentParams.numResults = value.uris.length - componentParams.children = - {value.uris.length === 0 ? <> - - : <> + componentParams.children = value.uris.length === 0 ? undefined + : {value.uris.map((uri, i) => ( { commandService.executeCommand('vscode.open', uri, { preview: true }) }} @@ -1246,8 +1267,8 @@ const toolNameToComponent: { [T in ToolName]: { {value.hasNextPage && } - } - + + } else { componentParams.children = <> @@ -1255,7 +1276,7 @@ const toolNameToComponent: { [T in ToolName]: { } - return + return } }, 'search': { @@ -1275,10 +1296,8 @@ const toolNameToComponent: { [T in ToolName]: { if (toolMessage.result.type === 'success') { const { value, params } = toolMessage.result componentParams.numResults = value.uris.length - componentParams.children = - {value.uris.length === 0 ? <> - - : <> + componentParams.children = value.uris.length === 0 ? undefined + : {value.uris.map((uri, i) => ( { commandService.executeCommand('vscode.open', uri, { preview: true }) }} @@ -1286,15 +1305,15 @@ const toolNameToComponent: { [T in ToolName]: { {value.hasNextPage && } - } - + + } else { componentParams.children = <> {toolMessage.result.value} } - return + return } }, @@ -1304,6 +1323,7 @@ const toolNameToComponent: { [T in ToolName]: { requestWrapper: ({ toolRequest }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') + const explorerService = accessor.get('IExplorerService') const title = toolNameToTitle[toolRequest.name].proposed const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params) const icon = null @@ -1312,9 +1332,13 @@ const toolNameToComponent: { [T in ToolName]: { const componentParams: ToolHeaderParams = { title, desc1, isError, icon, } const { params } = toolRequest - componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } - return + // TODO!!! would be cool to open up the lowest parent that exists + // componentParams.onClick = () => { + // // open the parent + // } + + return }, resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() @@ -1341,7 +1365,7 @@ const toolNameToComponent: { [T in ToolName]: { } - return + return } }, 'delete_uri': { @@ -1358,7 +1382,7 @@ const toolNameToComponent: { [T in ToolName]: { const { params } = toolRequest componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } - return + return }, resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() @@ -1384,7 +1408,7 @@ const toolNameToComponent: { [T in ToolName]: { } - return + return } }, 'edit': { @@ -1399,21 +1423,16 @@ const toolNameToComponent: { [T in ToolName]: { const componentParams: ToolHeaderParams = { title, desc1, isError, icon, } const { params } = toolRequest - componentParams.children = - { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }} - /> -
    - -
    -
    + componentParams.children = - return + return }, - resultWrapper: ({ toolMessage }) => { + resultWrapper: ({ toolMessage, messageIdx }) => { const accessor = useAccessor() - const commandService = accessor.get('ICommandService') + const chatThreadsService = accessor.get('IChatThreadService') const title = toolNameToTitle[toolMessage.name].past const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params) const icon = null @@ -1421,15 +1440,25 @@ const toolNameToComponent: { [T in ToolName]: { const isError = toolMessage.result.type === 'error' const componentParams: ToolHeaderParams = { title, desc1, isError, icon } - if (toolMessage.result.type === 'success') { + if (toolMessage.result.type === 'success' || toolMessage.result.type === 'rejected') { const { params } = toolMessage.result - componentParams.children = - componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } - } - else if (toolMessage.result.type === 'rejected') { - const { params } = toolMessage.result - componentParams.children = - componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } + + const threadId = chatThreadsService.getCurrentThread().id + const applyBoxId = getApplyBoxId({ + threadId: threadId, + messageIdx: messageIdx, + tokenIdx: 'N/A', + }) + + componentParams.children = + componentParams.desc2 = } else if (toolMessage.result.type === 'error') { componentParams.children = <> @@ -1437,7 +1466,7 @@ const toolNameToComponent: { [T in ToolName]: { } - return + return } }, 'terminal_command': { @@ -1458,8 +1487,7 @@ const toolNameToComponent: { [T in ToolName]: { if (!waitForCompletion) componentParams.desc2 = '(background task)' - // TODO!!! open terminal - return + return }, resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() @@ -1510,9 +1538,7 @@ const toolNameToComponent: { [T in ToolName]: { } - // TODO!!! open terminal - - return + return } } }; @@ -1551,8 +1577,8 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx, isLast }: ChatBubblePr } else if (role === 'tool') { - const ToolResultWrapper = toolNameToComponent[chatMessage.name].resultWrapper as React.FC<{ toolMessage: any }> // ts isnt smart enough... - return + const ToolResultWrapper = toolNameToComponent[chatMessage.name].resultWrapper as React.FC<{ toolMessage: any, messageIdx: number }> // ts isnt smart enough... + return } } diff --git a/src/vs/workbench/contrib/void/common/helpers/getLanguage.ts b/src/vs/workbench/contrib/void/common/helpers/getLanguage.ts index da2c82d1..47835df5 100644 --- a/src/vs/workbench/contrib/void/common/helpers/getLanguage.ts +++ b/src/vs/workbench/contrib/void/common/helpers/getLanguage.ts @@ -67,10 +67,10 @@ const markdownLangToVscodeLang: { [key: string]: string } = { 'less': 'less', 'javascript': 'typescript', 'js': 'typescript', // use more general renderer - 'jsx': 'typescript', + 'jsx': 'typescriptreact', 'typescript': 'typescript', 'ts': 'typescript', - 'tsx': 'typescript', + 'tsx': 'typescriptreact', 'json': 'json', 'jsonc': 'json', From cf6cef8e0eecd59d06c141eec6c4fb6671d315f4 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sat, 15 Mar 2025 03:24:48 -0700 Subject: [PATCH 064/173] massively improve styles --- .../react/src/sidebar-tsx/SidebarChat.tsx | 88 ++++++++++++------- 1 file changed, 55 insertions(+), 33 deletions(-) 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 9181e0f1..c1d394b3 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 @@ -28,7 +28,7 @@ import { WarningBox } from '../void-settings-tsx/WarningBox.js'; import { getModelSelectionState, getModelCapabilities } from '../../../../common/modelCapabilities.js'; import { AlertTriangle, ChevronRight, Dot, Pencil, X } from 'lucide-react'; import { ChatMessage, StagingSelectionItem, ToolMessage, ToolRequestApproval } from '../../../../common/chatThreadServiceTypes.js'; -import { ToolCallParams, ToolName, ToolNameWithApproval } from '../../../../common/toolsServiceTypes.js'; +import { ResolveReason, ToolCallParams, ToolName, ToolNameWithApproval } from '../../../../common/toolsServiceTypes.js'; import { getLanguageFromModel } from '../../../../common/helpers/getLanguage.js'; import { dirname } from '../../../../../../../base/common/resources.js'; import { useApplyButtonHTML } from '../markdown/ApplyBlockHoverButtons.js'; @@ -1143,21 +1143,6 @@ const ListableToolItem = ({ name, onClick, isSmall, className, showDot }: { name
    } -const EditToolChildren = ({ uri, changeDescription }: { uri: URI, changeDescription: string }) => { - const accessor = useAccessor() - const commandService = accessor.get('ICommandService') - return - { commandService.executeCommand('vscode.open', uri, { preview: true }) }} - /> -
    - -
    -
    -} const EditToolApplyButton = ({ changeDescription, applyBoxId, uri }: { changeDescription: string, applyBoxId: string, uri: URI }) => { const { statusIndicatorHTML, buttonsHTML } = useApplyButtonHTML({ codeStr: changeDescription, applyBoxId, uri }) return
    @@ -1167,6 +1152,51 @@ const EditToolApplyButton = ({ changeDescription, applyBoxId, uri }: { changeDes } +const EditToolChildren = ({ uri, changeDescription }: { uri: URI, changeDescription: string }) => { + const accessor = useAccessor() + const commandService = accessor.get('ICommandService') + return + { commandService.executeCommand('vscode.open', uri, { preview: true }) }} + /> +
    +
    + +
    +
    +
    +} +const TerminalToolChildren = ({ command, terminalId, result, resolveReason }: { command: string, terminalId: string, result: string, resolveReason: ResolveReason }) => { + const accessor = useAccessor() + const terminalToolsService = accessor.get('ITerminalToolService') + + const resultStr = resolveReason.type === 'done' ? (resolveReason.exitCode !== 0 ? `\nError: exit code ${resolveReason.exitCode}` : null) + : resolveReason.type === 'bgtask' ? null : + resolveReason.type === 'timeout' ? `\n(partial results; request timed out)` : + resolveReason.type === 'toofull' ? `\n(truncated)` + : null + + return + terminalToolsService.openTerminal(terminalId)} + /> +
    +
    + {resolveReason.type === 'bgtask' ? 'Result so far:\n' : null} + {result} + {resultStr} +
    +
    +
    +} + + const toolNameToComponent: { [T in ToolName]: { requestWrapper: T extends ToolNameWithApproval ? ((props: { toolRequest: ToolRequestApproval }) => React.ReactNode) : null, resultWrapper: (props: { toolMessage: ToolMessage, messageIdx: number }) => React.ReactNode, @@ -1222,6 +1252,7 @@ const toolNameToComponent: { [T in ToolName]: { : {value.children.map((child, i) => ( { commandService.executeCommand('workbench.view.explorer'); explorerService.select(child.uri, true); @@ -1262,6 +1293,7 @@ const toolNameToComponent: { [T in ToolName]: { : {value.uris.map((uri, i) => ( { commandService.executeCommand('vscode.open', uri, { preview: true }) }} />))} {value.hasNextPage && @@ -1300,6 +1332,7 @@ const toolNameToComponent: { [T in ToolName]: { : {value.uris.map((uri, i) => ( { commandService.executeCommand('vscode.open', uri, { preview: true }) }} />))} {value.hasNextPage && @@ -1504,23 +1537,12 @@ const toolNameToComponent: { [T in ToolName]: { const { command } = toolMessage.result.params const { terminalId, resolveReason, result } = toolMessage.result.value - componentParams.children =
    -
    terminalToolsService.openTerminal(terminalId)} - >$ {command}
    - -
    - - {resolveReason.type === 'bgtask' ? 'Result so far:\n' : null} - {result} - {resolveReason.type === 'done' ? (resolveReason.exitCode !== 0 ? `\nError: exit code ${resolveReason.exitCode}` : null) - : resolveReason.type === 'bgtask' ? null : - resolveReason.type === 'timeout' ? `\n(partial results; request timed out)` : - resolveReason.type === 'toofull' ? `\n(truncated)` - : null - } -
    + componentParams.children = if (resolveReason.type === 'bgtask') componentParams.desc2 = '(background task)' From 7ea5870a11adf8f27fb350bc11746f9bdb6e1736 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sat, 15 Mar 2025 03:38:36 -0700 Subject: [PATCH 065/173] alive ping --- .../void/browser/metricsPollService.ts | 56 ++++++++++++++ .../void/browser/quickEditStateService.ts | 77 ------------------- .../src/quick-edit-tsx/QuickEditChat.tsx | 4 +- .../void/browser/react/src/util/services.tsx | 27 +------ .../contrib/void/browser/void.contribution.ts | 3 + .../contrib/void/browser/voidUpdateActions.ts | 2 +- 6 files changed, 63 insertions(+), 106 deletions(-) create mode 100644 src/vs/workbench/contrib/void/browser/metricsPollService.ts delete mode 100644 src/vs/workbench/contrib/void/browser/quickEditStateService.ts diff --git a/src/vs/workbench/contrib/void/browser/metricsPollService.ts b/src/vs/workbench/contrib/void/browser/metricsPollService.ts new file mode 100644 index 00000000..92bbd16c --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/metricsPollService.ts @@ -0,0 +1,56 @@ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ + +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; +import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; + +import * as dom from '../../../../base/browser/dom.js'; +import { IMetricsService } from '../common/metricsService.js'; + + +export interface IMetricsPollService { + readonly _serviceBrand: undefined; +} + + +const PING_EVERY_MS = 15 * 1000 * 60 // 15 minutes + +export const IMetricsPollService = createDecorator('voidMetricsPollService'); +class MetricsPollService extends Disposable implements IMetricsPollService { + _serviceBrand: undefined; + + static readonly ID = 'voidMetricsPollService'; + + + private readonly intervalID: number + constructor( + @IMetricsService private readonly metricsService: IMetricsService, + ) { + super() + + // initial state + const { window } = dom.getActiveWindow() + let i = 1 + + this.intervalID = window.setInterval(() => { + this.metricsService.capture('Alive', { i }) + i += 1 + console.log('ping', i) + }, PING_EVERY_MS) + + + } + + override dispose() { + super.dispose() + const { window } = dom.getActiveWindow() + window.clearInterval(this.intervalID) + } + + +} + +registerWorkbenchContribution2(MetricsPollService.ID, MetricsPollService, WorkbenchPhase.BlockRestore); diff --git a/src/vs/workbench/contrib/void/browser/quickEditStateService.ts b/src/vs/workbench/contrib/void/browser/quickEditStateService.ts deleted file mode 100644 index 62f3823b..00000000 --- a/src/vs/workbench/contrib/void/browser/quickEditStateService.ts +++ /dev/null @@ -1,77 +0,0 @@ -/*-------------------------------------------------------------------------------------- - * Copyright 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. - *--------------------------------------------------------------------------------------*/ - -import { Emitter, Event } from '../../../../base/common/event.js'; -import { Disposable } from '../../../../base/common/lifecycle.js'; -import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; -import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { QuickEdit } from './quickEditActions.js'; - - - -// service that manages state -export type VoidQuickEditState = { - quickEditsOfDocument: { [uri: string]: QuickEdit } -} - -export interface IQuickEditStateService { - readonly _serviceBrand: undefined; - - readonly state: VoidQuickEditState; // readonly to the user - setState(newState: Partial): void; - onDidChangeState: Event; - - onDidFocusChat: Event; - onDidBlurChat: Event; - fireFocusChat(): void; - fireBlurChat(): void; - -} - -export const IQuickEditStateService = createDecorator('voidQuickEditStateService'); -class VoidQuickEditStateService extends Disposable implements IQuickEditStateService { - _serviceBrand: undefined; - - static readonly ID = 'voidQuickEditStateService'; - - private readonly _onDidChangeState = new Emitter(); - readonly onDidChangeState: Event = this._onDidChangeState.event; - - private readonly _onFocusChat = new Emitter(); - readonly onDidFocusChat: Event = this._onFocusChat.event; - - private readonly _onBlurChat = new Emitter(); - readonly onDidBlurChat: Event = this._onBlurChat.event; - - - // state - state: VoidQuickEditState - - constructor( - ) { - super() - - // initial state - this.state = { quickEditsOfDocument: {} } - } - - - setState(newState: Partial) { - - this.state = { ...this.state, ...newState } - this._onDidChangeState.fire() - } - - fireFocusChat() { - this._onFocusChat.fire() - } - - fireBlurChat() { - this._onBlurChat.fire() - } - -} - -registerSingleton(IQuickEditStateService, VoidQuickEditStateService, InstantiationType.Eager); diff --git a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx index 01a56226..de52d120 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx @@ -3,8 +3,8 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import React, { FormEvent, useCallback, useEffect, useRef, useState } from 'react'; -import { useSettingsState, useSidebarState, useChatThreadsState, useQuickEditState, useAccessor, useCtrlKZoneStreamingState } from '../util/services.js'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { useSettingsState, useAccessor, useCtrlKZoneStreamingState } from '../util/services.js'; import { TextAreaFns, VoidInputBox2 } from '../util/inputs.js'; import { QuickEditPropsType } from '../../../quickEditActions.js'; import { ButtonStop, ButtonSubmit, IconX, VoidChatArea } from '../sidebar-tsx/SidebarChat.js'; 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 5e9f4d4a..7ae0ac4e 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 @@ -10,7 +10,6 @@ import { VoidSidebarState } from '../../../sidebarStateService.js' import { VoidSettingsState } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js' import { ColorScheme } from '../../../../../../../platform/theme/common/theme.js' import { VoidUriState } from '../../../voidUriStateService.js'; -import { VoidQuickEditState } from '../../../quickEditStateService.js' import { RefreshModelStateOfProvider } from '../../../../../../../workbench/contrib/void/common/refreshModelService.js' import { ServicesAccessor } from '../../../../../../../editor/browser/editorExtensions.js'; @@ -27,7 +26,6 @@ import { IVoidSettingsService } from '../../../../../../../workbench/contrib/voi import { IEditCodeService, URIStreamState } from '../../../editCodeServiceInterface.js' import { IVoidUriStateService } from '../../../voidUriStateService.js'; -import { IQuickEditStateService } from '../../../quickEditStateService.js'; import { ISidebarStateService } from '../../../sidebarStateService.js'; import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js' import { ICodeEditorService } from '../../../../../../../editor/browser/services/codeEditorService.js' @@ -57,9 +55,6 @@ import { ILanguageService } from '../../../../../../../editor/common/languages/l let uriState: VoidUriState const uriStateListeners: Set<(s: VoidUriState) => void> = new Set() -let quickEditState: VoidQuickEditState -const quickEditStateListeners: Set<(s: VoidQuickEditState) => void> = new Set() - let sidebarState: VoidSidebarState const sidebarStateListeners: Set<(s: VoidSidebarState) => void> = new Set() @@ -94,7 +89,6 @@ export const _registerServices = (accessor: ServicesAccessor) => { const stateServices = { uriStateService: accessor.get(IVoidUriStateService), - quickEditStateService: accessor.get(IQuickEditStateService), sidebarStateService: accessor.get(ISidebarStateService), chatThreadsStateService: accessor.get(IChatThreadService), settingsStateService: accessor.get(IVoidSettingsService), @@ -103,7 +97,7 @@ export const _registerServices = (accessor: ServicesAccessor) => { editCodeService: accessor.get(IEditCodeService), } - const { uriStateService, sidebarStateService, quickEditStateService, settingsStateService, chatThreadsStateService, refreshModelService, themeService, editCodeService } = stateServices + const { uriStateService, sidebarStateService, settingsStateService, chatThreadsStateService, refreshModelService, themeService, editCodeService } = stateServices uriState = uriStateService.state disposables.push( @@ -113,14 +107,6 @@ export const _registerServices = (accessor: ServicesAccessor) => { }) ) - quickEditState = quickEditStateService.state - disposables.push( - quickEditStateService.onDidChangeState(() => { - quickEditState = quickEditStateService.state - quickEditStateListeners.forEach(l => l(quickEditState)) - }) - ) - sidebarState = sidebarStateService.state disposables.push( sidebarStateService.onDidChangeState(() => { @@ -206,7 +192,6 @@ const getReactAccessor = (accessor: ServicesAccessor) => { IVoidSettingsService: accessor.get(IVoidSettingsService), IEditCodeService: accessor.get(IEditCodeService), IVoidUriStateService: accessor.get(IVoidUriStateService), - IQuickEditStateService: accessor.get(IQuickEditStateService), ISidebarStateService: accessor.get(ISidebarStateService), IChatThreadService: accessor.get(IChatThreadService), @@ -265,16 +250,6 @@ export const useUriState = () => { return s } -export const useQuickEditState = () => { - const [s, ss] = useState(quickEditState) - useEffect(() => { - ss(quickEditState) - quickEditStateListeners.add(ss) - return () => { quickEditStateListeners.delete(ss) } - }, [ss]) - return s -} - export const useSidebarState = () => { const [s, ss] = useState(sidebarState) useEffect(() => { diff --git a/src/vs/workbench/contrib/void/browser/void.contribution.ts b/src/vs/workbench/contrib/void/browser/void.contribution.ts index 559c15dc..c69e56af 100644 --- a/src/vs/workbench/contrib/void/browser/void.contribution.ts +++ b/src/vs/workbench/contrib/void/browser/void.contribution.ts @@ -40,6 +40,9 @@ import './terminalToolService.js' // register Thread History import './chatThreadService.js' +// ping +import './metricsPollService.js' + // ---------- common (unclear if these actually need to be imported, because they're already imported wherever they're used) ---------- diff --git a/src/vs/workbench/contrib/void/browser/voidUpdateActions.ts b/src/vs/workbench/contrib/void/browser/voidUpdateActions.ts index d3e92f03..4296c358 100644 --- a/src/vs/workbench/contrib/void/browser/voidUpdateActions.ts +++ b/src/vs/workbench/contrib/void/browser/voidUpdateActions.ts @@ -87,7 +87,7 @@ class VoidUpdateWorkbenchContribution extends Disposable implements IWorkbenchCo // check every 3 hours const { window } = dom.getActiveWindow() - const intervalId = window.setInterval(() => autoCheck(), 3 * 60 * 60 * 1000) + const intervalId = window.setInterval(() => autoCheck(), 3 * 60 * 60 * 1000) // every 3 hrs this._register({ dispose: () => window.clearInterval(intervalId) }) } From c1e83e458b3c5caa04ee0bd9cc798c1ddf664390 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sat, 15 Mar 2025 03:42:21 -0700 Subject: [PATCH 066/173] header uri name --- .../browser/react/src/markdown/ApplyBlockHoverButtons.tsx | 4 ++-- .../void/browser/react/src/sidebar-tsx/SidebarChat.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) 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 37977c40..3c19866a 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 @@ -5,7 +5,7 @@ import { isFeatureNameDisabled } from '../../../../common/voidSettingsTypes.js' import { URI } from '../../../../../../../base/common/uri.js' import { LucideIcon, RotateCw } from 'lucide-react' import { Check, X, Square, Copy, Play, } from 'lucide-react' -import { ToolContentsWrapper } from '../sidebar-tsx/SidebarChat.js' +import { getBasename, ToolContentsWrapper } from '../sidebar-tsx/SidebarChat.js' import { ChatMarkdownRender } from './ChatMarkdownRender.js' enum CopyButtonText { @@ -288,7 +288,7 @@ export const BlockCodeApplyWrapper = ({
    {statusIndicatorHTML} - {language || 'text'} + {uri !== 'current' ? getBasename(uri.fsPath) : language || 'text'}
    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 c1d394b3..710df58e 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 @@ -491,7 +491,7 @@ const ScrollToBottomContainer = ({ children, className, style, scrollContainerRe }; -const getBasename = (pathStr: string) => { +export const getBasename = (pathStr: string) => { // 'unixify' path pathStr = pathStr.replace(/[/\\]+/g, '/') // replace any / or \ or \\ with / const parts = pathStr.split('/') // split on / From a4d93ac95a14e00559a92f9050dfbfbe93cb73b3 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sat, 15 Mar 2025 04:19:30 -0700 Subject: [PATCH 067/173] chatMode and agent mode UI --- .../contrib/void/browser/chatThreadService.ts | 4 +- .../react/src/sidebar-tsx/SidebarChat.tsx | 60 ++++++++++++++++--- .../void/browser/react/src/util/inputs.tsx | 20 ++++--- .../contrib/void/common/voidSettingsTypes.ts | 2 + 4 files changed, 69 insertions(+), 17 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index caad3b73..82abaebb 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -540,11 +540,13 @@ class ChatThreadService extends Disposable implements IChatThreadService { // CAN THROW ERRORS approveTool(toolId: string) { + const chatMode = this._settingsService.state.globalSettings.chatMode + // if not streaming, approveToolAndStreamResponse const threadId = this.getCurrentThread().id const isStreaming = !!this.streamState[threadId]?.streamingToken if (!isStreaming) { - this._approveToolAndStreamResponse_NotStreamingNow({ chatMode: 'agent' }) + this._approveToolAndStreamResponse_NotStreamingNow({ chatMode }) } else { const resRej = this.resRejOfToolAwaitingApproval[toolId] 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 710df58e..c87fb3e3 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 @@ -17,13 +17,13 @@ import { ChatMarkdownRender, ChatMessageLocation, getApplyBoxId } from '../markd import { URI } from '../../../../../../../base/common/uri.js'; import { IDisposable } from '../../../../../../../base/common/lifecycle.js'; import { ErrorDisplay } from './ErrorDisplay.js'; -import { TextAreaFns, VoidInputBox2, VoidSlider, VoidSwitch } from '../util/inputs.js'; +import { TextAreaFns, VoidCustomDropdownBox, VoidInputBox2, VoidSlider, VoidSwitch } from '../util/inputs.js'; import { ModelDropdown, } from '../void-settings-tsx/ModelDropdown.js'; import { SidebarThreadSelector } from './SidebarThreadSelector.js'; import { useScrollbarStyles } from '../util/useScrollbarStyles.js'; import { VOID_CTRL_L_ACTION_ID } from '../../../actionIDs.js'; import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js'; -import { FeatureName, isFeatureNameDisabled } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'; +import { ChatMode, FeatureName, isFeatureNameDisabled } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'; import { WarningBox } from '../void-settings-tsx/WarningBox.js'; import { getModelSelectionState, getModelCapabilities } from '../../../../common/modelCapabilities.js'; import { AlertTriangle, ChevronRight, Dot, Pencil, X } from 'lucide-react'; @@ -206,7 +206,7 @@ const getChatBubbleId = (threadId: string, messageIdx: number) => `${threadId}-$ // SLIDER ONLY: -const ReasoningOptionDropdown = ({ featureName }: { featureName: FeatureName }) => { +const ReasoningOptionSlider = ({ featureName }: { featureName: FeatureName }) => { const accessor = useAccessor() const voidSettingsService = accessor.get('IVoidSettingsService') @@ -264,6 +264,46 @@ const ReasoningOptionDropdown = ({ featureName }: { featureName: FeatureName }) +const nameOfChatMode = { + 'chat': 'Chat', + 'gather': 'Gather', + 'agent': 'Agent', +} + +const detailOfChatMode = { + 'chat': 'Chat only', + 'gather': 'Read files', + 'agent': 'Read and edit files', +} + + +const ChatModeDropdown = ({ className }: { className: string }) => { + const accessor = useAccessor() + + const voidSettingsService = accessor.get('IVoidSettingsService') + const voidSettingsState = useSettingsState() + + const options: ChatMode[] = useMemo(() => ['chat', 'gather', 'agent'], []) + + const onChangeOption = useCallback((newVal: ChatMode) => { + voidSettingsService.setGlobalSetting('chatMode', newVal) + }, [voidSettingsService]) + + return nameOfChatMode[val]} + getOptionDropdownName={(val) => nameOfChatMode[val]} + getOptionDropdownDetail={(val) => detailOfChatMode[val]} + getOptionsEqual={(a, b) => a === b} + /> + +} + + + interface VoidChatAreaProps { @@ -363,9 +403,13 @@ export const VoidChatArea: React.FC = ({ {/* Bottom row */}
    {showModelDropdown && ( -
    - - +
    + + +
    + + +
    )} @@ -1675,8 +1719,10 @@ export const SidebarChat = () => { // getModelCapabilities() // TODO!!! check if can go into agent mode + const chatMode = settingsState.globalSettings.chatMode + try { - await chatThreadsService.addUserMessageAndStreamResponse({ userMessage, chatMode: 'agent' }) + await chatThreadsService.addUserMessageAndStreamResponse({ userMessage, chatMode }) } catch (e) { console.error('Error while sending message in chat:', e) } 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 770df495..ed328dc4 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 @@ -316,18 +316,18 @@ export const VoidSlider = ({ {/* Track */}
    {/* Filled part of track */}
    @@ -471,7 +471,8 @@ export const VoidCustomDropdownBox = >({ className, arrowTouchesText = true, matchInputWidth = false, - gap = 0, + gapPx = 0, + offsetPx = -6, }: { options: T[]; selectedOption: T | undefined; @@ -483,7 +484,8 @@ export const VoidCustomDropdownBox = >({ className?: string; arrowTouchesText?: boolean; matchInputWidth?: boolean; - gap?: number; + gapPx?: number; + offsetPx?:number; }) => { const [isOpen, setIsOpen] = useState(false); const measureRef = useRef(null); @@ -502,7 +504,7 @@ export const VoidCustomDropdownBox = >({ placement: 'bottom-start', middleware: [ - offset(gap), + offset({ mainAxis: gapPx, crossAxis: offsetPx }), flip({ boundary: document.body, padding: 8 diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index f5516ff3..b0c2c618 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -387,6 +387,7 @@ export type GlobalSettings = { enableAutocomplete: boolean; syncApplyToChat: boolean; enableFastApply: boolean; + chatMode: ChatMode; } export const defaultGlobalSettings: GlobalSettings = { @@ -395,6 +396,7 @@ export const defaultGlobalSettings: GlobalSettings = { enableAutocomplete: false, syncApplyToChat: true, enableFastApply: true, + chatMode: 'agent', } export type GlobalSettingName = keyof GlobalSettings From 824de11f21d7b675ebbf0e7bf8d12bb8907b10eb Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sat, 15 Mar 2025 04:21:01 -0700 Subject: [PATCH 068/173] setting --- .../void/browser/react/src/void-settings-tsx/Settings.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 58965f9f..3ab75258 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 @@ -505,7 +505,7 @@ export const FeaturesTab = () => { value={voidSettingsState.globalSettings.syncApplyToChat} onChange={(newVal) => voidSettingsService.setGlobalSetting('syncApplyToChat', newVal)} /> - {voidSettingsState.globalSettings.syncApplyToChat ? 'Sync with Chat model' : 'Use another model'} + {voidSettingsState.globalSettings.syncApplyToChat ? 'Same as Chat model' : 'Different model'}
    {/* Model Dropdown */} From aca5deb8141f58ac93dc14536769ca3f33306491 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sat, 15 Mar 2025 04:24:39 -0700 Subject: [PATCH 069/173] desc --- .../void/browser/react/src/sidebar-tsx/SidebarChat.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 c87fb3e3..ff0c2f41 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 @@ -271,9 +271,9 @@ const nameOfChatMode = { } const detailOfChatMode = { - 'chat': 'Chat only', - 'gather': 'Read files', - 'agent': 'Read and edit files', + 'chat': 'Normal chat', + 'gather': 'Read and search only', + 'agent': 'Full tool use', } From c6d246f783f68af94e8aae7a1796800cbde48d05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Commaret?= Date: Sat, 15 Mar 2025 16:07:33 +0100 Subject: [PATCH 070/173] main branch conflicts fixed --- .../contrib/void/common/modelCapabilities.ts | 43 --------------- .../contrib/void/common/voidSettingsTypes.ts | 6 -- .../llmMessage/sendLLMMessage.impl.ts | 55 ++----------------- 3 files changed, 5 insertions(+), 99 deletions(-) diff --git a/src/vs/workbench/contrib/void/common/modelCapabilities.ts b/src/vs/workbench/contrib/void/common/modelCapabilities.ts index fe8e8b5d..25587fc9 100644 --- a/src/vs/workbench/contrib/void/common/modelCapabilities.ts +++ b/src/vs/workbench/contrib/void/common/modelCapabilities.ts @@ -132,15 +132,6 @@ const mistralModelOptions = { supportsTools: 'openai-style', supportsReasoning: false, }, - 'open-codestral-mamba': { - contextWindow: 32_000, - maxOutputTokens: 4_096, - cost: { input: 0.00, output: 0.00 }, - supportsFIM: true, - supportsSystemMessage: 'system-role', - supportsTools: 'openai-style', - supportsReasoning: false, - }, 'mistral-large-latest': { contextWindow: 32_000, maxOutputTokens: 4_096, @@ -209,12 +200,6 @@ const openSourceModelOptions_assumingOAICompat = { supportsTools: 'openai-style', supportsReasoning: { canToggleReasoning: false, canIOReasoning: true, openSourceThinkTags: ['', ''] }, }, - 'mistral-large-latest': { - supportsFIM: false, - supportsSystemMessage: 'system-role', - supportsTools: 'openai-style', - supportsReasoning: false, - }, // FIM only 'starcoder2': { supportsFIM: true, @@ -222,13 +207,6 @@ const openSourceModelOptions_assumingOAICompat = { supportsTools: false, supportsReasoning: false, }, - // Mistral - 'codestral-latest': { - supportsFIM: true, - supportsSystemMessage: 'system-role', - supportsTools: 'openai-style', - supportsReasoning: false, - }, 'codegemma:2b': { supportsFIM: true, supportsSystemMessage: false, @@ -391,26 +369,6 @@ const openAISettings: ProviderSettings = { } } -const mistralModelOptions = { - 'codestral-latest': { - contextWindow: 32_000, - maxOutputTokens: 4_096, - cost: { input: 0.00, output: 0.00 }, - supportsFIM: true, - supportsSystemMessage: 'system-role', - supportsTools: 'openai-style', - supportsReasoning: false, - }, - 'mistral-large-latest': { - contextWindow: 32_000, - maxOutputTokens: 4_096, - cost: { input: 0.00, output: 0.00 }, - supportsFIM: false, - supportsSystemMessage: 'system-role', - supportsTools: 'openai-style', - supportsReasoning: false, - } -} as const satisfies { [s: string]: ModelOptions } @@ -668,7 +626,6 @@ const modelSettingsOfProvider: { [providerName in ProviderName]: ProviderSetting anthropic: anthropicSettings, xAI: xAISettings, gemini: geminiSettings, - mistral: mistralSettings, // open source models deepseek: deepseekSettings, groq: groqSettings, diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index 0f3e50e6..79d2204b 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -316,12 +316,6 @@ export const defaultSettingsOfProvider: SettingsOfProvider = { ...modelInfoOfDefaultModelNames(defaultModelsOfProvider.vLLM), _didFillInProviderSettings: undefined, }, - mistral: { // aggregator - ...defaultCustomSettings, - ...defaultProviderSettings.mistral, - ...modelInfoOfDefaultModelNames(defaultModelsOfProvider.mistral), - _didFillInProviderSettings: undefined, - }, } 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 24afe0c5..bd4c3da4 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 @@ -153,51 +153,6 @@ const _sendOpenAICompatibleFIM = ({ messages: messages_, onFinalMessage, onError }) } - -const _sendMistralFIM = ({ messages: messages_, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, aiInstructions }: SendFIMParams_Internal) => { - const { modelName, supportsFIM } = getModelCapabilities(providerName, modelName_) - if (!supportsFIM) { - if (modelName === modelName_) - onError({ message: `Model ${modelName} does not support FIM.`, fullError: null }) - else - onError({ message: `Model ${modelName_} (${modelName}) does not support FIM.`, fullError: null }) - return - } - const messages = prepareFIMMessage({ messages: messages_, aiInstructions }) - - const mistral = new MistralCore({ apiKey: settingsOfProvider.mistral.apiKey }) - - // DEBUG : request params - // console.log('🔍 Sending FIM request with params:', { - // model: modelName, - // promptLength: messages.prefix.length, - // suffixLength: messages.suffix.length, - // stream: false, - // maxTokens: messages.maxTokens - //}); - - fimComplete( - mistral, { - model: modelName, - prompt: messages.prefix, - suffix: messages.suffix, - stream: false, - topP: 1, - maxTokens: messages.maxTokens, - stop: messages.stopTokens - }, - ) - .then(async response => { - const fullText = response.choices[0]?.text || ''; - onFinalMessage({ fullText, }); - // console.log('✅ Réponse FIM reçue:', fullText); - - }) - .catch(error => { - onError({ message: error + '', fullError: error }); - }) -} - const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, aiInstructions, modelSelectionOptions, tools: tools_ }: SendChatParams_Internal) => { const { modelName, @@ -512,7 +467,7 @@ const sendOllamaFIM = ({ messages: messages_, onFinalMessage, onError, settingsO } //////// MISTRAL //////// -const sendMistralChat = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, aiInstructions, modelSelectionOptions }: SendChatParams_Internal) => { +const _sendMistralChat = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, aiInstructions, modelSelectionOptions }: SendChatParams_Internal) => { _sendOpenAICompatibleChat({ messages: messages_, onText, @@ -527,7 +482,7 @@ const sendMistralChat = ({ messages: messages_, onText, onFinalMessage, onError, }); } -const sendMistralFIM = ({ messages: messages_, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, aiInstructions, modelSelectionOptions }: SendFIMParams_Internal) => { +const _sendMistralFIM = ({ messages: messages_, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, aiInstructions, modelSelectionOptions }: SendFIMParams_Internal) => { const { modelName, supportsFIM } = getModelCapabilities(providerName, modelName_) if (!supportsFIM) { if (modelName === modelName_) @@ -614,8 +569,8 @@ export const sendLLMMessageToProviderImplementation = { list: null, }, mistral: { - sendChat: (params) => sendMistralChat(params), - sendFIM: (params) => sendMistralFIM(params), + sendChat: (params) => _sendMistralChat(params), + sendFIM: (params) => _sendMistralFIM(params), list: null, }, -} satisfies CallFnOfProvider \ No newline at end of file +} satisfies CallFnOfProvider From 7b76df235a590456c847bad4414ea1fcd4261639 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sat, 15 Mar 2025 16:00:03 -0700 Subject: [PATCH 071/173] reasoning component when streaming --- .../src/markdown/ApplyBlockHoverButtons.tsx | 2 +- .../react/src/markdown/ChatMarkdownRender.tsx | 4 +- .../react/src/sidebar-tsx/SidebarChat.tsx | 119 ++++++------------ 3 files changed, 42 insertions(+), 83 deletions(-) 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 3c19866a..485a0937 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 @@ -280,7 +280,7 @@ export const BlockCodeApplyWrapper = ({ const { statusIndicatorHTML, buttonsHTML } = useApplyButtonHTML({ codeStr: initValue, applyBoxId, uri }) return
    {/* header */} diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index f21bef23..b09b163f 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -278,13 +278,13 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, .. token={token} tokenIdx={`${tokenIdx ? `${tokenIdx}-` : ''}${index}`} // assign a unique tokenId to inPTag components chatMessageLocation={chatMessageLocation} - inPTag={true} // TODO!!! check this + inPTag={true} {...options} /> ))} - if (inPTag) return contents + if (inPTag) return {contents} return

    {contents} 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 ff0c2f41..433378ea 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 @@ -149,62 +149,6 @@ const getChatBubbleId = (threadId: string, messageIdx: number) => `${threadId}-$ - -// const ReasoningOptionDropdown = () => { -// const accessor = useAccessor() - -// const voidSettingsService = accessor.get('IVoidSettingsService') -// const voidSettingsState = useSettingsState() - -// const modelSelection = voidSettingsState.modelSelectionOfFeature['Chat'] -// if (!modelSelection) return null - -// const { modelName, providerName } = modelSelection -// const { canToggleReasoning, reasoningBudgetSlider } = getModelCapabilities(providerName, modelName).supportsReasoningOutput || {} - -// const defaultEnabledVal = canToggleReasoning ? true : false -// const isEnabled = voidSettingsState.optionsOfModelSelection[modelSelection.providerName]?.[modelSelection.modelName]?.reasoningEnabled ?? defaultEnabledVal - -// let toggleButton: React.ReactNode = null -// if (canToggleReasoning) { -// toggleButton =

    -// {isEnabled ? 'Thinking' : 'Thinking'} -// { voidSettingsService.setOptionsOfModelSelection(modelSelection.providerName, modelSelection.modelName, { reasoningEnabled: newVal }) }} -// /> -//
    -// } - -// let slider: React.ReactNode = null -// if (isEnabled && reasoningBudgetSlider?.type === 'slider') { -// const { min, max, default: defaultVal } = reasoningBudgetSlider -// const value = voidSettingsState.optionsOfModelSelection[modelSelection.providerName]?.[modelSelection.modelName]?.reasoningBudget ?? defaultVal -// slider =
    -// Budget -// { voidSettingsService.setOptionsOfModelSelection(modelSelection.providerName, modelSelection.modelName, { reasoningBudget: newVal }) }} -// /> -// {`${value} tokens`} -//
    - -// } - -// return <> -// {toggleButton} -// {slider} -// -// } - - - // SLIDER ONLY: const ReasoningOptionSlider = ({ featureName }: { featureName: FeatureName }) => { const accessor = useAccessor() @@ -705,24 +649,17 @@ export const SelectedFiles = ( -const ReasoningComponent = ({ children }: { children: React.ReactNode }) => { - - const [isOpen, setIsOpen] = useState(false) - return children - -} - - type ToolHeaderParams = { icon?: React.ReactNode; title: string; - desc1: string; + desc1: React.ReactNode; desc2?: React.ReactNode; isError?: boolean; numResults?: number; children?: React.ReactNode; onClick?: () => void; + isOpen?: boolean, } const ToolHeaderWrapper = ({ @@ -734,9 +671,11 @@ const ToolHeaderWrapper = ({ children, isError, onClick, + isOpen, }: ToolHeaderParams) => { - const [isExpanded, setIsExpanded] = useState(false); + const [isExpanded_, setIsExpanded] = useState(false); + const isExpanded = isOpen ? isOpen : isExpanded_ const isDropdown = children !== undefined // null ALLOWS dropdown const isClickable = !!(isDropdown || onClick) @@ -780,7 +719,8 @@ const ToolHeaderWrapper = ({ {/* children */} {
    {children} @@ -994,6 +934,7 @@ const AssistantMessageComponent = ({ chatMessage, isLoading, messageIdx, isLast const reasoningStr = chatMessage.reasoning?.trim() || null const hasReasoning = !!reasoningStr + const isDoneReasoning = !!chatMessage.content const thread = chatThreadsService.getCurrentThread() @@ -1035,14 +976,15 @@ const AssistantMessageComponent = ({ chatMessage, isLoading, messageIdx, isLast > {/* reasoning token */} - {hasReasoning && + {hasReasoning && - } + } {/* assistant message */} { return
    -
    +
    {children}
    @@ -1203,7 +1145,7 @@ const EditToolChildren = ({ uri, changeDescription }: { uri: URI, changeDescript { commandService.executeCommand('vscode.open', uri, { preview: true }) }} />
    @@ -1227,7 +1169,7 @@ const TerminalToolChildren = ({ command, terminalId, result, resolveReason }: { terminalToolsService.openTerminal(terminalId)} />
    @@ -1241,6 +1183,23 @@ const TerminalToolChildren = ({ command, terminalId, result, resolveReason }: { } + +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(isWriting) // if just finished reasoning, close + }, [isWriting]) + return : ''} isOpen={isOpen}> + +
    + {children} +
    +
    +
    +} + const toolNameToComponent: { [T in ToolName]: { requestWrapper: T extends ToolNameWithApproval ? ((props: { toolRequest: ToolRequestApproval }) => React.ReactNode) : null, resultWrapper: (props: { toolMessage: ToolMessage, messageIdx: number }) => React.ReactNode, @@ -1296,14 +1255,14 @@ const toolNameToComponent: { [T in ToolName]: { : {value.children.map((child, i) => ( { commandService.executeCommand('workbench.view.explorer'); explorerService.select(child.uri, true); }} />))} {value.hasNextPage && - + } } @@ -1337,11 +1296,11 @@ const toolNameToComponent: { [T in ToolName]: { : {value.uris.map((uri, i) => ( { commandService.executeCommand('vscode.open', uri, { preview: true }) }} />))} {value.hasNextPage && - + } @@ -1376,11 +1335,11 @@ const toolNameToComponent: { [T in ToolName]: { : {value.uris.map((uri, i) => ( { commandService.executeCommand('vscode.open', uri, { preview: true }) }} />))} {value.hasNextPage && - + } @@ -1611,7 +1570,7 @@ const toolNameToComponent: { [T in ToolName]: { type ChatBubbleMode = 'display' | 'edit' -type ChatBubbleProps = { chatMessage: ChatMessage, messageIdx: number, isLoading?: boolean, isLast: boolean } +type ChatBubbleProps = { chatMessage: ChatMessage, messageIdx: number, isLoading: boolean, isLast: boolean } const ChatBubble = ({ chatMessage, isLoading, messageIdx, isLast }: ChatBubbleProps) => { @@ -1753,7 +1712,7 @@ export const SidebarChat = () => { const previousMessagesHTML = useMemo(() => { return previousMessages.map((message, i) => - + ) }, [previousMessages, currentThread, numMessages]) From a99d1c99a561413c17a6eae77a33462cd16fb310 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sat, 15 Mar 2025 16:23:14 -0700 Subject: [PATCH 072/173] agent mode must use tool models only --- .../src/void-settings-tsx/ModelDropdown.tsx | 6 ++--- .../void/common/voidSettingsService.ts | 27 ++++++++++--------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/ModelDropdown.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/ModelDropdown.tsx index 42f4de23..813968a4 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/ModelDropdown.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/ModelDropdown.tsx @@ -86,7 +86,7 @@ const MemoizedModelDropdown = ({ featureName, className }: { featureName: Featur useEffect(() => { const oldOptions = oldOptionsRef.current - const newOptions = settingsState._modelOptions.filter((o) => filter(o.selection)) + const newOptions = settingsState._modelOptions.filter((o) => filter(o.selection, { chatMode: settingsState.globalSettings.chatMode })) if (!optionsEqual(oldOptions, newOptions)) { setMemoizedOptions(newOptions) @@ -95,7 +95,7 @@ const MemoizedModelDropdown = ({ featureName, className }: { featureName: Featur }, [settingsState._modelOptions, filter]) if (memoizedOptions.length === 0) { // Pretty sure this will never be reached unless filter is enabled - return + return } return @@ -116,7 +116,7 @@ export const ModelDropdown = ({ featureName, className }: { featureName: Feature const isDisabled = isFeatureNameDisabled(featureName, settingsState) if (isDisabled) return boolean; emptyMessage: string | null } } = { - 'Autocomplete': { filter: o => getModelCapabilities(o.providerName, o.modelName).supportsFIM, emptyMessage: 'No models support FIM' }, - 'Chat': { filter: o => true, emptyMessage: null }, - 'Ctrl+K': { filter: o => true, emptyMessage: null }, - 'Apply': { filter: o => true, emptyMessage: null }, +export const modelFilterOfFeatureName: { [featureName in FeatureName]: { filter: (o: ModelSelection, opts: { chatMode: ChatMode }) => boolean; emptyMessage: null | { message: string, priority: 'always' | 'fallback' } } } = { + 'Autocomplete': { filter: (o) => getModelCapabilities(o.providerName, o.modelName).supportsFIM, emptyMessage: { message: 'No models support FIM', priority: 'always' } }, + 'Chat': { filter: (o, { chatMode }) => chatMode === 'chat' ? true : !!getModelCapabilities(o.providerName, o.modelName).supportsTools, emptyMessage: { message: 'No models support tool use', priority: 'fallback' } }, + 'Ctrl+K': { filter: o => true, emptyMessage: null, }, + 'Apply': { filter: o => true, emptyMessage: null, }, } -const _validatedState = (state: Omit) => { +const _validatedModelState = (state: Omit) => { let newSettingsOfProvider = state.settingsOfProvider @@ -143,7 +143,8 @@ const _validatedState = (state: Omit) => { for (const featureName of featureNames) { const { filter } = modelFilterOfFeatureName[featureName] - const modelOptionsForThisFeature = newModelOptions.filter((o) => filter(o.selection)) + const filterOpts = { chatMode: state.globalSettings.chatMode } + const modelOptionsForThisFeature = newModelOptions.filter((o) => filter(o.selection, filterOpts)) const modelSelectionAtFeature = newModelSelectionOfFeature[featureName] const selnIdx = modelSelectionAtFeature === null ? -1 : modelOptionsForThisFeature.findIndex(m => modelSelectionsEqual(m.selection, modelSelectionAtFeature)) @@ -218,7 +219,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { // the stored data structure might be outdated, so we need to update it here const finalState = readS - this.state = _validatedState(finalState); + this.state = _validatedModelState(finalState); this._resolver(); this._onDidChangeState.fire(); @@ -265,7 +266,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { globalSettings: newGlobalSettings, } - this.state = _validatedState(newState) + this.state = _validatedModelState(newState) await this._storeState() this._onDidChangeState.fire() @@ -287,7 +288,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { [settingName]: newVal } } - this.state = newState + this.state = _validatedModelState(newState) await this._storeState() this._onDidChangeState.fire() @@ -305,7 +306,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { } } - this.state = newState + this.state = _validatedModelState(newState) await this._storeState() this._onDidChangeState.fire() @@ -331,7 +332,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { } } } - this.state = newState + this.state = _validatedModelState(newState) await this._storeState() this._onDidChangeState.fire() From 47e93d1cf3d50b56016e139b0e9f6b9cab0ae037 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sat, 15 Mar 2025 17:00:21 -0700 Subject: [PATCH 073/173] fix up settings --- .../void/browser/react/src/util/inputs.tsx | 2 +- .../react/src/void-settings-tsx/Settings.tsx | 88 ++++++++++--------- 2 files changed, 46 insertions(+), 44 deletions(-) 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 ed328dc4..6ac3d712 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 @@ -925,7 +925,7 @@ export const VoidCodeEditor = ({ initValue, language, maxHeight, showScrollbars export const VoidButton = ({ children, disabled, onClick }: { children: React.ReactNode; disabled?: boolean; onClick: () => void }) => { return } 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 3ab75258..7fa7d183 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 @@ -18,13 +18,12 @@ import { ModelDropdown } from './ModelDropdown.js' import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js' import { WarningBox } from './WarningBox.js' import { os } from '../../../../common/helpers/systemInfo.js' +import { IconX } from '../sidebar-tsx/SidebarChat.js' -const SubtleButton = ({ onClick, text, icon, disabled }: { onClick: () => void, text: string, icon: React.ReactNode, disabled: boolean }) => { +const ButtonLeftTextRightOption = ({ text, leftButton }: { text: string, leftButton?: React.ReactNode }) => { return
    - + {leftButton ? leftButton : null} {text} @@ -57,22 +56,28 @@ const RefreshModelButton = ({ providerName }: { providerName: RefreshableProvide const { state } = refreshModelState[providerName] const { title: providerTitle } = displayInfoOfProviderName(providerName) - return { - refreshModelService.startRefreshingModels(providerName, { enableProviderOnSuccess: false, doNotFire: false }) - metricsService.capture('Click', { providerName, action: 'Refresh Models' }) - }} - text={justFinished === 'finished' ? `${providerTitle} Models are up-to-date!` - : justFinished === 'error' ? `${providerTitle} not found!` - : `Manually refresh ${providerTitle} models.` - } - icon={justFinished === 'finished' ? - : justFinished === 'error' ? - : state === 'refreshing' ? - : + + return { + refreshModelService.startRefreshingModels(providerName, { enableProviderOnSuccess: false, doNotFire: false }) + metricsService.capture('Click', { providerName, action: 'Refresh Models' }) + }} + > + {justFinished === 'finished' ? + : justFinished === 'error' ? + : state === 'refreshing' ? + : } + } - disabled={state === 'refreshing' || justFinished !== null} + text={justFinished === 'finished' ? `${providerTitle} Models are up-to-date!` + : justFinished === 'error' ? `${providerTitle} not found!` + : `Manually refresh ${providerTitle} models.`} /> } @@ -93,7 +98,7 @@ const RefreshableModels = () => { -const AddModelMenu = ({ onSubmit }: { onSubmit: () => void }) => { +const AddModelMenu = ({ onSubmit, onClose }: { onSubmit: () => void, onClose: () => void }) => { const accessor = useAccessor() const settingsStateService = accessor.get('IVoidSettingsService') @@ -116,8 +121,8 @@ const AddModelMenu = ({ onSubmit }: { onSubmit: () => void }) => { options={providerNames} selectedOption={providerName} onChangeOption={(pn) => setProviderName(pn)} - getOptionDisplayName={(pn) => pn ? displayInfoOfProviderName(pn).title : '(null)'} - getOptionDropdownName={(pn) => pn ? displayInfoOfProviderName(pn).title : '(null)'} + getOptionDisplayName={(pn) => pn ? displayInfoOfProviderName(pn).title : 'Provider Name'} + getOptionDropdownName={(pn) => pn ? displayInfoOfProviderName(pn).title : 'Provider Name'} getOptionsEqual={(a, b) => a === b} className={`max-w-44 w-full border border-void-border-2 bg-void-bg-1 text-void-fg-3 text-root py-[4px] px-[6px] @@ -141,8 +146,8 @@ const AddModelMenu = ({ onSubmit }: { onSubmit: () => void }) => {
    {/* button */} -
    - { + { const modelName = modelNameRef.current?.value if (providerName === null) { @@ -161,16 +166,16 @@ const AddModelMenu = ({ onSubmit }: { onSubmit: () => void }) => { settingsStateService.addModel(providerName, modelName) onSubmit() - }} - >Add model -
    + >Add model - {!errorString ? null :
    - {errorString} -
    } +
    + {!errorString ? null :
    + {errorString} +
    } + } @@ -178,9 +183,9 @@ const AddModelMenu = ({ onSubmit }: { onSubmit: () => void }) => { const AddModelMenuFull = () => { const [open, setOpen] = useState(false) - return
    + return
    {open ? - { setOpen(false) }} /> + setOpen(false)} onClose={() => setOpen(false)} /> : setOpen(true)}>Add Model }
    @@ -354,7 +359,7 @@ export const VoidProviderSettings = ({ providerNames }: { providerNames: Provide type TabName = 'models' | 'general' -export const AutoRefreshToggle = () => { +export const AutoDetectLocalModelsToggle = () => { const settingName: GlobalSettingName = 'autoRefreshModels' const accessor = useAccessor() @@ -366,19 +371,17 @@ export const AutoRefreshToggle = () => { // right now this is just `enabled_autoRefreshModels` const enabled = voidSettingsState.globalSettings[settingName] - return
    - { voidSettingsService.setGlobalSetting(settingName, newVal) metricsService.capture('Click', { action: 'Autorefresh Toggle', settingName, enabled: newVal }) - }} /> - - - {`Automatically detect local providers and models (${refreshableProviderNames.map(providerName => displayInfoOfProviderName(providerName).title).join(', ')}).`} - -
    + }} + />} + text={`Automatically detect local providers and models (${refreshableProviderNames.map(providerName => displayInfoOfProviderName(providerName).title).join(', ')}).`} + /> } @@ -431,11 +434,10 @@ export const FeaturesTab = () => { return <>

    Models

    - - -
    + + From 3f7f2fb40cc487237258551b02924425a8a9da81 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Sat, 15 Mar 2025 01:19:52 -0700 Subject: [PATCH 074/173] commandbar draft --- .../contrib/void/browser/editCodeService.ts | 64 ++++++- .../void/browser/editCodeServiceInterface.ts | 12 ++ .../react/src/markdown/ChatMarkdownRender.tsx | 4 +- .../react/src/sidebar-tsx/SidebarChat.tsx | 171 ++++++++++++++++++ 4 files changed, 244 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 978016f7..78acce8e 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -177,7 +177,7 @@ type CtrlKZone = { } & CommonZoneProps -type DiffZone = { +export type DiffZone = { type: 'DiffZone', originalCode: string; _diffOfId: Record; // diffid -> diff in this DiffArea @@ -207,7 +207,7 @@ type TrackingZone = { // called DiffArea for historical purposes, we can rename to something like TextRegion if we want -type DiffArea = CtrlKZone | DiffZone | TrackingZone +export type DiffArea = CtrlKZone | DiffZone | TrackingZone const diffAreaSnapshotKeys = [ 'type', @@ -240,16 +240,22 @@ class EditCodeService extends Disposable implements IEditCodeService { // URI <--> model - diffAreasOfURI: Record | undefined> = {} + diffAreasOfURI: Record | undefined> = {}; // uri -> diffareaId - diffAreaOfId: Record = {}; - diffOfId: Record = {}; // redundant with diffArea._diffs + diffAreaOfId: Record = {}; // diffareaId -> diffArea + diffOfId: Record = {}; // diffid -> diff (redundant with diffArea._diffOfId) + _sortedUrisWithDiffs: URI[] = [] // derivative of diffAreaOfId + _sortedDiffsOfFspath: { [uriString: string]: Diff[] | undefined } = {} // derivative of diffAreaOfId // only applies to diffZones // streamingDiffZones: Set = new Set() private readonly _onDidChangeDiffZoneStreaming = new Emitter<{ uri: URI; diffareaid: number }>(); private readonly _onDidAddOrDeleteDiffZones = new Emitter<{ uri: URI }>(); + onDidAddOrDeleteDiffZones = this._onDidAddOrDeleteDiffZones.event; + + private readonly _onDidFinishAddOrDeleteDiffInDiffZone = new Emitter<{ uri: URI }>(); + onDidAddOrDeleteDiffInDiffZone = this._onDidFinishAddOrDeleteDiffInDiffZone.event; private readonly _onDidChangeCtrlKZoneStreaming = new Emitter<{ uri: URI; diffareaid: number }>(); onDidChangeCtrlKZoneStreaming = this._onDidChangeCtrlKZoneStreaming.event @@ -344,6 +350,46 @@ class EditCodeService extends Disposable implements IEditCodeService { for (let editor of this._codeEditorService.listCodeEditors()) { initializeEditor(editor) } this._register(this._codeEditorService.onCodeEditorAdd(editor => { initializeEditor(editor) })) + + // update `_sortedUrisWithDiffs` and `_sortedDiffsOfUri` on all changes to diffzones + this._register(this._onDidFinishAddOrDeleteDiffInDiffZone.event(({ uri }) => { + + + // 1. Update _sortedUrisWithDiffs + const hasDiffZones = Array.from(this.diffAreasOfURI[uri.fsPath] || []) + .some(diffAreaId => this.diffAreaOfId[diffAreaId]?.type === 'DiffZone'); + + // Add or remove this URI from _sortedUrisWithDiffs + const currentIndex = this._sortedUrisWithDiffs.findIndex(u => u.fsPath === uri.fsPath); + if (hasDiffZones && currentIndex === -1) { + // Add URI and maintain sort + this._sortedUrisWithDiffs.push(uri); + this._sortedUrisWithDiffs.sort((a, b) => a.fsPath.localeCompare(b.fsPath)); + } else if (!hasDiffZones && currentIndex !== -1) { + // Remove URI + this._sortedUrisWithDiffs.splice(currentIndex, 1); + } + + // 2. Update _sortedDiffsOfUri only for this URI + const diffsInUri: Diff[] = []; + + // Collect all diffs from DiffZones in this URI + for (const diffAreaId of this.diffAreasOfURI[uri.fsPath] || []) { + const diffArea = this.diffAreaOfId[diffAreaId]; + if (diffArea?.type === 'DiffZone') { + diffsInUri.push(...Object.values(diffArea._diffOfId)); + } + } + + // Update or remove the entry for this URI + if (diffsInUri.length > 0) { + this._sortedDiffsOfFspath[uri.fsPath] = diffsInUri.sort((a, b) => a.startLine - b.startLine); + console.log('_sortedDiffsOfFspath', this._sortedDiffsOfFspath) + } else { + delete this._sortedDiffsOfFspath[uri.fsPath]; + } + })); + } @@ -901,6 +947,10 @@ class EditCodeService extends Disposable implements IEditCodeService { if (diffArea.type !== 'DiffZone') return delete diffArea._diffOfId[diff.diffid] delete this.diffOfId[diff.diffid] + + if (!diffArea._streamState.isStreaming) { + this._onDidFinishAddOrDeleteDiffInDiffZone.fire({ uri: diffArea._URI }) + } } private _deleteDiffs(diffZone: DiffZone) { @@ -993,6 +1043,10 @@ class EditCodeService extends Disposable implements IEditCodeService { this.diffOfId[diffid] = newDiff diffZone._diffOfId[diffid] = newDiff + if (!diffZone._streamState.isStreaming) { + this._onDidFinishAddOrDeleteDiffInDiffZone.fire({ uri }) + } + return newDiff } diff --git a/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts b/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts index e9b1f49e..7fa25ce5 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts @@ -7,6 +7,7 @@ import { Event } from '../../../../base/common/event.js'; import { URI } from '../../../../base/common/uri.js'; import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; +import { Diff, DiffArea } from './editCodeService.js'; @@ -36,13 +37,24 @@ export const IEditCodeService = createDecorator('editCodeServi export interface IEditCodeService { readonly _serviceBrand: undefined; + // main entrypoints (initialize things for the functions below to be called): startApplying(opts: StartApplyingOpts): Promise<[URI, Promise] | null>; + _sortedUrisWithDiffs: URI[]; + _sortedDiffsOfFspath: { [fsPath: string]: Diff[] | undefined }; + + diffAreaOfId: Record; + diffOfId: Record; + + addCtrlKZone(opts: AddCtrlKOpts): number | undefined; removeCtrlKZone(opts: { diffareaid: number }): void; removeDiffAreas(opts: { uri: URI, removeCtrlKs: boolean, behavior: 'reject' | 'accept' }): void; + onDidAddOrDeleteDiffZones: Event<{ uri: URI }>; + onDidAddOrDeleteDiffInDiffZone: Event<{ uri: URI }>; + // CtrlKZone streaming state isCtrlKZoneStreaming(opts: { diffareaid: number }): boolean; interruptCtrlKStreaming(opts: { diffareaid: number }): void; diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index b09b163f..31ad9784 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -41,7 +41,7 @@ const CodespanWithLink = ({ text, rawText, chatMessageLocation }: { text: string const accessor = useAccessor() const chatThreadService = accessor.get('IChatThreadService') - const commandSerivce = accessor.get('ICommandService') + const commandService = accessor.get('ICommandService') const editorService = accessor.get('ICodeEditorService') const { messageIdx, threadId } = chatMessageLocation @@ -73,7 +73,7 @@ const CodespanWithLink = ({ text, rawText, chatMessageLocation }: { text: string const selection = link.selection // open the file - commandSerivce.executeCommand('vscode.open', link.uri).then(() => { + commandService.executeCommand('vscode.open', link.uri).then(() => { // select the text setTimeout(() => { 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 433378ea..2d011a36 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 @@ -32,6 +32,8 @@ import { ResolveReason, ToolCallParams, ToolName, ToolNameWithApproval } from '. import { getLanguageFromModel } from '../../../../common/helpers/getLanguage.js'; import { dirname } from '../../../../../../../base/common/resources.js'; import { useApplyButtonHTML } from '../markdown/ApplyBlockHoverButtons.js'; +import { DiffZone } from '../../../editCodeService.js'; +import { ScrollType } from '../../../../../../../editor/common/editorCommon.js'; @@ -1609,6 +1611,173 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx, isLast }: ChatBubblePr } +const VoidCommandBar = () => { + const accessor = useAccessor() + const editCodeService = accessor.get('IEditCodeService') + const editorService = accessor.get('ICodeEditorService') + const commandService = accessor.get('ICommandService') + + const [_, rerender] = useState(0) + console.log('rerender count: ', _) + + // state for what the user is currently focused on (both URI and diff) + const [diffIdxOfFspath, setDiffIdxOfFspath] = useState>({}) + // const [currentUriIdx, setCurrentUriIdx] = useState(-1) // we are doing O(n) search for this + + const getCurrentUri = useCallback(() => { + const editor = editorService.getActiveCodeEditor() + if (!editor) return null + const uri = editor.getModel()?.uri + if (!uri) return null + return uri + }, [editorService]) + + const diffZones: DiffZone[] = [] + + + // trigger rerender when diffzone is created (TODO need to also update when diff is accepted/rejected) + useEffect(() => { + const disposable = editCodeService.onDidAddOrDeleteDiffInDiffZone(() => { + rerender(c => c + 1) // rerender + }) + return () => disposable.dispose() + }, [editCodeService, rerender]) + + + const getNextDiff = useCallback(({ step }: { step: 1 | -1 }) => { + + const currentUri = getCurrentUri() + + if (!currentUri) { + return; + } + + const sortedDiffs = editCodeService._sortedDiffsOfFspath[currentUri.fsPath] + + if (!sortedDiffs || sortedDiffs.length === 0) { + return; + } + + const currentDiffIdx = diffIdxOfFspath[currentUri.fsPath] || 0 + const nextDiffIdx = (currentDiffIdx + step) % sortedDiffs.length + + const nextDiff = sortedDiffs[nextDiffIdx] + + return { nextDiff, nextDiffIdx, } + + }, [getCurrentUri, editCodeService._sortedDiffsOfFspath, diffIdxOfFspath]) + + const getNextUri = useCallback(({ step }: { step: 1 | -1 }) => { + + const sortedUris = editCodeService._sortedUrisWithDiffs + if (sortedUris.length === 0) { + return; + } + + const currentUri = getCurrentUri() + + const defaultUriIdx = step === 1 ? -1 : 0 // defaults: if next, currentIdx = -1; if prev, currentIdx = 0 + let currentUriIdx = -1 + if (currentUri) { + currentUriIdx = sortedUris.findIndex(u => u.fsPath === currentUri.fsPath) + } + + if (currentUriIdx === -1) { // not found + currentUriIdx = defaultUriIdx // set to default + } + + const nextUriIdx = (currentUriIdx + step) % sortedUris.length + const nextUri = sortedUris[nextUriIdx] + + return { nextUri, nextUriIdx, } + + }, [getCurrentUri, editCodeService._sortedUrisWithDiffs]) + + + + return
    + + + + + +
    +
    +
    numUris: {Object.keys(editCodeService._sortedDiffsOfFspath).length}
    +
    numDiffs: {Object.values(editCodeService._sortedDiffsOfFspath).reduce((acc, diffs) => acc + (diffs?.length || 0), 0)}
    +
    +
    + {diffZones.map((area, index) => ( + <> +
    {getBasename(area?._URI?.toString())}
    + + ))} +
    +} + export const SidebarChat = () => { const textAreaRef = useRef(null) @@ -1805,6 +1974,8 @@ export const SidebarChat = () => { fnsRef={textAreaFnsRef} multiline={true} /> + +
    From 4284461b060f452a0f4a357055ffdecaf0ccc80f Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Sat, 15 Mar 2025 02:47:02 -0700 Subject: [PATCH 075/173] fix height issue --- .../contrib/void/browser/editCodeService.ts | 1 - .../react/src/sidebar-tsx/SidebarChat.tsx | 130 ++++++++++-------- 2 files changed, 73 insertions(+), 58 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 78acce8e..3785310e 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -384,7 +384,6 @@ class EditCodeService extends Disposable implements IEditCodeService { // Update or remove the entry for this URI if (diffsInUri.length > 0) { this._sortedDiffsOfFspath[uri.fsPath] = diffsInUri.sort((a, b) => a.startLine - b.startLine); - console.log('_sortedDiffsOfFspath', this._sortedDiffsOfFspath) } else { delete this._sortedDiffsOfFspath[uri.fsPath]; } 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 2d011a36..4d5e3ac6 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 @@ -1611,6 +1611,11 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx, isLast }: ChatBubblePr } +const VoidCommandBarNavButtonsShell = () => { + +} + + const VoidCommandBar = () => { const accessor = useAccessor() const editCodeService = accessor.get('IEditCodeService') @@ -1618,6 +1623,8 @@ const VoidCommandBar = () => { const commandService = accessor.get('ICommandService') const [_, rerender] = useState(0) + // Add a state variable to track focus + const [isFocused, setIsFocused] = useState(false) console.log('rerender count: ', _) // state for what the user is currently focused on (both URI and diff) @@ -1694,76 +1701,83 @@ const VoidCommandBar = () => { }, [getCurrentUri, editCodeService._sortedUrisWithDiffs]) + const gotoNextDiff = ({ step }: { step: 1 | -1 }) => { - return
    + // get the next diff + const res = getNextDiff({ step: 1 }) + if (!res) return; - + // scroll to the next diff + const { nextDiff, nextDiffIdx } = res; + const editor = editorService.getActiveCodeEditor() + if (!editor) return; - + }) + } + + return
    setIsFocused(true)} + onBlurCapture={() => setIsFocused(false)} + > - // get the next uri - const res = getNextUri({ step: 1 }) - if (!res) return; + - const { nextUri, nextUriIdx } = res; + - // open the uri and scroll to diff - const sortedDiffs = editCodeService._sortedDiffsOfFspath[nextUri.fsPath] - if (!sortedDiffs) return; + - const diffIdx = diffIdxOfFspath[nextUri.fsPath] || 0 - const diff = sortedDiffs[diffIdx] - - const range = { startLineNumber: diff.startLine, endLineNumber: diff.startLine, startColumn: 1, endColumn: 1 }; - - commandService.executeCommand('vscode.open', nextUri).then(() => { - - // select the text - setTimeout(() => { - - const editor = editorService.getActiveCodeEditor() - if (!editor) return; - - editor.revealRange(range, ScrollType.Immediate) - - }, 50) - - }) - }} - > - next diff area -
    numUris: {Object.keys(editCodeService._sortedDiffsOfFspath).length}
    @@ -1908,7 +1922,7 @@ export const SidebarChat = () => {
    const messagesHTML = { onAbort() } }, [onSubmit, onAbort, isStreaming]) - const inputForm =
    0 ? 'absolute bottom-0' : ''}`}> + const inputForm =
    0 ? 'absolute bottom-0' : ''}`}> { fnsRef={textAreaFnsRef} multiline={true} /> - +
    return
    From 0ec9e4a54e69473dfac258aaf3af5cfae6b40891 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Sat, 15 Mar 2025 02:53:10 -0700 Subject: [PATCH 076/173] codespan shorten paths --- .../browser/react/src/markdown/ChatMarkdownRender.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index 31ad9784..da820cdd 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -3,7 +3,7 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import React, { JSX, useState } from 'react' +import React, { JSX, useMemo, useState } from 'react' import { marked, MarkedToken, Token } from 'marked' import { BlockCode } from './BlockCode.js' import { convertToVscodeLang, getFirstLine, getLanguage } from '../../../../common/helpers/getLanguage.js' @@ -11,6 +11,7 @@ import { BlockCodeApplyWrapper, useApplyButtonHTML } from './ApplyBlockHoverButt import { useAccessor } from '../util/services.js' import { ScrollType } from '../../../../../../../editor/common/editorCommon.js' import { URI } from '../../../../../../../base/common/uri.js' +import { getBasename } from '../sidebar-tsx/SidebarChat.js' export type ChatMessageLocation = { @@ -27,11 +28,15 @@ export const getApplyBoxId = ({ threadId, messageIdx, tokenIdx }: ApplyBoxLocati const Codespan = ({ text, className, onClick }: { text: string, className?: string, onClick?: () => void }) => { + // TODO compute this once for efficiency. we should use `labels.ts/shorten` to display duplicates properly + + const filename = useMemo(() => getBasename(text), [text]) + return - {text} + {filename} } From 71dc1ab93e5250bda84afa2ba4ab099d12dd335e Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Sat, 15 Mar 2025 03:06:27 -0700 Subject: [PATCH 077/173] change default setting for tailwind autosuggestions --- src/vs/editor/common/config/editorOptions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 3fdecc87..65d56c06 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -3570,7 +3570,7 @@ class EditorQuickSuggestions extends BaseEditorOption Date: Sat, 15 Mar 2025 04:17:12 -0700 Subject: [PATCH 078/173] loading symbol Cmd+K --- src/vs/base/browser/ui/codicons/codicon/codicon-modifiers.css | 2 +- .../void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/vs/base/browser/ui/codicons/codicon/codicon-modifiers.css b/src/vs/base/browser/ui/codicons/codicon/codicon-modifiers.css index 9666216f..7839fbd8 100644 --- a/src/vs/base/browser/ui/codicons/codicon/codicon-modifiers.css +++ b/src/vs/base/browser/ui/codicons/codicon/codicon-modifiers.css @@ -26,7 +26,7 @@ } /* custom speed & easing for loading icon */ -.codicon-loading, +.codicon-loading:not(.codicon-no-default-spin), /* Void changed this as it is literally broken to the !important */ .codicon-tree-item-loading::before { animation-duration: 1s !important; animation-timing-function: cubic-bezier(0.53, 0.21, 0.29, 0.67) !important; diff --git a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx index de52d120..bb4d72cd 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx @@ -54,6 +54,9 @@ export const QuickEditChat = ({ setIsStreamingRef(isStreaming) }, [diffareaid, setIsStreamingRef])) + const loadingIcon =
    const onSubmit = useCallback(() => { if (isDisabled) return @@ -91,6 +94,7 @@ export const QuickEditChat = ({ onAbort={onInterrupt} onClose={onX} isStreaming={isStreamingRef.current} + loadingIcon={loadingIcon} isDisabled={isDisabled} className="py-2 w-full" onClickAnywhere={() => { textAreaRef.current?.focus() }} From a33160f33ee86bd93add4336e69a6767ebed90b7 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Sat, 15 Mar 2025 04:19:58 -0700 Subject: [PATCH 079/173] loading --- .../react/src/sidebar-tsx/SidebarChat.tsx | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) 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 4d5e3ac6..cc419ba7 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 @@ -268,6 +268,7 @@ interface VoidChatAreaProps { showModelDropdown?: boolean; showSelections?: boolean; showProspectiveSelections?: boolean; + loadingIcon?: React.ReactNode; selections?: StagingSelectionItem[] setSelections?: (s: StagingSelectionItem[]) => void @@ -297,6 +298,7 @@ export const VoidChatArea: React.FC = ({ selections, setSelections, featureName, + loadingIcon, }) => { return (
    = ({
    )} - {isStreaming ? ( - - ) : ( - - )} + +
    + + {isStreaming && loadingIcon} + + {isStreaming ? ( + + ) : ( + + )} +
    +
    ); From 4a9af177368bb5149a598c18582ce9d2866ac09a Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Sat, 15 Mar 2025 21:52:10 -0700 Subject: [PATCH 080/173] commandbar draft (broken) --- .../browser/react/src/sidebar-tsx/SidebarChat.tsx | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) 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 cc419ba7..45f1c25c 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 @@ -1767,16 +1767,16 @@ const VoidCommandBar = () => { onFocusCapture={() => setIsFocused(true)} onBlurCapture={() => setIsFocused(false)} > - - + +
    } -const ListableToolItem = ({ name, onClick, isSmall, className, showDot }: { name: React.ReactNode, onClick?: () => void, isSmall?: boolean, className?: string, showDot?: boolean }) => { +export const ListableToolItem = ({ name, onClick, isSmall, className, showDot }: { name: React.ReactNode, onClick?: () => void, isSmall?: boolean, className?: string, showDot?: boolean }) => { return
    { commandService.executeCommand('vscode.open', uri, { preview: true }) }} /> -
    -
    - -
    +
    +
    } @@ -1183,12 +1185,10 @@ const TerminalToolChildren = ({ command, terminalId, result, resolveReason }: { className='w-full overflow-auto py-1' onClick={() => terminalToolsService.openTerminal(terminalId)} /> -
    -
    - {resolveReason.type === 'bgtask' ? 'Result so far:\n' : null} - {result} - {resultStr} -
    +
    + {resolveReason.type === 'bgtask' ? 'Result so far:\n' : null} + {result} + {resultStr}
    } @@ -1203,7 +1203,7 @@ const ReasoningWrapper = ({ isDoneReasoning, isStreaming, children }: { isDoneRe if (!isWriting) setIsOpen(isWriting) // if just finished reasoning, close }, [isWriting]) return : ''} isOpen={isOpen}> - +
    {children}
    @@ -1396,7 +1396,8 @@ const toolNameToComponent: { [T in ToolName]: { const isError = toolMessage.result.type === 'error' - const componentParams: ToolHeaderParams = { title, desc1, isError, icon } + const isRejected = toolMessage.result.type === 'rejected' + const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isRejected } if (toolMessage.result.type === 'success') { const { params } = toolMessage.result @@ -1434,12 +1435,13 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') - const title = toolNameToTitle[toolMessage.name].past + const title = toolMessage.result.type === 'success' ? toolNameToTitle[toolMessage.name].past : toolNameToTitle[toolMessage.name].proposed const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params) const icon = null const isError = toolMessage.result.type === 'error' - const componentParams: ToolHeaderParams = { title, desc1, isError, icon } + const isRejected = toolMessage.result.type === 'rejected' + const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isRejected } if (toolMessage.result.type === 'success') { const { params } = toolMessage.result @@ -1480,12 +1482,13 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ({ toolMessage, messageIdx }) => { const accessor = useAccessor() const chatThreadsService = accessor.get('IChatThreadService') - const title = toolNameToTitle[toolMessage.name].past + const title = toolMessage.result.type === 'success' ? toolNameToTitle[toolMessage.name].past : toolNameToTitle[toolMessage.name].proposed const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params) const icon = null const isError = toolMessage.result.type === 'error' - const componentParams: ToolHeaderParams = { title, desc1, isError, icon } + const isRejected = toolMessage.result.type === 'rejected' + const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isRejected } if (toolMessage.result.type === 'success' || toolMessage.result.type === 'rejected') { const { params } = toolMessage.result @@ -1540,12 +1543,13 @@ const toolNameToComponent: { [T in ToolName]: { const accessor = useAccessor() const commandService = accessor.get('ICommandService') const terminalToolsService = accessor.get('ITerminalToolService') - const title = toolNameToTitle[toolMessage.name].past + const title = toolMessage.result.type === 'success' ? toolNameToTitle[toolMessage.name].past : toolNameToTitle[toolMessage.name].proposed const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params) const icon = null const isError = toolMessage.result.type === 'error' - const componentParams: ToolHeaderParams = { title, desc1, isError, icon } + const isRejected = toolMessage.result.type === 'rejected' + const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isRejected } if (toolMessage.result.type === 'success') { const { command } = toolMessage.result.params @@ -1605,11 +1609,11 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx, isLast }: ChatBubblePr } else if (role === 'tool_request') { const ToolRequestWrapper = toolNameToComponent[chatMessage.name].requestWrapper as React.FC<{ toolRequest: any }> // ts isnt smart enough... - // if (!isLast) return null + if (!isLast) return null if (!ToolRequestWrapper) return null return <> - + } else if (role === 'tool') { @@ -1838,7 +1842,7 @@ export const SidebarChat = () => { // stream state const currThreadStreamState = useChatThreadsStreamState(chatThreadsState.currentThreadId) - const isStreaming = !!currThreadStreamState?.streamingToken + const isRunning = !!currThreadStreamState?.isRunning const latestError = currThreadStreamState?.error const messageSoFar = currThreadStreamState?.messageSoFar const reasoningSoFar = currThreadStreamState?.reasoningSoFar @@ -1861,7 +1865,7 @@ export const SidebarChat = () => { const onSubmit = useCallback(async () => { if (isDisabled) return - if (isStreaming) return + if (isRunning) return // update state chatThreadsService.closeStagingSelectionsInCurrentThread() // close all selections @@ -1871,10 +1875,8 @@ export const SidebarChat = () => { // getModelCapabilities() // TODO!!! check if can go into agent mode - const chatMode = settingsState.globalSettings.chatMode - try { - await chatThreadsService.addUserMessageAndStreamResponse({ userMessage, chatMode }) + await chatThreadsService.addUserMessageAndStreamResponse({ userMessage }) } catch (e) { console.error('Error while sending message in chat:', e) } @@ -1883,11 +1885,11 @@ export const SidebarChat = () => { textAreaFnsRef.current?.setValue('') textAreaRef.current?.focus() // focus input after submit - }, [chatThreadsService, isDisabled, isStreaming, textAreaRef, textAreaFnsRef, setSelections]) + }, [chatThreadsService, isDisabled, isRunning, textAreaRef, textAreaFnsRef, setSelections, settingsState]) const onAbort = () => { const threadId = currentThread.id - chatThreadsService.cancelStreaming(threadId) + chatThreadsService.stopRunning(threadId) } // const [_test_messages, _set_test_messages] = useState([]) @@ -1910,7 +1912,7 @@ export const SidebarChat = () => { }, [previousMessages, currentThread, numMessages]) const streamingChatIdx = previousMessagesHTML.length - const currStreamingMessageHTML = !!(reasoningSoFar || messageSoFar || isStreaming) ? + const currStreamingMessageHTML = !!(reasoningSoFar || messageSoFar || isRunning) ? { reasoning: reasoningSoFar ?? '', anthropicReasoning: null, }} - isLoading={isStreaming} + isLoading={isRunning} isLast={true} /> : null @@ -1970,10 +1972,10 @@ export const SidebarChat = () => { const onKeyDown = useCallback((e: KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { onSubmit() - } else if (e.key === 'Escape' && isStreaming) { + } else if (e.key === 'Escape' && isRunning) { onAbort() } - }, [onSubmit, onAbort, isStreaming]) + }, [onSubmit, onAbort, isRunning]) const inputForm =
    0 ? 'absolute bottom-0' : ''}`}> @@ -1982,7 +1984,7 @@ export const SidebarChat = () => { divRef={chatAreaRef} onSubmit={onSubmit} onAbort={onAbort} - isStreaming={isStreaming} + isStreaming={isRunning} isDisabled={isDisabled} showSelections={true} showProspectiveSelections={previousMessagesHTML.length === 0} @@ -1991,7 +1993,8 @@ export const SidebarChat = () => { onClickAnywhere={() => { textAreaRef.current?.focus() }} > 0 ? 'min-h-[9px]' : 'min-h-[81px]'} px-0.5`} + // className={`${previousMessages.length > 0 ? 'min-h-[9px]' : 'min-h-[81px]'} px-0.5`} + className={`min-h-[81px] px-0.5 py-0.5`} placeholder={`${keybindingString ? `${keybindingString} to select. ` : ''}Enter instructions...`} onChangeText={onChangeText} onKeyDown={onKeyDown} diff --git a/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts b/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts index 893b4dc1..4667b62e 100644 --- a/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts @@ -21,7 +21,7 @@ export type ToolRequestApproval = { name: T; // internal use params: ToolCallParams[T]; // internal use paramsStr: string; // internal use - this is what the LLM outputted, not necessarily JSON.stringify(params) - voidToolId: string; // internal id Void uses + id: string; // proposed tool's id } // WARNING: changing this format is a big deal!!!!!! need to migrate old format to new format on users' computers so people don't get errors. diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index 067fc649..ddc9d35f 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -8,6 +8,7 @@ import { URI } from '../../../../../base/common/uri.js'; import { os } from '../helpers/systemInfo.js'; import { IVoidFileService } from '../voidFileService.js'; import { CodeSelection, FileSelection, StagingSelectionItem } from '../chatThreadServiceTypes.js'; +import { ChatMode } from '../voidSettingsTypes.js'; // this is just for ease of readability @@ -21,8 +22,8 @@ Do NOT output the whole file here if possible, and try to write as LITTLE code a -export const chat_systemMessage = (workspaces: string[], runningTerminalIds: string[], mode: 'agent' | 'gather' | 'chat') => `\ -You are an expert coding ${mode === 'agent' ? 'agent' : 'assistant'} created by Void. Your job is to help the user ${mode === 'agent' ? 'develop, run, and make changes to their project' : 'search and understand their codebase'}. +export const chat_systemMessage = (workspaces: string[], runningTerminalIds: string[], mode: ChatMode) => `\ +You are an expert coding ${mode === 'agent' ? 'agent' : 'assistant'} created by Void. Your job is to help the user ${mode === 'agent' ? 'develop, run, and make changes to their project' : 'search and understand their codebase by providing specific references to files and content'}. You will be given instructions to follow from the user, \`INSTRUCTIONS\`. You may also be given a list of files that the user has specifically selected, \`SELECTIONS\`. Please assist the user with their query${mode === 'agent' ? `, bringing the task to completion (make all necessary changes, and do not be lazy)` : ''}. The user's query is never invalid. @@ -56,6 +57,7 @@ If you think it's appropriate to suggest an edit to a file, then you must descri Misc: - Do not make things up. +- Do not be lazy. - Always wrap any code you produce in triple backticks, and specify a language if possible. For example, ${tripleTick[0]}typescript\n...\n${tripleTick[1]}.\ ` diff --git a/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts b/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts index b27ea20b..9ecb680e 100644 --- a/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts +++ b/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts @@ -81,7 +81,7 @@ export type ServiceSendLLMMessageParams = { onText: OnText; onFinalMessage: OnFinalMessage; onError: OnError; - logging: { loggingName: string, }; + logging: { loggingName: string, loggingExtras?: { [k: string]: any } }; modelSelection: ModelSelection | null; modelSelectionOptions: ModelSelectionOptions | undefined; } & SendLLMType; @@ -91,7 +91,7 @@ export type SendLLMMessageParams = { onText: OnText; onFinalMessage: OnFinalMessage; onError: OnError; - logging: { loggingName: string, }; + logging: { loggingName: string, loggingExtras?: { [k: string]: any } }; abortRef: AbortRef; aiInstructions: string; diff --git a/src/vs/workbench/contrib/void/common/voidSettingsService.ts b/src/vs/workbench/contrib/void/common/voidSettingsService.ts index 10eee7b2..e631a8aa 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsService.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsService.ts @@ -99,7 +99,7 @@ const _updatedModelsAfterDefaultModelsChange = (defaultModelNames: string[], opt export const modelFilterOfFeatureName: { [featureName in FeatureName]: { filter: (o: ModelSelection, opts: { chatMode: ChatMode }) => boolean; emptyMessage: null | { message: string, priority: 'always' | 'fallback' } } } = { 'Autocomplete': { filter: (o) => getModelCapabilities(o.providerName, o.modelName).supportsFIM, emptyMessage: { message: 'No models support FIM', priority: 'always' } }, - 'Chat': { filter: (o, { chatMode }) => chatMode === 'chat' ? true : !!getModelCapabilities(o.providerName, o.modelName).supportsTools, emptyMessage: { message: 'No models support tool use', priority: 'fallback' } }, + 'Chat': { filter: (o, { chatMode }) => chatMode === 'normal' ? true : !!getModelCapabilities(o.providerName, o.modelName).supportsTools, emptyMessage: { message: 'No models support tool use', priority: 'fallback' } }, 'Ctrl+K': { filter: o => true, emptyMessage: null, }, 'Apply': { filter: o => true, emptyMessage: null, }, } diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index b0c2c618..6668610f 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -378,7 +378,7 @@ export const isFeatureNameDisabled = (featureName: FeatureName, settingsState: V -export type ChatMode = 'agent' | 'gather' | 'chat' +export type ChatMode = 'agent' | 'gather' | 'normal' export type GlobalSettings = { diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts index 1e17d97b..25f0e19e 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts @@ -17,7 +17,7 @@ export const sendLLMMessage = ({ onFinalMessage: onFinalMessage_, onError: onError_, abortRef: abortRef_, - logging: { loggingName }, + logging: { loggingName, loggingExtras }, settingsOfProvider, modelSelection, modelSelectionOptions, @@ -48,6 +48,7 @@ export const sendLLMMessage = ({ suffixLength: messages_.suffix.length, } : {}, + ...loggingExtras, ...extras, }) } From 17ac9c081c8cfc111519dc027c840adbee263aad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Commaret?= Date: Sun, 16 Mar 2025 21:07:04 +0100 Subject: [PATCH 082/173] Mistral FIM Method : seems to be the good one --- .../llmMessage/sendLLMMessage.impl.ts | 39 +++++++++++++------ 1 file changed, 27 insertions(+), 12 deletions(-) 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 bd4c3da4..743e2a61 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 @@ -8,6 +8,11 @@ import { Ollama } from 'ollama'; import OpenAI, { ClientOptions } from 'openai'; import { Model as OpenAIModel } from 'openai/resources/models.js'; +// Mistral FIM +import { MistralCore } from "@mistralai/mistralai/core.js"; +import { fimComplete } from "@mistralai/mistralai/funcs/fimComplete.js"; +// + import { extractReasoningOnFinalMessage, extractReasoningOnTextWrapper } from '../../common/helpers/extractCodeFromResult.js'; import { LLMChatMessage, LLMFIMMessage, ModelListParams, OllamaModelResponse, OnError, OnFinalMessage, OnText } from '../../common/sendLLMMessageTypes.js'; import { defaultProviderSettings, displayInfoOfProviderName, ModelSelectionOptions, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js'; @@ -494,18 +499,28 @@ const _sendMistralFIM = ({ messages: messages_, onFinalMessage, onError, setting prepareFIMMessage({ messages: messages_, aiInstructions }) - _sendOpenAICompatibleFIM({ - messages: messages_, - onFinalMessage, - onError, - settingsOfProvider, - modelName: modelName_, - _setAborter, - providerName, - aiInstructions, - modelSelectionOptions, - onText: () => { } - }); + const mistral = new MistralCore({ apiKey: settingsOfProvider.mistral.apiKey }) + + fimComplete( + mistral, { + model: modelName, + prompt: messages_.prefix, + suffix: messages_.suffix, + stream: false, + topP: 1, + stop: messages_.stopTokens + }, + ) + .then(async response => { + let content = response?.ok ? response.value.choices?.[0]?.message?.content : ''; + const fullText = typeof content === 'string' ? content : + Array.isArray(content) ? content.map(chunk => chunk.type === 'text' ? chunk.text : '').join('') : ''; + onFinalMessage({ fullText, fullReasoning: '', anthropicReasoning: null }); + console.log('✅ Réponse FIM reçue:', fullText); + }) + .catch(error => { + onError({ message: error + '', fullError: error }); + }) } From 7b3a82f1d289fa9cce134852ec4a0b72848089c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Commaret?= Date: Sun, 16 Mar 2025 21:13:57 +0100 Subject: [PATCH 083/173] Removed log --- .../contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts | 1 - 1 file changed, 1 deletion(-) 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 743e2a61..087b61b5 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 @@ -516,7 +516,6 @@ const _sendMistralFIM = ({ messages: messages_, onFinalMessage, onError, setting const fullText = typeof content === 'string' ? content : Array.isArray(content) ? content.map(chunk => chunk.type === 'text' ? chunk.text : '').join('') : ''; onFinalMessage({ fullText, fullReasoning: '', anthropicReasoning: null }); - console.log('✅ Réponse FIM reçue:', fullText); }) .catch(error => { onError({ message: error + '', fullError: error }); From a3cfd0d0239569c64fdcbf4e0ed7cf3469be608f Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Sun, 16 Mar 2025 22:29:17 -0700 Subject: [PATCH 084/173] fix codespan links --- .../contrib/void/browser/chatThreadService.ts | 27 +++++++++++++++---- .../react/src/markdown/ChatMarkdownRender.tsx | 6 ++--- .../void/common/chatThreadServiceTypes.ts | 1 + 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 1447b7ff..3b6104c8 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -27,6 +27,7 @@ import { ChatMessage, CodespanLocationLink, StagingSelectionItem, ToolMessage, T import { Position } from '../../../../editor/common/core/position.js'; import { ITerminalToolService } from './terminalToolService.js'; import { IMetricsService } from '../common/metricsService.js'; +import { shorten } from '../../../../base/common/labels.js'; const findLastIndex = (arr: T[], condition: (t: T) => boolean): number => { for (let i = arr.length - 1; i >= 0; i--) { @@ -910,15 +911,30 @@ class ChatThreadService extends Disposable implements IChatThreadService { const doesUriMatchTarget = (uri: URI) => uri.path.includes(target) // check if any prevFiles are the `codespanSearch` - for (const uri of prevUris) { - if (doesUriMatchTarget(uri)) return { uri } + for (const [idx, uri] of prevUris.entries()) { + if (doesUriMatchTarget(uri)) { + + // shorten it + const prevUriStrs = prevUris.map(uri => uri.toString()) + const shortenedUriStrs = shorten(prevUriStrs) + const displayText = shortenedUriStrs[idx] + + return { uri, displayText } + } } // else search codebase for file const { uris } = await this._toolsService.callTool['pathname_search']({ queryStr: target, pageNumber: 0 }) - for (const uri of uris) { - if (doesUriMatchTarget(uri)) return { uri } + for (const [idx, uri] of uris.entries()) { + if (doesUriMatchTarget(uri)) { + + const prevUriStrs = prevUris.map(uri => uri.toString()) + const shortenedUriStrs = shorten(prevUriStrs) + const displayText = shortenedUriStrs[idx] + + return { uri, displayText } + } } } @@ -967,7 +983,8 @@ class ChatThreadService extends Disposable implements IChatThreadService { startColumn: definition.range.startColumn, endLineNumber: definition.range.endLineNumber, endColumn: definition.range.endColumn, - } + }, + displayText: _codespanStr, }; // const defModelRef = await this._textModelService.createModelReference(definition.uri); diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index 040f01d0..09078700 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -34,13 +34,11 @@ const Codespan = ({ text, className, onClick }: { text: string, className?: stri // TODO compute this once for efficiency. we should use `labels.ts/shorten` to display duplicates properly - const filename = useMemo(() => getBasename(text), [text]) - return - {filename} + {text} } @@ -101,7 +99,7 @@ const CodespanWithLink = ({ text, rawText, chatMessageLocation }: { text: string } return diff --git a/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts b/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts index 4667b62e..bbdc6cf0 100644 --- a/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts @@ -75,6 +75,7 @@ export type StagingSelectionItem = CodeSelection | FileSelection export type CodespanLocationLink = { uri: URI, // we handle serialization for this + displayText: string, selection?: { // store as JSON so dont have to worry about serialization startLineNumber: number startColumn: number, From 39e989d3cc2121647a308d84f6c38d5dcc7b38a8 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Sun, 16 Mar 2025 22:48:17 -0700 Subject: [PATCH 085/173] codespan link --- .../contrib/void/browser/chatThreadService.ts | 20 +++++++++++++++---- .../react/src/markdown/ChatMarkdownRender.tsx | 1 + 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 3b6104c8..9092f38e 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -910,28 +910,40 @@ class ChatThreadService extends Disposable implements IChatThreadService { const doesUriMatchTarget = (uri: URI) => uri.path.includes(target) - // check if any prevFiles are the `codespanSearch` + // check if any prevFiles are the `target` for (const [idx, uri] of prevUris.entries()) { if (doesUriMatchTarget(uri)) { // shorten it + + // TODO make this logic more general const prevUriStrs = prevUris.map(uri => uri.toString()) const shortenedUriStrs = shorten(prevUriStrs) - const displayText = shortenedUriStrs[idx] + let displayText = shortenedUriStrs[idx] + const ellipsisIdx = displayText.lastIndexOf('…/'); + if (ellipsisIdx >= 0) { + displayText = displayText.slice(ellipsisIdx + 2) + } return { uri, displayText } } } - // else search codebase for file + // else search codebase for `target` const { uris } = await this._toolsService.callTool['pathname_search']({ queryStr: target, pageNumber: 0 }) for (const [idx, uri] of uris.entries()) { if (doesUriMatchTarget(uri)) { + // TODO make this logic more general const prevUriStrs = prevUris.map(uri => uri.toString()) const shortenedUriStrs = shorten(prevUriStrs) - const displayText = shortenedUriStrs[idx] + let displayText = shortenedUriStrs[idx] + const ellipsisIdx = displayText.lastIndexOf('…/'); + if (ellipsisIdx >= 0) { + displayText = displayText.slice(ellipsisIdx + 2) + } + return { uri, displayText } } diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index 09078700..caa26bf8 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -99,6 +99,7 @@ const CodespanWithLink = ({ text, rawText, chatMessageLocation }: { text: string } return Date: Sun, 16 Mar 2025 23:19:11 -0700 Subject: [PATCH 086/173] misc state fixes and fix model background (add voidModelService, delete voidFileService) --- .../contrib/void/browser/chatThreadService.ts | 6 +- .../contrib/void/browser/editCodeService.ts | 129 +++++++---------- .../src/markdown/ApplyBlockHoverButtons.tsx | 29 +++- .../browser/react/src/markdown/BlockCode.tsx | 12 -- .../react/src/markdown/ChatMarkdownRender.tsx | 25 ++-- .../react/src/sidebar-tsx/SidebarChat.tsx | 134 ++++++++++-------- .../void/browser/react/src/util/helpers.tsx | 11 +- .../void/browser/react/src/util/inputs.tsx | 7 +- .../void/browser/react/src/util/services.tsx | 4 + .../void/browser/searchReplaceCacheService.ts | 4 +- .../contrib/void/browser/toolsService.ts | 11 +- .../{getLanguage.ts => languageHelpers.ts} | 50 ++----- .../contrib/void/common/helpers/util.ts | 18 +++ .../contrib/void/common/prompt/prompts.ts | 12 +- .../contrib/void/common/voidFileService.ts | 114 --------------- .../contrib/void/common/voidModelService.ts | 68 +++++++++ 16 files changed, 295 insertions(+), 339 deletions(-) delete mode 100644 src/vs/workbench/contrib/void/browser/react/src/markdown/BlockCode.tsx rename src/vs/workbench/contrib/void/common/helpers/{getLanguage.ts => languageHelpers.ts} (74%) create mode 100644 src/vs/workbench/contrib/void/common/helpers/util.ts delete mode 100644 src/vs/workbench/contrib/void/common/voidFileService.ts create mode 100644 src/vs/workbench/contrib/void/common/voidModelService.ts diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 9092f38e..91489c78 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -14,7 +14,6 @@ import { ILLMMessageService } from '../common/sendLLMMessageService.js'; import { chat_userMessageContent, chat_systemMessage, chat_lastUserMessageWithFilesAdded, chat_selectionsString } from '../common/prompt/prompts.js'; import { getErrorMessage, LLMChatMessage, ToolCallType } from '../common/sendLLMMessageTypes.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; -import { IVoidFileService } from '../common/voidFileService.js'; import { generateUuid } from '../../../../base/common/uuid.js'; import { ChatMode, FeatureName, ModelSelection, ModelSelectionOptions } from '../common/voidSettingsTypes.js'; import { IVoidSettingsService } from '../common/voidSettingsService.js'; @@ -28,6 +27,7 @@ import { Position } from '../../../../editor/common/core/position.js'; import { ITerminalToolService } from './terminalToolService.js'; import { IMetricsService } from '../common/metricsService.js'; import { shorten } from '../../../../base/common/labels.js'; +import { IVoidModelService } from '../common/voidModelService.js'; const findLastIndex = (arr: T[], condition: (t: T) => boolean): number => { for (let i = arr.length - 1; i >= 0; i--) { @@ -203,7 +203,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { constructor( @IStorageService private readonly _storageService: IStorageService, - @IVoidFileService private readonly _voidFileService: IVoidFileService, + @IVoidModelService private readonly _voidModelService: IVoidModelService, @ILLMMessageService private readonly _llmMessageService: ILLMMessageService, @IToolsService private readonly _toolsService: IToolsService, @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, @@ -658,7 +658,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { // define helper functions so we can tell what's going on const getLatestMessages = async () => { // recompute files in last message - const selectionsStr = await chat_selectionsString(prevSelns, currSelns, this._voidFileService) // all the file CONTENTS or "selections" de-duped + const selectionsStr = await chat_selectionsString(prevSelns, currSelns, this._voidModelService) // all the file CONTENTS or "selections" de-duped const userMessageFullContent = chat_lastUserMessageWithFilesAdded(userMessageContent, selectionsStr) // full last message: user message + CONTENTS of all files // replace last userMessage with userMessageFullContent (which contains all the files too) diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 3785310e..01bb2b0c 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -12,7 +12,7 @@ import { ICodeEditor, IOverlayWidget, IViewZone, OverlayWidgetPositionPreference import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; // import { throttle } from '../../../../base/common/decorators.js'; import { ComputedDiff, findDiffs } from './helpers/findDiffs.js'; -import { IModelDecorationOptions, ITextModel } from '../../../../editor/common/model.js'; +import { EndOfLinePreference, IModelDecorationOptions, ITextModel } from '../../../../editor/common/model.js'; import { IRange } from '../../../../editor/common/core/range.js'; import { registerColor } from '../../../../platform/theme/common/colorUtils.js'; import { Color, RGBA } from '../../../../base/common/color.js'; @@ -40,14 +40,10 @@ import { ICommandService } from '../../../../platform/commands/common/commands.j import { ILLMMessageService } from '../common/sendLLMMessageService.js'; import { LLMChatMessage, OnError, errorDetails } from '../common/sendLLMMessageTypes.js'; import { IMetricsService } from '../common/metricsService.js'; -import { IVoidFileService } from '../common/voidFileService.js'; import { IEditCodeService, URIStreamState, AddCtrlKOpts, StartApplyingOpts } from './editCodeServiceInterface.js'; import { IVoidSettingsService } from '../common/voidSettingsService.js'; import { FeatureName } from '../common/voidSettingsTypes.js'; -import { ILanguageService } from '../../../../editor/common/languages/language.js'; -import { getFullLanguage, getLanguageFromModel } from '../common/helpers/getLanguage.js'; -// import { IFileService } from '../../../../platform/files/common/files.js'; -// import { VSBuffer } from '../../../../base/common/buffer.js'; +import { IVoidModelService } from '../common/voidModelService.js'; const configOfBG = (color: Color) => { return { dark: color, light: color, hcDark: color, hcLight: color, } @@ -245,8 +241,8 @@ class EditCodeService extends Disposable implements IEditCodeService { diffAreaOfId: Record = {}; // diffareaId -> diffArea diffOfId: Record = {}; // diffid -> diff (redundant with diffArea._diffOfId) - _sortedUrisWithDiffs: URI[] = [] // derivative of diffAreaOfId - _sortedDiffsOfFspath: { [uriString: string]: Diff[] | undefined } = {} // derivative of diffAreaOfId + _sortedUrisWithDiffs: URI[] = [] // derivative of diffAreaOfId (computed from it) + _sortedDiffsOfFspath: { [uriString: string]: Diff[] | undefined } = {} // derivative of diffAreaOfId (computed from it) // only applies to diffZones // streamingDiffZones: Set = new Set() @@ -278,16 +274,17 @@ class EditCodeService extends Disposable implements IEditCodeService { @IMetricsService private readonly _metricsService: IMetricsService, @INotificationService private readonly _notificationService: INotificationService, @ICommandService private readonly _commandService: ICommandService, - @IVoidFileService private readonly _voidFileService: IVoidFileService, @IVoidSettingsService private readonly _settingsService: IVoidSettingsService, // @IFileService private readonly _fileService: IFileService, - @ILanguageService private readonly _languageService: ILanguageService, + @IVoidModelService private readonly _voidModelService: IVoidModelService, ) { super(); // this function initializes data structures and listens for changes const registeredModelListeners = new Set() - const initializeModel = (model: ITextModel) => { + const initializeModel = async (model: ITextModel) => { + + await this._voidModelService.initializeModel(model.uri) // do not add listeners to the same model twice - important, or will see duplicates if (registeredModelListeners.has(model.uri.fsPath)) return @@ -442,7 +439,7 @@ class EditCodeService extends Disposable implements IEditCodeService { private _addDiffAreaStylesToURI = (uri: URI) => { - const model = this._getModel(uri) + const { model } = this._voidModelService.getModel(uri) for (const diffareaid of this.diffAreasOfURI[uri.fsPath] || []) { const diffArea = this.diffAreaOfId[diffareaid] @@ -471,7 +468,9 @@ class EditCodeService extends Disposable implements IEditCodeService { private _computeDiffsAndAddStylesToURI = (uri: URI) => { - const fullFileText = this._voidFileService.readModel(uri) ?? '' + const { model } = this._voidModelService.getModel(uri) + if (model === null) return + const fullFileText = model.getValue(EndOfLinePreference.LF) for (const diffareaid of this.diffAreasOfURI[uri.fsPath] || []) { const diffArea = this.diffAreaOfId[diffareaid] @@ -636,8 +635,7 @@ class EditCodeService extends Disposable implements IEditCodeService { const disposeInThisEditorFns: (() => void)[] = [] - const model = this._getModel(uri) - if (model === null) return + const { model } = this._voidModelService.getModel(uri) // green decoration and minimap decoration if (type !== 'deletion') { @@ -775,38 +773,6 @@ class EditCodeService extends Disposable implements IEditCodeService { - - - // private _readURI(uri: URI, range?: IRange): string | null { - // if (!range) return this._getModel(uri)?.getValue(EndOfLinePreference.LF) ?? null - // else return this._getModel(uri)?.getValueInRange(range, EndOfLinePreference.LF) ?? null - // } - // private _getNumLines(uri: URI): number | null { - // return this._getModel(uri)?.getLineCount() ?? null - // } - - - private _getModel(uri: URI) { - const model = this._modelService.getModel(uri) - if (!model || model.isDisposed()) { - return null - } - return model - } - - // not obvious at all, but if we want the model we can just create it. our listeners in the constructor handle it. call this the moment we know we have a uri that we want to make changes to - private async _ensureModelExists(uri: URI) { - const m = this._getModel(uri) - if (m !== null) return m - - const fileStr = await this._voidFileService.readFile(uri) - if (fileStr === null) return null - const lang = getFullLanguage(this._languageService, { uri, fileContents: fileStr }) - const model = this._modelService.createModel(fileStr, lang, uri); - return model - } - - private _getActiveEditorURI(): URI | null { const editor = this._codeEditorService.getActiveCodeEditor() if (!editor) return null @@ -817,8 +783,8 @@ class EditCodeService extends Disposable implements IEditCodeService { weAreWriting = false private _writeURIText(uri: URI, text: string, range_: IRange | 'wholeFileRange', { shouldRealignDiffAreas, }: { shouldRealignDiffAreas: boolean, }) { - const model = this._getModel(uri) - if (model === null) return + const { model } = this._voidModelService.getModel(uri) + if (!model) return // TODO!!!! make sure this works const range: IRange = range_ === 'wholeFileRange' ? { startLineNumber: 1, startColumn: 1, endLineNumber: model.getLineCount(), endColumn: Number.MAX_SAFE_INTEGER } // whole file @@ -831,7 +797,7 @@ class EditCodeService extends Disposable implements IEditCodeService { this._realignAllDiffAreasLines(uri, newText, oldRange) } - const uriStr = model.getValue() + const uriStr = model.getValue(EndOfLinePreference.LF) // heuristic check const dontNeedToWrite = uriStr === text @@ -852,6 +818,8 @@ class EditCodeService extends Disposable implements IEditCodeService { private _addToHistory(uri: URI, opts?: { onUndo?: () => void }) { const getCurrentSnapshot = async (): Promise => { + await this._voidModelService.initializeModel(uri) + const { model } = this._voidModelService.getModel(uri) const snapshottedDiffAreaOfId: Record = {} for (const diffareaid in this.diffAreaOfId) { @@ -864,7 +832,9 @@ class EditCodeService extends Disposable implements IEditCodeService { ) as DiffAreaSnapshot } - const entireFileCode = await this._voidFileService.readFile(uri) ?? '' + const entireFileCode = model ? model.getValue(EndOfLinePreference.LF) : '' // TODO!!! make sure this isn't '' usually + + // this._noLongerNeedModelReference(uri) return { snapshottedDiffAreaOfId, entireFileCode, // the whole file's code @@ -915,11 +885,13 @@ class EditCodeService extends Disposable implements IEditCodeService { } this._onDidAddOrDeleteDiffZones.fire({ uri }) + await this._voidModelService.initializeModel(uri) // restore file content this._writeURIText(uri, entireModelCode, 'wholeFileRange', { shouldRealignDiffAreas: false } ) + // this._noLongerNeedModelReference(uri) } const beforeSnapshot: Promise = getCurrentSnapshot() @@ -1330,13 +1302,12 @@ class EditCodeService extends Disposable implements IEditCodeService { const uri_ = this._getActiveEditorURI() if (!uri_) return uri = uri_ - await this._ensureModelExists(uri) + await this._voidModelService.initializeModel(uri) + const { model } = this._voidModelService.getModel(uri) + if (!model) return - const c_ = await this._voidFileService.readFile(uri) - if (c_ === null) return - currentFileStr = c_ - - const numLines = numLinesOfStr(currentFileStr) + currentFileStr = model.getValue(EndOfLinePreference.LF) + const numLines = model.getLineCount() // remove all diffZones on this URI, adding to history (there can't possibly be overlap after this) const behavior: 'accept' | 'reject' = opts.startBehavior === 'accept-conflicts' ? 'accept' : 'reject' @@ -1353,11 +1324,11 @@ class EditCodeService extends Disposable implements IEditCodeService { const { startLine: startLine_, endLine: endLine_, _URI } = ctrlKZone uri = _URI - await this._ensureModelExists(uri) + await this._voidModelService.initializeModel(uri) + const { model } = this._voidModelService.getModel(uri) + if (!model) return - const c_ = await this._voidFileService.readFile(uri) - if (c_ === null) return - currentFileStr = c_ + currentFileStr = model.getValue(EndOfLinePreference.LF) startLine = startLine_ endLine = endLine_ @@ -1366,8 +1337,11 @@ class EditCodeService extends Disposable implements IEditCodeService { throw new Error(`Void: diff.type not recognized on: ${from}`) } + const { model } = this._voidModelService.getModel(uri) + if (!model) return + const originalCode = currentFileStr.split('\n').slice((startLine - 1), (endLine - 1) + 1).join('\n') - const language = getLanguageFromModel(uri, this._modelService) + const language = model.getLanguageId() let streamRequestIdRef: { current: string | null } = { current: null } @@ -1517,7 +1491,9 @@ class EditCodeService extends Disposable implements IEditCodeService { { startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed { shouldRealignDiffAreas: true } ) - this._voidFileService.saveOrWriteFileAssumingModelExists(uri) + + const { editorModel } = this._voidModelService.getModel(uri) + editorModel?.save() // save the file onDone() resMessageDonePromise() }, @@ -1535,7 +1511,10 @@ class EditCodeService extends Disposable implements IEditCodeService { console.log('done waiting') } - writeover().then(() => resApplyPromise()).catch((e) => rejApplyPromise(e)) + writeover().then(() => { + resApplyPromise() + // this._noLongerNeedModelReference(uri) + }).catch((e) => rejApplyPromise(e)) return [diffZone, applyPromise] } @@ -1555,14 +1534,12 @@ class EditCodeService extends Disposable implements IEditCodeService { else { uri = givenURI } - await this._ensureModelExists(uri) - + const { model } = this._voidModelService.getModel(uri) + if (!model) return // generate search/replace block text - const originalFileCode = await this._voidFileService.readFile(uri) - if (originalFileCode === null) return - - const numLines = numLinesOfStr(originalFileCode) + const originalFileCode = model.getValue(EndOfLinePreference.LF) + const numLines = model.getLineCount() // reject all diffZones on this URI, adding to history (there can't possibly be overlap after this) this.removeDiffAreas({ uri, behavior: 'reject', removeCtrlKs: true }) @@ -1738,7 +1715,7 @@ class EditCodeService extends Disposable implements IEditCodeService { const originalBounds = findTextInCode(block.orig, originalFileCode) // if error if (typeof originalBounds === 'string') { - console.log('TEXT NOT FOUND') + console.log('TEXT NOT FOUND, RETRYING') const content = errMsgOfInvalidStr(originalBounds, block.orig) messages.push( { role: 'assistant', content: fullText, anthropicReasoning: null }, // latest output @@ -1776,7 +1753,7 @@ class EditCodeService extends Disposable implements IEditCodeService { const trackingZone = this._addDiffArea(adding) addedTrackingZoneOfBlockNum.push(trackingZone) latestStreamLocationMutable = { line: startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 } - } // <-- done adding diffarea + } // end adding diffarea // should always be in streaming state here @@ -1846,7 +1823,8 @@ class EditCodeService extends Disposable implements IEditCodeService { { shouldRealignDiffAreas: true } ) - this._voidFileService.saveOrWriteFileAssumingModelExists(uri) + const { editorModel } = this._voidModelService.getModel(uri) + editorModel?.save() // save the file // TODO!!! make sure this works onDone() resMessageDonePromise() }, @@ -1868,7 +1846,10 @@ class EditCodeService extends Disposable implements IEditCodeService { } // end retryLoop - retryLoop().then(() => resApplyDonePromise()).catch((e) => rejApplyDonePromise(e)) + retryLoop().then(() => { + resApplyDonePromise(); + // this._noLongerNeedModelReference(uri) + }).catch((e) => rejApplyDonePromise(e)) return [diffZone, applyDonePromise] } 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 2016ea3b..8afd17f1 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 @@ -1,9 +1,9 @@ import { useState, useEffect, useCallback } from 'react' import { useAccessor, useURIStreamState, useSettingsState } from '../util/services.js' -import { useRefState } from '../util/helpers.js' +import { usePromise, useRefState } from '../util/helpers.js' import { isFeatureNameDisabled } from '../../../../common/voidSettingsTypes.js' import { URI } from '../../../../../../../base/common/uri.js' -import { LucideIcon, RotateCw } from 'lucide-react' +import { FileSymlink, LucideIcon, RotateCw } from 'lucide-react' import { Check, X, Square, Copy, Play, } from 'lucide-react' import { getBasename, ListableToolItem, ToolContentsWrapper } from '../sidebar-tsx/SidebarChat.js' import { ChatMarkdownRender } from './ChatMarkdownRender.js' @@ -16,7 +16,7 @@ enum CopyButtonText { type IconButtonProps = { - onClick: () => void + onClick: () => void; title: string Icon: LucideIcon disabled?: boolean @@ -27,7 +27,11 @@ export const IconShell1 = ({ onClick, title, Icon, disabled, className }: IconBu - - - - - - - -
    -
    -
    File: {(editCodeService._sortedUrisWithDiffs.findIndex(u => u.fsPath === getCurrentUri()?.fsPath) ?? 0) + 1}/{editCodeService._sortedUrisWithDiffs.length}
    -
    Diff: {(diffIdxOfFspath[getCurrentUri()?.fsPath ?? ''] ?? 0) + 1}/{editCodeService._sortedDiffsOfFspath[getCurrentUri()?.fsPath ?? '']?.length ?? 0}
    -
    -
    - - {diffZones.map((area, index) => ( - <> -
    {getBasename(area?._URI?.toString())}
    - - ))} -
    -} - export const SidebarChat = () => { const textAreaRef = useRef(null) const textAreaFnsRef = useRef(null) @@ -2051,7 +1874,6 @@ export const SidebarChat = () => { /> -
    return ( diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx new file mode 100644 index 00000000..9f494909 --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx @@ -0,0 +1,205 @@ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ + + +import { useAccessor, useIsDark } from '../util/services.js'; + +import '../styles.css' +import { DiffZone } from '../../../editCodeService.js'; +import { useCallback, useEffect, useState } from 'react'; +import { ScrollType } from '../../../../../../../editor/common/editorCommon.js'; +import { getBasename } from '../sidebar-tsx/SidebarChat.js'; + +export const VoidCommandBarMain = ({ className }: { className: string }) => { + const isDark = useIsDark() + + return
    + +
    +} + + + +const VoidCommandBar = () => { + const accessor = useAccessor() + const editCodeService = accessor.get('IEditCodeService') + const editorService = accessor.get('ICodeEditorService') + const commandService = accessor.get('ICommandService') + + const [_, rerender] = useState(0) + // Add a state variable to track focus + const [isFocused, setIsFocused] = useState(false) + console.log('rerender count: ', _) + + // state for what the user is currently focused on (both URI and diff) + const [diffIdxOfFspath, setDiffIdxOfFspath] = useState>({}) + // const [currentUriIdx, setCurrentUriIdx] = useState(-1) // we are doing O(n) search for this + + const getCurrentUri = useCallback(() => { + const editor = editorService.getActiveCodeEditor() + if (!editor) return null + const uri = editor.getModel()?.uri + if (!uri) return null + return uri + }, [editorService]) + + const diffZones: DiffZone[] = [] + + + // trigger rerender when diffzone is created (TODO need to also update when diff is accepted/rejected) + useEffect(() => { + const disposable = editCodeService.onDidAddOrDeleteDiffInDiffZone(() => { + rerender(c => c + 1) // rerender + }) + return () => disposable.dispose() + }, [editCodeService, rerender]) + + + const getNextDiff = useCallback(({ step }: { step: 1 | -1 }) => { + + const currentUri = getCurrentUri() + + if (!currentUri) { + return; + } + + const sortedDiffs = editCodeService._sortedDiffsOfFspath[currentUri.fsPath] + + if (!sortedDiffs || sortedDiffs.length === 0) { + return; + } + + const currentDiffIdx = diffIdxOfFspath[currentUri.fsPath] || 0 + const nextDiffIdx = (currentDiffIdx + step) % sortedDiffs.length + + const nextDiff = sortedDiffs[nextDiffIdx] + + return { nextDiff, nextDiffIdx, } + + }, [getCurrentUri, editCodeService._sortedDiffsOfFspath, diffIdxOfFspath]) + + const getNextUri = useCallback(({ step }: { step: 1 | -1 }) => { + + const sortedUris = editCodeService._sortedUrisWithDiffs + if (sortedUris.length === 0) { + return; + } + + const currentUri = getCurrentUri() + + const defaultUriIdx = step === 1 ? -1 : 0 // defaults: if next, currentIdx = -1; if prev, currentIdx = 0 + let currentUriIdx = -1 + if (currentUri) { + currentUriIdx = sortedUris.findIndex(u => u.fsPath === currentUri.fsPath) + } + + if (currentUriIdx === -1) { // not found + currentUriIdx = defaultUriIdx // set to default + } + + const nextUriIdx = (currentUriIdx + step) % sortedUris.length + const nextUri = sortedUris[nextUriIdx] + + return { nextUri, nextUriIdx, } + + }, [getCurrentUri, editCodeService._sortedUrisWithDiffs]) + + + const gotoNextDiff = ({ step }: { step: 1 | -1 }) => { + + // get the next diff + const res = getNextDiff({ step: 1 }) + if (!res) return; + + // scroll to the next diff + const { nextDiff, nextDiffIdx } = res; + const editor = editorService.getActiveCodeEditor() + if (!editor) return; + + const range = { startLineNumber: nextDiff.startLine, endLineNumber: nextDiff.startLine, startColumn: 1, endColumn: 1 }; + editor.revealRange(range, ScrollType.Immediate) + + // update state + const diffArea = editCodeService.diffAreaOfId[nextDiff.diffareaid] + setDiffIdxOfFspath(v => ({ ...v, [diffArea._URI.fsPath]: nextDiffIdx })) + + } + + const gotoNextUri = ({ step }: { step: 1 | -1 }) => { + + // get the next uri + const res = getNextUri({ step: 1 }) + if (!res) return; + + const { nextUri, nextUriIdx } = res; + + // open the uri and scroll to diff + const sortedDiffs = editCodeService._sortedDiffsOfFspath[nextUri.fsPath] + if (!sortedDiffs) return; + + const diffIdx = diffIdxOfFspath[nextUri.fsPath] || 0 + const diff = sortedDiffs[diffIdx] + + const range = { startLineNumber: diff.startLine, endLineNumber: diff.startLine, startColumn: 1, endColumn: 1 }; + + commandService.executeCommand('vscode.open', nextUri).then(() => { + + // select the text + setTimeout(() => { + + const editor = editorService.getActiveCodeEditor() + if (!editor) return; + + editor.revealRange(range, ScrollType.Immediate) + + }, 50) + + }) + } + + return
    setIsFocused(true)} + onBlurCapture={() => setIsFocused(false)} + > +
    + + + + + + + +
    + +
    +
    File {(editCodeService._sortedUrisWithDiffs.findIndex(u => u.fsPath === getCurrentUri()?.fsPath) ?? 0) + 1} of {editCodeService._sortedUrisWithDiffs.length}
    +
    Diff {(diffIdxOfFspath[getCurrentUri()?.fsPath ?? ''] ?? 0) + 1} of {editCodeService._sortedDiffsOfFspath[getCurrentUri()?.fsPath ?? '']?.length ?? 0}
    +
    +
    + +} diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/index.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/index.tsx new file mode 100644 index 00000000..5b185788 --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/index.tsx @@ -0,0 +1,9 @@ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ + +import { mountFnGenerator } from '../util/mountFnGenerator.js' +import { VoidCommandBarMain } from './VoidCommandBar.js' + +export const mountVoidCommandBar = mountFnGenerator(VoidCommandBarMain) diff --git a/src/vs/workbench/contrib/void/browser/react/tsup.config.js b/src/vs/workbench/contrib/void/browser/react/tsup.config.js index e51218e5..ab3bd525 100644 --- a/src/vs/workbench/contrib/void/browser/react/tsup.config.js +++ b/src/vs/workbench/contrib/void/browser/react/tsup.config.js @@ -7,6 +7,7 @@ import { defineConfig } from 'tsup' export default defineConfig({ entry: [ + './src2/void-command-bar-tsx/index.tsx', './src2/sidebar-tsx/index.tsx', './src2/void-settings-tsx/index.tsx', './src2/quick-edit-tsx/index.tsx', From 5b3471a7d7c9b84a3bbfb1213b40ff6c5d7ed79e Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Mon, 17 Mar 2025 23:47:33 -0700 Subject: [PATCH 108/173] changes + potentially fix error finding SEARCH block --- .../contrib/void/browser/editCodeService.ts | 50 +++++++++++++++---- .../contrib/void/common/prompt/prompts.ts | 19 ++++--- 2 files changed, 52 insertions(+), 17 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 68f7e0af..74ea60ba 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, searchReplace_systemMessage, searchReplace_userMessage, FINAL, ORIGINAL, DIVIDER, tripleTick, } from '../common/prompt/prompts.js'; import { mountCtrlK } from './react/out/quick-edit-tsx/index.js' import { mountVoidCommandBar } from './react/out/void-command-bar-tsx/index.js' @@ -1629,13 +1629,24 @@ class EditCodeService extends Disposable implements IEditCodeService { } - const errHelper = (erroneousOriginal: string) => `All previous SEARCH/REPLACE blocks (if any) have been applied except the latest erroneous one. Please continue outputting SEARCH/REPLACE blocks. The ORIGINAL code with an error was: ${JSON.stringify(erroneousOriginal)}` - const errMsgOfInvalidStr = (str: string & ReturnType, blockOrig: string) => { - return str === `Not found` ? - `The ORIGINAL code provided could not be found in the file. You should make sure the text in ORIGINAL matches lines of code EXACTLY. ${errHelper(blockOrig)}` + const errMsgOfInvalidStr = (str: string & ReturnType, blockOrig: string, blockNum: number, blocks: ExtractedSearchReplaceBlock[]) => { + + const descStr = str === `Not found` ? + `The most recent ORIGINAL code could not be found in the file, so you were interrupted. You should make sure the text in ORIGINAL matches lines of code EXACTLY. Erroneous ORIGINAL code:\n${JSON.stringify(blockOrig)}` : str === `Not unique` ? - `The ORIGINAL code provided shows up multiple times in the file. We recommend making the ORIGINAL portion bigger so we can find a unique match. ${errHelper(blockOrig)}` + `The most recent ORIGINAL code shows up multiple times in the file, so you were interrupted. Please make the ORIGINAL excerpt bigger so it's unique. Erroneous ORIGINAL code:\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}` : ''}` + + console.log('ERr msg:', errMsg) + return errMsg + } @@ -1729,7 +1740,7 @@ class EditCodeService extends Disposable implements IEditCodeService { shouldUpdateOrigStreamStyle = true - // if this is the first time we're seeing this block, add it as a diffarea so we can start streaming + // 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) // if error @@ -1738,7 +1749,7 @@ class EditCodeService extends Disposable implements IEditCodeService { console.log('fullText', { fullText }) console.log('error:', originalBounds) console.log('block.orig:', block.orig) - const content = errMsgOfInvalidStr(originalBounds, block.orig) + const content = errMsgOfInvalidStr(originalBounds, block.orig, blockNum, blocks) messages.push( { role: 'assistant', content: fullText, anthropicReasoning: null }, // latest output { role: 'user', content: content } // user explanation of what's wrong @@ -1746,11 +1757,30 @@ class EditCodeService extends Disposable implements IEditCodeService { // REVERT THIS ONE BLOCK // TODO!!! test this - latestStreamLocationMutable = null - shouldUpdateOrigStreamStyle = true blocks.splice(blockNum, Infinity) // remove all blocks at and after this one oldBlocks = deepClone(blocks) + // Reset streaming state but preserve line context + shouldUpdateOrigStreamStyle = true + // initialize with the last known good position + if (blockNum > 0 && blockNum - 1 < addedTrackingZoneOfBlockNum.length) { + const lastGoodZone = addedTrackingZoneOfBlockNum[blockNum - 1]; + latestStreamLocationMutable = { + line: lastGoodZone.endLine + 1, + addedSplitYet: false, + col: 1, + originalCodeStartLine: 1 + }; + } else { + // If we're at the first block, reset to beginning + latestStreamLocationMutable = { + line: diffZone.startLine, + addedSplitYet: false, + col: 1, + originalCodeStartLine: 1 + }; + } + // abort and resolve shouldSendAnotherMessage = true if (streamRequestIdRef.current) { diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index aec37012..4ba61b6d 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -17,21 +17,26 @@ export const tripleTick = ['```', '```'] export const editToolDesc_toolDescription = `\ A high level description of the change you'd like to make in the file. This description will be handed to a dumber, faster model that will quickly apply the change.\ -The model does not have ANY context except the file content and this description, so make sure to include all necessary information to make the change here.\ -Typically the best description you can give is a code block of the form:\n${tripleTick[0]}\n// ... existing code ...\n{{change 1}}\n// ... existing code ...\n{{change2}}\n// ... existing code ...\n{{change 3}}\n...\n${tripleTick[1]}. \ +Make sure to include all necessary information to make the change in this description, since it is the only context given to the fast-apply model that will actually write the change.\ +The best description you can give is a single code block of the form:\n${tripleTick[0]}\n// ... existing code ...\n{{change 1}}\n// ... existing code ...\n{{change2}}\n// ... existing code ...\n{{change 3}}\n...\n${tripleTick[1]}. \ Wrap all code in triple backticks. Do NOT output the whole file here if possible, and try to write as LITTLE code as needed to describe the change.` + export const chat_systemMessage = (workspaces: string[], runningTerminalIds: string[], mode: ChatMode) => `\ -You are an expert coding ${mode === 'agent' ? 'agent' : 'assistant'} created by Void. Your job is to help the user ${mode === 'agent' ? 'develop, run, and make changes to their project' : 'search and understand their codebase by providing specific references to files and content'}. +You are an expert coding ${mode === 'agent' ? 'agent' : 'assistant'} that runs in the Void code editor. Your job is \ +${mode === 'agent' ? `to help the user develop, run, deploy, and make changes to their codebase. You should ALWAYS bring user's task to completion to the fullest extent possible, making all necessary changes. Do not be lazy.` + : mode === 'gather' ? `to search and understand their codebase by reading files and content and providing references to help with their query.` + : mode === 'normal' ? `to assist the user with their coding tasks.` + : ''} You will be given instructions to follow from the user, \`INSTRUCTIONS\`. You may also be given a list of files that the user has specifically selected, \`SELECTIONS\`. -Please assist the user with their query${mode === 'agent' ? `, bringing the task to completion (make all necessary changes, and do not be lazy)` : ''}. The user's query is never invalid. +Please assist the user with their query. The user's query is never invalid. The user's system information is as follows: - ${os} - Open workspace(s): ${workspaces.join(', ') || 'NO WORKSPACE OPEN'} -${(mode === 'agent' || mode === 'gather') && runningTerminalIds.length !== 0 ? `\ +${(mode === 'agent') && runningTerminalIds.length !== 0 ? `\ - Existing terminal IDs: ${runningTerminalIds.join(', ')} `: '\n'} ${mode === 'agent' || mode === 'gather' /* tool use */ ? `\ @@ -43,9 +48,9 @@ You will be given tools you can call. - NEVER modify a file outside the user's workspace(s) without permission from the user.` : ''} \ `: `\ -You're allowed to ask for more context. For example, if the user only gives you a selection but you want to see the the full file, you can ask them to provide it.\ +You're allowed to ask for more context. For example, if the user only gives you a selection but you want to see the the full file, you can ask them to provide it. +\ `} - ${mode === 'agent' /* code blocks */ ? `\ - Prioritize editing files and running commands over simply making suggestions. - Prioritize taking as many steps as you need to complete your request over stopping early.\ From 9ab3ec3c11b8ded360f2a9c37b8f735c5f870113 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Tue, 18 Mar 2025 00:33:40 -0700 Subject: [PATCH 109/173] add autoapply --- .../contrib/void/browser/chatThreadService.ts | 2 +- .../contrib/void/browser/editCodeService.ts | 24 ++++++++++++++----- .../void/browser/editCodeServiceInterface.ts | 2 +- .../src/markdown/ApplyBlockHoverButtons.tsx | 2 +- .../react/src/sidebar-tsx/SidebarChat.tsx | 21 ++++++++++++++-- .../react/src/void-settings-tsx/Settings.tsx | 23 ++++++++++++++++-- .../contrib/void/browser/toolsService.ts | 2 +- .../contrib/void/common/voidSettingsTypes.ts | 2 ++ 8 files changed, 64 insertions(+), 14 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 8bdbc6b6..85f74703 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -712,7 +712,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { } // 2. if tool requires approval, break from the loop, awaiting approval - const requiresApproval = true // TODO!!! + const requiresApproval = !this._settingsService.state.globalSettings.autoApprove if (requiresApproval && toolNamesThatRequireApproval.has(toolName)) { this._addMessageToThread(threadId, { role: 'tool_request', name: toolName, paramsStr: toolParamsStr, params: toolParams, id: toolId }) return true diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 74ea60ba..8e141547 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -28,7 +28,6 @@ import { IConsistentEditorItemService, IConsistentItemService } from './helperSe import { voidPrefixAndSuffix, ctrlKStream_userMessage, ctrlKStream_systemMessage, defaultQuickEditFimTags, rewriteCode_systemMessage, rewriteCode_userMessage, searchReplace_systemMessage, searchReplace_userMessage, FINAL, ORIGINAL, DIVIDER, tripleTick, } from '../common/prompt/prompts.js'; import { mountCtrlK } from './react/out/quick-edit-tsx/index.js' -import { mountVoidCommandBar } from './react/out/void-command-bar-tsx/index.js' import { QuickEditPropsType } from './quickEditActions.js'; import { IModelContentChangedEvent } from '../../../../editor/common/textModelEvents.js'; import { extractCodeFromFIM, extractCodeFromRegular, ExtractedSearchReplaceBlock, extractSearchReplaceBlocks } from '../common/helpers/extractCodeFromResult.js'; @@ -1558,8 +1557,11 @@ class EditCodeService extends Disposable implements IEditCodeService { const originalFileCode = model.getValue(EndOfLinePreference.LF) const numLines = model.getLineCount() - // reject all diffZones on this URI, adding to history (there can't possibly be overlap after this) - this.removeDiffAreas({ uri, behavior: 'reject', removeCtrlKs: true }) + // Only reject diffZones if we're not using keep-conflicts + if (opts.startBehavior !== 'keep-conflicts') { + // reject all diffZones on this URI, adding to history (there can't possibly be overlap after this) + this.removeDiffAreas({ uri, behavior: 'reject', removeCtrlKs: true }) + } const startLine = 1 const endLine = numLines @@ -1692,7 +1694,7 @@ class EditCodeService extends Disposable implements IEditCodeService { nMessagesSent += 1 if (nMessagesSent >= 5) { - this._notifyError({ message: 'Void Error: Tried to Fast Apply 5 times but failed. Please try again with a smarter model or disable Fast Apply.', fullError: null }) + this._notifyError({ message: 'Tried to Fast Apply 5 times but failed. Please try again with a smarter model or disable Fast Apply.', fullError: null }) onDone() this._undoHistory(uri) break @@ -1732,7 +1734,11 @@ class EditCodeService extends Disposable implements IEditCodeService { else { // starting line is at least the number of lines in the generated code minus 1 const numLinesInOrig = numLinesOfStr(block.orig) - diffZone._streamState.line = Math.max(numLinesInOrig - 1, 1, diffZone._streamState.line ?? 1) + 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 @@ -1746,6 +1752,7 @@ class EditCodeService extends Disposable implements IEditCodeService { // if error if (typeof originalBounds === 'string') { console.log('Error finding text in code:') + console.log('originalFileCode', { originalFileCode }) console.log('fullText', { fullText }) console.log('error:', originalBounds) console.log('block.orig:', block.orig) @@ -1759,6 +1766,7 @@ class EditCodeService extends Disposable implements IEditCodeService { // TODO!!! test this blocks.splice(blockNum, Infinity) // remove all blocks at and after this one oldBlocks = deepClone(blocks) + addedTrackingZoneOfBlockNum.splice(blockNum, Infinity) // also remove corresponding tracking zones // Reset streaming state but preserve line context shouldUpdateOrigStreamStyle = true @@ -2412,7 +2420,11 @@ class AcceptAllRejectAllWidget extends Widget implements IOverlayWidget { // Mount command bar using mountVoidCommandBar this._instantiationService.invokeFunction(accessor => { - mountVoidCommandBar(voidCommandBar, accessor, {}) + console.log(voidCommandBar) + if (voidCommandBar) { // remove this + Math.random() + } + // mountVoidCommandBar(voidCommandBar, accessor, {}) }); // Style accept button diff --git a/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts b/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts index 7fa25ce5..a16aaea2 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts @@ -19,7 +19,7 @@ export type StartApplyingOpts = ({ from: 'ClickApply'; applyStr: string; uri: 'current' | URI; - startBehavior: 'accept-conflicts' | 'reject-conflicts'; + startBehavior: 'accept-conflicts' | 'reject-conflicts' | 'keep-conflicts'; }) 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 e68b3bde..57807247 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 @@ -154,7 +154,7 @@ export const useApplyButtonHTML = ({ codeStr, applyBoxId, uri }: { codeStr: stri from: 'ClickApply', applyStr: codeStr, uri: uri, - startBehavior: 'reject-conflicts', + startBehavior: 'keep-conflicts', }) ?? [] if (!newApplyingUri) console.log('NOT new applying') 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 e84f563e..e369fd58 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 @@ -1147,6 +1147,8 @@ const ToolRequestAcceptRejectButtons = () => { const accessor = useAccessor() const chatThreadsService = accessor.get('IChatThreadService') const metricsService = accessor.get('IMetricsService') + const voidSettingsService = accessor.get('IVoidSettingsService') + const voidSettingsState = useSettingsState() const onAccept = useCallback(() => { try { // this doesn't need to be wrapped in try/catch anymore @@ -1164,6 +1166,11 @@ const ToolRequestAcceptRejectButtons = () => { metricsService.capture('Tool Request Rejected', {}) }, [chatThreadsService, metricsService]) + const onToggleAutoApprove = useCallback((newValue: boolean) => { + voidSettingsService.setGlobalSetting('autoApprove', newValue) + metricsService.capture('Tool Auto-Accept Toggle', { enabled: newValue }) + }, [voidSettingsService, metricsService]) + const approveButton = ( ) - // const isCancelled = state.cancelled || (!isLastMessage && state.awaiting) + const autoApproveToggle = ( +
    + + Auto +
    + ) - return
    + return
    {approveButton} {cancelButton} + {autoApproveToggle}
    } 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 7fa7d183..b0bdd19a 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 @@ -524,12 +524,31 @@ export const FeaturesTab = () => {
    - -
    + {/* Tools Section */} +

    Agent Options

    +
    +
    +

    Tools

    +
    Settings that control Tool behavior.
    + +
    + {/* Auto Accept Switch */} +
    + voidSettingsService.setGlobalSetting('autoApprove', newVal)} + /> + {voidSettingsState.globalSettings.autoApprove ? 'Auto-approve' : 'Ask for approval'} +
    +
    +
    +
    + diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index 8c0a8187..89e4d8ae 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -348,7 +348,7 @@ export class ToolsService implements IToolsService { uri, applyStr: changeDescription, from: 'ClickApply', - startBehavior: 'accept-conflicts', + startBehavior: 'keep-conflicts', }) if (!res) throw new Error(`The Apply model did not start running on ${basename(uri.fsPath)}. Please try again.`) const [_, applyDonePromise] = res diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index 6668610f..acfe57f0 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -388,6 +388,7 @@ export type GlobalSettings = { syncApplyToChat: boolean; enableFastApply: boolean; chatMode: ChatMode; + autoApprove: boolean; } export const defaultGlobalSettings: GlobalSettings = { @@ -397,6 +398,7 @@ export const defaultGlobalSettings: GlobalSettings = { syncApplyToChat: true, enableFastApply: true, chatMode: 'agent', + autoApprove: false, } export type GlobalSettingName = keyof GlobalSettings From b305fc503465b870ccd5cea26a2e0a263786e7d6 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Tue, 18 Mar 2025 02:41:45 -0700 Subject: [PATCH 110/173] keep-conflicts on apply --- .../contrib/void/browser/editCodeService.ts | 395 +++++++++--------- .../void/browser/editCodeServiceInterface.ts | 6 +- .../src/markdown/ApplyBlockHoverButtons.tsx | 4 +- .../react/src/sidebar-tsx/SidebarChat.tsx | 2 +- .../react/src/void-settings-tsx/Settings.tsx | 2 +- 5 files changed, 209 insertions(+), 200 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 8e141547..14a1e006 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -512,11 +512,11 @@ class EditCodeService extends Disposable implements IEditCodeService { const buttonsWidget = new AcceptAllRejectAllWidget({ editor, onAcceptAll: () => { - this.removeDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false }) + this.acceptOrRejectDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false, _addToHistory: true }) this._metricsService.capture('Accept All', {}) }, onRejectAll: () => { - this.removeDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false }) + this.acceptOrRejectDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false, _addToHistory: true }) this._metricsService.capture('Reject All', {}) }, instantiationService: this._instantiationService, @@ -1297,74 +1297,47 @@ class EditCodeService extends Disposable implements IEditCodeService { - private async _initializeWriteoverStream(opts: StartApplyingOpts): Promise<[DiffZone, Promise] | undefined> { - const { from, } = opts - - let startLine: number - let endLine: number - let uri: URI - let currentFileStr: string - - if (from === 'ClickApply') { - const uri_ = this._getActiveEditorURI() - if (!uri_) return - uri = uri_ - await this._voidModelService.initializeModel(uri) - const { model } = this._voidModelService.getModel(uri) - if (!model) return - - currentFileStr = model.getValue(EndOfLinePreference.LF) - const numLines = model.getLineCount() - - // remove all diffZones on this URI, adding to history (there can't possibly be overlap after this) - const behavior: 'accept' | 'reject' = opts.startBehavior === 'accept-conflicts' ? 'accept' : 'reject' - this.removeDiffAreas({ uri, behavior, removeCtrlKs: true }) - - // in ctrl+L the start and end lines are the full document - startLine = 1 - endLine = numLines - } - else if (from === 'QuickEdit') { - const { diffareaid } = opts - const ctrlKZone = this.diffAreaOfId[diffareaid] - if (ctrlKZone.type !== 'CtrlKZone') return - - const { startLine: startLine_, endLine: endLine_, _URI } = ctrlKZone - uri = _URI - await this._voidModelService.initializeModel(uri) - const { model } = this._voidModelService.getModel(uri) - if (!model) return - currentFileStr = model.getValue(EndOfLinePreference.LF) - - startLine = startLine_ - endLine = endLine_ - } - else { - throw new Error(`Void: diff.type not recognized on: ${from}`) - } + private _startStreamingDiffZone({ + uri, + startRange, + startBehavior, + streamRequestIdRef, + onUndo, + }: { + uri: URI, + startRange: 'fullFile' | [number, number], + startBehavior: 'accept-conflicts' | 'reject-conflicts' | 'keep-conflicts', + streamRequestIdRef: { current: string | null }, + onUndo: () => void, + }) { const { model } = this._voidModelService.getModel(uri) if (!model) return - const originalCode = currentFileStr.split('\n').slice((startLine - 1), (endLine - 1) + 1).join('\n') - const language = model.getLanguageId() + const startLine = startRange === 'fullFile' ? 1 : startRange[0] + const endLine = startRange === 'fullFile' ? model.getLineCount() : startRange[1] + let originalCode = startRange === 'fullFile' ? model.getValue(EndOfLinePreference.LF) : model.getValue(EndOfLinePreference.LF).split('\n').slice((startLine - 1), (endLine - 1) + 1).join('\n') - let streamRequestIdRef: { current: string | null } = { current: null } + const diffZones = this._getDiffZonesOnURI(uri) - // promise that resolves when the apply is done - let resApplyPromise: () => void - let rejApplyPromise: (e: any) => void - const applyPromise = new Promise((res_, rej_) => { resApplyPromise = res_; rejApplyPromise = rej_ }) + // clear diffZones so no conflict + if (startBehavior === 'keep-conflicts' && diffZones.length !== 0) { + // delete them then re-apply their change + this.acceptOrRejectDiffAreas({ uri, removeCtrlKs: true, behavior: 'reject', _addToHistory: false }) + const originalCodeReplacement = model.getValue(EndOfLinePreference.LF) // use this as original code + this._writeURIText(uri, originalCode, 'wholeFileRange', { shouldRealignDiffAreas: false }) // un-revert + originalCode = originalCodeReplacement + } + else { + const behavior = startBehavior === 'accept-conflicts' ? 'accept' + : startBehavior === 'reject-conflicts' ? 'reject' + : startBehavior === 'keep-conflicts' ? null // do nothing + : null - - // add to history - const { onFinishEdit } = this._addToHistory(uri, { - onUndo: () => { if (diffZone._streamState.isStreaming) rejApplyPromise(new Error('Edit was interrupted by pressing undo.')) } - }) - - // TODO!!! let users customize modelFimTags - const quickEditFIMTags = defaultQuickEditFimTags + if (behavior) + this.acceptOrRejectDiffAreas({ uri, removeCtrlKs: true, behavior: behavior, _addToHistory: false }) + } const adding: Omit = { type: 'DiffZone', @@ -1380,24 +1353,75 @@ class EditCodeService extends Disposable implements IEditCodeService { _diffOfId: {}, // added later _removeStylesFns: new Set(), } + const diffZone = this._addDiffArea(adding) this._onDidChangeDiffZoneStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) this._onDidAddOrDeleteDiffZones.fire({ uri }) + // add to history + const { onFinishEdit } = this._addToHistory(uri, { onUndo }) + return { diffZone, onFinishEdit } + } - if (from === 'QuickEdit') { + + + + + + private async _initializeWriteoverStream(opts: StartApplyingOpts): Promise<[DiffZone, Promise] | undefined> { + + const { from, } = opts + + let uri: URI + let startRange: 'fullFile' | [number, number] + + if (from === 'ClickApply') { + const uri_ = this._getActiveEditorURI() + if (!uri_) return + uri = uri_ + startRange = 'fullFile' + } + else if (from === 'QuickEdit') { const { diffareaid } = opts const ctrlKZone = this.diffAreaOfId[diffareaid] if (ctrlKZone.type !== 'CtrlKZone') return - - ctrlKZone._linkedStreamingDiffZone = diffZone.diffareaid - this._onDidChangeCtrlKZoneStreaming.fire({ uri, diffareaid: ctrlKZone.diffareaid }) + const { startLine: startLine_, endLine: endLine_, _URI } = ctrlKZone + uri = _URI + startRange = [startLine_, endLine_] + } + else { + throw new Error(`Void: diff.type not recognized on: ${from}`) } - // now handle messages - let messages: LLMChatMessage[] + await this._voidModelService.initializeModel(uri) + const { model } = this._voidModelService.getModel(uri) + if (!model) return + + // promise that resolves when the apply is done + let resApplyPromise: () => void + let rejApplyPromise: (e: any) => void + const applyPromise = new Promise((res_, rej_) => { resApplyPromise = res_; rejApplyPromise = rej_ }) + let streamRequestIdRef: { current: string | null } = { current: null } // can use this as a proxy to set the diffArea's stream state requestId + + // start diffzone + const res = this._startStreamingDiffZone({ + uri, + streamRequestIdRef, + startRange, + startBehavior: opts.startBehavior, + onUndo: () => { if (diffZone._streamState.isStreaming) rejApplyPromise(new Error('Edit was interrupted by pressing undo.')) }, + }) + if (!res) return + const { diffZone, onFinishEdit } = res + + // build messages + const quickEditFIMTags = defaultQuickEditFimTags // TODO can eventually let users customize modelFimTags + const originalFileCode = model.getValue(EndOfLinePreference.LF) + const originalCode = startRange === 'fullFile' ? originalFileCode : originalFileCode.split('\n').slice((startRange[0] - 1), (startRange[1] - 1) + 1).join('\n') + const language = model.getLanguageId() + let messages: LLMChatMessage[] if (from === 'ClickApply') { const userContent = rewriteCode_userMessage({ originalCode, applyStr: opts.applyStr, language }) messages = [ @@ -1412,7 +1436,9 @@ class EditCodeService extends Disposable implements IEditCodeService { const { _mountInfo } = ctrlKZone const instructions = _mountInfo?.textAreaRef.current?.value ?? '' - const { prefix, suffix } = voidPrefixAndSuffix({ fullFileStr: currentFileStr, startLine, endLine }) + const startLine = startRange === 'fullFile' ? 1 : startRange[0] + const endLine = startRange === 'fullFile' ? model.getLineCount() : startRange[1] + const { prefix, suffix } = voidPrefixAndSuffix({ fullFileStr: originalFileCode, startLine, endLine }) const userContent = ctrlKStream_userMessage({ selection: originalCode, instructions: instructions, prefix, suffix, isOllamaFIM: false, fimTags: quickEditFIMTags, language }) // type: 'messages', messages = [ @@ -1423,6 +1449,17 @@ class EditCodeService extends Disposable implements IEditCodeService { else { throw new Error(`featureName ${from} is invalid`) } + // a few items related to writeover streams and quickEdits + if (from === 'QuickEdit') { + const { diffareaid } = opts + const ctrlKZone = this.diffAreaOfId[diffareaid] + if (ctrlKZone.type !== 'CtrlKZone') return + + ctrlKZone._linkedStreamingDiffZone = diffZone.diffareaid + this._onDidChangeCtrlKZoneStreaming.fire({ uri, diffareaid: ctrlKZone.diffareaid }) + } + + // helpers const onDone = () => { console.log('called onDone') diffZone._streamState = { isStreaming: false, } @@ -1439,10 +1476,6 @@ class EditCodeService extends Disposable implements IEditCodeService { onFinishEdit() } - // refresh now in case onText takes a while to get 1st message - this._refreshStylesAndDiffsInURI(uri) - - const extractText = (fullText: string, recentlyAddedTextLen: number) => { if (from === 'QuickEdit') { return extractCodeFromFIM({ text: fullText, recentlyAddedTextLen, midTag: quickEditFIMTags.midTag }) @@ -1453,76 +1486,82 @@ class EditCodeService extends Disposable implements IEditCodeService { throw new Error('Void 1') } - const latestStreamInfoMutable: StreamLocationMutable = { line: diffZone.startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 } + // refresh now in case onText takes a while to get 1st message + this._refreshStylesAndDiffsInURI(uri) + + const latestStreamLocationMutable: StreamLocationMutable = { line: diffZone.startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 } const featureName: FeatureName = opts.from === 'ClickApply' ? 'Apply' : 'Ctrl+K' const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName] const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[modelSelection.providerName]?.[modelSelection.modelName] : undefined const writeover = async () => { + let shouldSendAnotherMessage = true + while (shouldSendAnotherMessage) { + shouldSendAnotherMessage = false - let resMessageDonePromise: () => void = () => { } - const messageDonePromise = new Promise((res_) => { resMessageDonePromise = res_ }) + let resMessageDonePromise: () => void = () => { } + const messageDonePromise = new Promise((res_) => { resMessageDonePromise = res_ }) - // state used in onText: - let fullTextSoFar = '' // so far (INCLUDING ignored suffix) - let prevIgnoredSuffix = '' - let aborted = false + // state used in onText: + let fullTextSoFar = '' // so far (INCLUDING ignored suffix) + let prevIgnoredSuffix = '' + let aborted = false - streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({ - messagesType: 'chatMessages', - logging: { loggingName: `Edit (Writeover) - ${from}` }, - messages, - modelSelection, - modelSelectionOptions, - onText: (params) => { - const { fullText: fullText_ } = params - const newText_ = fullText_.substring(fullTextSoFar.length, Infinity) + streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({ + messagesType: 'chatMessages', + logging: { loggingName: `Edit (Writeover) - ${from}` }, + messages, + modelSelection, + modelSelectionOptions, + onText: (params) => { + const { fullText: fullText_ } = params + const newText_ = fullText_.substring(fullTextSoFar.length, Infinity) - const newText = prevIgnoredSuffix + newText_ // add the previously ignored suffix because it's no longer the suffix! - fullTextSoFar += newText // full text, including ```, etc + const newText = prevIgnoredSuffix + newText_ // add the previously ignored suffix because it's no longer the suffix! + fullTextSoFar += newText // full text, including ```, etc - const [croppedText, deltaCroppedText, croppedSuffix] = extractText(fullTextSoFar, newText.length) - const { endLineInLlmTextSoFar } = this._writeStreamedDiffZoneLLMText(uri, originalCode, croppedText, deltaCroppedText, latestStreamInfoMutable) - diffZone._streamState.line = (diffZone.startLine - 1) + endLineInLlmTextSoFar // change coordinate systems from originalCode to full file + const [croppedText, deltaCroppedText, croppedSuffix] = extractText(fullTextSoFar, newText.length) + const { endLineInLlmTextSoFar } = this._writeStreamedDiffZoneLLMText(uri, originalCode, croppedText, deltaCroppedText, latestStreamLocationMutable) + diffZone._streamState.line = (diffZone.startLine - 1) + endLineInLlmTextSoFar // change coordinate systems from originalCode to full file - this._refreshStylesAndDiffsInURI(uri) + this._refreshStylesAndDiffsInURI(uri) - prevIgnoredSuffix = croppedSuffix - }, - onFinalMessage: (params) => { - const { fullText } = params - // console.log('DONE! FULL TEXT\n', extractText(fullText), diffZone.startLine, diffZone.endLine) - // at the end, re-write whole thing to make sure no sync errors - const [croppedText, _1, _2] = extractText(fullText, 0) - this._writeURIText(uri, croppedText, - { startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed - { shouldRealignDiffAreas: true } - ) + prevIgnoredSuffix = croppedSuffix + }, + onFinalMessage: (params) => { + const { fullText } = params + // console.log('DONE! FULL TEXT\n', extractText(fullText), diffZone.startLine, diffZone.endLine) + // at the end, re-write whole thing to make sure no sync errors + const [croppedText, _1, _2] = extractText(fullText, 0) + this._writeURIText(uri, croppedText, + { startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed + { shouldRealignDiffAreas: true } + ) - onDone() - resMessageDonePromise() - }, - onError: (e) => { - this._notifyError(e) - onDone() - this._undoHistory(uri) - resMessageDonePromise() - }, - onAbort: () => { - // stop the loop to free up the promise, but don't modify state (already handled by whatever stopped it) - resMessageDonePromise() - aborted = true - }, - }) - // should never happen, just for safety - if (streamRequestIdRef.current === null) { return } + onDone() + resMessageDonePromise() + }, + onError: (e) => { + this._notifyError(e) + onDone() + this._undoHistory(uri) + resMessageDonePromise() + }, + onAbort: () => { + // stop the loop to free up the promise, but don't modify state (already handled by whatever stopped it) + resMessageDonePromise() + aborted = true + }, + }) + // should never happen, just for safety + if (streamRequestIdRef.current === null) { return } - await messageDonePromise - if (aborted) { return } - - } + await messageDonePromise + if (aborted) { return } + } // end while + } // end writeover writeover().then(() => { resApplyPromise() @@ -1549,70 +1588,39 @@ class EditCodeService extends Disposable implements IEditCodeService { } await this._voidModelService.initializeModel(uri) - const { model } = this._voidModelService.getModel(uri) if (!model) return - // generate search/replace block text + // promise that resolves when the apply is done + let resApplyDonePromise: () => void + let rejApplyDonePromise: (e: any) => void + const applyDonePromise = new Promise((res_, rej_) => { resApplyDonePromise = res_; rejApplyDonePromise = rej_ }) + let streamRequestIdRef: { current: string | null } = { current: null } // can use this as a proxy to set the diffArea's stream state requestId + + // start diffzone + const res = this._startStreamingDiffZone({ + uri, + streamRequestIdRef, + startRange: 'fullFile', + startBehavior: opts.startBehavior, + onUndo: () => { if (diffZone._streamState.isStreaming) rejApplyDonePromise(new Error('Edit was interrupted by user pressing undo.')) }, + }) + if (!res) return + const { diffZone, onFinishEdit } = res + + // build messages - ask LLM to generate search/replace block text const originalFileCode = model.getValue(EndOfLinePreference.LF) - const numLines = model.getLineCount() - - // Only reject diffZones if we're not using keep-conflicts - if (opts.startBehavior !== 'keep-conflicts') { - // reject all diffZones on this URI, adding to history (there can't possibly be overlap after this) - this.removeDiffAreas({ uri, behavior: 'reject', removeCtrlKs: true }) - } - - const startLine = 1 - const endLine = numLines - const userMessageContent = searchReplace_userMessage({ originalCode: originalFileCode, applyStr: applyStr }) const messages: LLMChatMessage[] = [ { role: 'system', content: searchReplace_systemMessage }, { role: 'user', content: userMessageContent }, ] - // can use this as a proxy to set the diffArea's stream state requestId - let streamRequestIdRef: { current: string | null } = { current: null } - - - // promise that resolves when the apply is done - let resApplyDonePromise: () => void - let rejApplyDonePromise: (e: any) => void - const applyDonePromise = new Promise((res_, rej_) => { resApplyDonePromise = res_; rejApplyDonePromise = rej_ }) - - // add to history - const { onFinishEdit } = this._addToHistory(uri, { - onUndo: () => { if (diffZone._streamState.isStreaming) rejApplyDonePromise(new Error('Edit was interrupted by pressing undo.')) } - }) - - // TODO replace these with whatever block we're on initially if already started (if add caching of apply S/R blocks) - + // helpers type SearchReplaceDiffAreaMetadata = { originalBounds: [number, number], // 1-indexed originalCode: string, } - - - const adding: Omit = { - type: 'DiffZone', - originalCode: originalFileCode, - startLine, - endLine, - _URI: uri, - _streamState: { - isStreaming: true, - streamRequestIdRef, - line: startLine, - }, - _diffOfId: {}, // added later - _removeStylesFns: new Set(), - } - const diffZone = this._addDiffArea(adding) - this._onDidChangeDiffZoneStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) - this._onDidAddOrDeleteDiffZones.fire({ uri }) - - const convertOriginalRangeToFinalRange = (originalRange: readonly [number, number]): [number, number] => { // adjust based on the changes by computing line offset const [originalStart, originalEnd] = originalRange @@ -1651,7 +1659,6 @@ class EditCodeService extends Disposable implements IEditCodeService { } - const onDone = () => { diffZone._streamState = { isStreaming: false, } this._onDidChangeDiffZoneStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) @@ -1667,21 +1674,16 @@ class EditCodeService extends Disposable implements IEditCodeService { // refresh now in case onText takes a while to get 1st message this._refreshStylesAndDiffsInURI(uri) - // stateful - const addedTrackingZoneOfBlockNum: TrackingZone[] = [] - - // stream style related + // stream style related - TODO replace these with whatever block we're on initially if already started (if add caching of apply S/R blocks) let latestStreamLocationMutable: StreamLocationMutable | null = null let shouldUpdateOrigStreamStyle = true - let oldBlocks: ExtractedSearchReplaceBlock[] = [] - + const addedTrackingZoneOfBlockNum: TrackingZone[] = [] const featureName: FeatureName = 'Apply' const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName] const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[modelSelection.providerName]?.[modelSelection.modelName] : undefined - const retryLoop = async () => { // this generates >>>>>>> ORIGINAL <<<<<<< REPLACE blocks and and simultaneously applies it let shouldSendAnotherMessage = true @@ -1692,7 +1694,6 @@ class EditCodeService extends Disposable implements IEditCodeService { while (shouldSendAnotherMessage) { shouldSendAnotherMessage = false nMessagesSent += 1 - if (nMessagesSent >= 5) { this._notifyError({ message: 'Tried to Fast Apply 5 times but failed. Please try again with a smarter model or disable Fast Apply.', fullError: null }) onDone() @@ -1922,7 +1923,7 @@ class EditCodeService extends Disposable implements IEditCodeService { retryLoop().then(() => { console.log('resolving Apply Done') - resApplyDonePromise(); + resApplyDonePromise() // this._noLongerNeedModelReference(uri) }).catch((e) => rejApplyDonePromise(e)) @@ -1983,14 +1984,18 @@ class EditCodeService extends Disposable implements IEditCodeService { } - - - getURIStreamState = ({ uri }: { uri: URI | null }) => { - if (uri === null) return 'idle' - + private _getDiffZonesOnURI(uri: URI) { const diffZones = [...this.diffAreasOfURI[uri.fsPath]?.values() ?? []] .map(diffareaid => this.diffAreaOfId[diffareaid]) .filter(diffArea => !!diffArea && diffArea.type === 'DiffZone') + + return diffZones + } + + getURIStreamState = ({ uri }: { uri: URI | null }) => { + if (uri === null) return 'idle' + const diffZones = this._getDiffZonesOnURI(uri) + const isStreaming = diffZones.find(diffZone => !!diffZone._streamState.isStreaming) const state: URIStreamState = isStreaming ? 'streaming' : (diffZones.length === 0 ? 'idle' : 'acceptRejectAll') @@ -2032,12 +2037,12 @@ class EditCodeService extends Disposable implements IEditCodeService { // remove a batch of diffareas all at once (and handle accept/reject of their diffs) - public removeDiffAreas({ uri, removeCtrlKs, behavior }: { uri: URI, removeCtrlKs: boolean, behavior: 'reject' | 'accept' }) { + public acceptOrRejectDiffAreas: IEditCodeService['acceptOrRejectDiffAreas'] = async ({ uri, behavior, removeCtrlKs, _addToHistory }) => { const diffareaids = this.diffAreasOfURI[uri.fsPath] if ((diffareaids?.size ?? 0) === 0) return // do nothing - const { onFinishEdit } = this._addToHistory(uri) + const { onFinishEdit } = _addToHistory === false ? { onFinishEdit: () => { } } : this._addToHistory(uri) for (const diffareaid of diffareaids ?? []) { const diffArea = this.diffAreaOfId[diffareaid] @@ -2061,6 +2066,8 @@ class EditCodeService extends Disposable implements IEditCodeService { // called on void.acceptDiff public async acceptDiff({ diffid }: { diffid: number }) { + // TODO could use an ITextModelto do this instead, would be much simpler + const diff = this.diffOfId[diffid] if (!diff) return diff --git a/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts b/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts index a16aaea2..b23b9302 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts @@ -11,15 +11,17 @@ import { Diff, DiffArea } from './editCodeService.js'; +export type StartBehavior = 'accept-conflicts' | 'reject-conflicts' | 'keep-conflicts' export type StartApplyingOpts = ({ from: 'QuickEdit'; diffareaid: number; // id of the CtrlK area (contains text selection) + startBehavior: StartBehavior; } | { from: 'ClickApply'; applyStr: string; uri: 'current' | URI; - startBehavior: 'accept-conflicts' | 'reject-conflicts' | 'keep-conflicts'; + startBehavior: StartBehavior; }) @@ -50,7 +52,7 @@ export interface IEditCodeService { addCtrlKZone(opts: AddCtrlKOpts): number | undefined; removeCtrlKZone(opts: { diffareaid: number }): void; - removeDiffAreas(opts: { uri: URI, removeCtrlKs: boolean, behavior: 'reject' | 'accept' }): void; + acceptOrRejectDiffAreas(opts: { uri: URI, removeCtrlKs: boolean, behavior: 'reject' | 'accept', _addToHistory?: boolean }): void; onDidAddOrDeleteDiffZones: Event<{ uri: URI }>; onDidAddOrDeleteDiffInDiffZone: Event<{ uri: URI }>; 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 57807247..e1b01e52 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 @@ -177,12 +177,12 @@ export const useApplyButtonHTML = ({ codeStr, applyBoxId, uri }: { codeStr: stri const onAccept = useCallback(() => { const uri = getUriBeingApplied() - if (uri) editCodeService.removeDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false }) + if (uri) editCodeService.acceptOrRejectDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false }) }, [getUriBeingApplied, editCodeService]) const onReject = useCallback(() => { const uri = getUriBeingApplied() - if (uri) editCodeService.removeDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false }) + if (uri) editCodeService.acceptOrRejectDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false }) }, [getUriBeingApplied, editCodeService]) const onReapply = useCallback(() => { 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 e369fd58..45d53956 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 @@ -1118,7 +1118,7 @@ const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName return getBasename(toolParams.uri.fsPath); } else if (toolName === 'list_dir') { const toolParams = _toolParams as ToolCallParams['list_dir'] - return `${getBasename(toolParams.rootURI.fsPath)}/`; + return `${getFolderName(toolParams.rootURI.fsPath)}`; } else if (toolName === 'pathname_search') { const toolParams = _toolParams as ToolCallParams['pathname_search'] return `"${toolParams.queryStr}"`; 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 b0bdd19a..979ff991 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 @@ -533,7 +533,7 @@ export const FeaturesTab = () => {

    Tools

    -
    Settings that control Tool behavior.
    +
    Tools that can modify files on your computer require approval by default.
    {/* Auto Accept Switch */} From 6856e23e2a629ad1ad7b561fae260fac8346831b Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Tue, 18 Mar 2025 03:35:54 -0700 Subject: [PATCH 111/173] just revert all SEARCH/REPLACE on error + history reorder --- .../contrib/void/browser/editCodeService.ts | 100 +++++++----------- .../void/browser/editCodeServiceInterface.ts | 2 +- .../src/markdown/ApplyBlockHoverButtons.tsx | 4 +- 3 files changed, 42 insertions(+), 64 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 14a1e006..a18f4a2a 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, FINAL, ORIGINAL, DIVIDER, tripleTick, } from '../common/prompt/prompts.js'; +import { voidPrefixAndSuffix, ctrlKStream_userMessage, ctrlKStream_systemMessage, defaultQuickEditFimTags, rewriteCode_systemMessage, rewriteCode_userMessage, searchReplace_systemMessage, searchReplace_userMessage, } from '../common/prompt/prompts.js'; import { mountCtrlK } from './react/out/quick-edit-tsx/index.js' import { QuickEditPropsType } from './quickEditActions.js'; @@ -512,11 +512,11 @@ class EditCodeService extends Disposable implements IEditCodeService { const buttonsWidget = new AcceptAllRejectAllWidget({ editor, onAcceptAll: () => { - this.acceptOrRejectDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false, _addToHistory: true }) + this.acceptOrRejectAllDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false, _addToHistory: true }) this._metricsService.capture('Accept All', {}) }, onRejectAll: () => { - this.acceptOrRejectDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false, _addToHistory: true }) + this.acceptOrRejectAllDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false, _addToHistory: true }) this._metricsService.capture('Reject All', {}) }, instantiationService: this._instantiationService, @@ -1319,24 +1319,20 @@ class EditCodeService extends Disposable implements IEditCodeService { const endLine = startRange === 'fullFile' ? model.getLineCount() : startRange[1] let originalCode = startRange === 'fullFile' ? model.getValue(EndOfLinePreference.LF) : model.getValue(EndOfLinePreference.LF).split('\n').slice((startLine - 1), (endLine - 1) + 1).join('\n') - const diffZones = this._getDiffZonesOnURI(uri) + // add to history as a checkpoint, before we start modifying + const { onFinishEdit } = this._addToHistory(uri, { onUndo }) // clear diffZones so no conflict - if (startBehavior === 'keep-conflicts' && diffZones.length !== 0) { + if (startBehavior === 'keep-conflicts') { // delete them then re-apply their change - this.acceptOrRejectDiffAreas({ uri, removeCtrlKs: true, behavior: 'reject', _addToHistory: false }) + this.acceptOrRejectAllDiffAreas({ uri, removeCtrlKs: true, behavior: 'reject', _addToHistory: false }) const originalCodeReplacement = model.getValue(EndOfLinePreference.LF) // use this as original code this._writeURIText(uri, originalCode, 'wholeFileRange', { shouldRealignDiffAreas: false }) // un-revert originalCode = originalCodeReplacement } - else { - const behavior = startBehavior === 'accept-conflicts' ? 'accept' - : startBehavior === 'reject-conflicts' ? 'reject' - : startBehavior === 'keep-conflicts' ? null // do nothing - : null - - if (behavior) - this.acceptOrRejectDiffAreas({ uri, removeCtrlKs: true, behavior: behavior, _addToHistory: false }) + else if (startBehavior === 'accept-conflicts' || startBehavior === 'reject-conflicts') { + const behavior = startBehavior === 'accept-conflicts' ? 'accept' : 'reject' + this.acceptOrRejectAllDiffAreas({ uri, removeCtrlKs: true, behavior, _addToHistory: false }) } const adding: Omit = { @@ -1358,8 +1354,8 @@ class EditCodeService extends Disposable implements IEditCodeService { this._onDidChangeDiffZoneStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) this._onDidAddOrDeleteDiffZones.fire({ uri }) - // add to history - const { onFinishEdit } = this._addToHistory(uri, { onUndo }) + + console.log('DONE _STREAMING', diffZone) return { diffZone, onFinishEdit } } @@ -1642,19 +1638,18 @@ class EditCodeService extends Disposable implements IEditCodeService { const errMsgOfInvalidStr = (str: string & ReturnType, blockOrig: string, blockNum: number, blocks: ExtractedSearchReplaceBlock[]) => { const descStr = str === `Not found` ? - `The most recent ORIGINAL code could not be found in the file, so you were interrupted. You should make sure the text in ORIGINAL matches lines of code EXACTLY. Erroneous ORIGINAL code:\n${JSON.stringify(blockOrig)}` + `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. Please make the ORIGINAL excerpt bigger so it's unique. Erroneous ORIGINAL code:\n${JSON.stringify(blockOrig)}` + `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)}` : `` // 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}` : ''}` - - console.log('ERr msg:', errMsg) + // 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.' + const errMsg = `${descStr}\n${soFarStr}` return errMsg } @@ -1679,6 +1674,8 @@ class EditCodeService extends Disposable implements IEditCodeService { let shouldUpdateOrigStreamStyle = true let oldBlocks: ExtractedSearchReplaceBlock[] = [] const addedTrackingZoneOfBlockNum: TrackingZone[] = [] + diffZone._streamState.line = 1 + const featureName: FeatureName = 'Apply' const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName] @@ -1732,15 +1729,16 @@ class EditCodeService extends Disposable implements IEditCodeService { shouldUpdateOrigStreamStyle = false } } - else { - // 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) - } - } + + // // 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 } @@ -1763,32 +1761,12 @@ class EditCodeService extends Disposable implements IEditCodeService { { role: 'user', content: content } // user explanation of what's wrong ) - // REVERT THIS ONE BLOCK - // TODO!!! test this - blocks.splice(blockNum, Infinity) // remove all blocks at and after this one - oldBlocks = deepClone(blocks) - addedTrackingZoneOfBlockNum.splice(blockNum, Infinity) // also remove corresponding tracking zones - - // Reset streaming state but preserve line context + // REVERT ALL BLOCKS + latestStreamLocationMutable = null shouldUpdateOrigStreamStyle = true - // initialize with the last known good position - if (blockNum > 0 && blockNum - 1 < addedTrackingZoneOfBlockNum.length) { - const lastGoodZone = addedTrackingZoneOfBlockNum[blockNum - 1]; - latestStreamLocationMutable = { - line: lastGoodZone.endLine + 1, - addedSplitYet: false, - col: 1, - originalCodeStartLine: 1 - }; - } else { - // If we're at the first block, reset to beginning - latestStreamLocationMutable = { - line: diffZone.startLine, - addedSplitYet: false, - col: 1, - originalCodeStartLine: 1 - }; - } + oldBlocks = [] + addedTrackingZoneOfBlockNum.splice(0, Infinity) + this._writeURIText(uri, originalFileCode, 'wholeFileRange', { shouldRealignDiffAreas: true }) // abort and resolve shouldSendAnotherMessage = true @@ -1796,8 +1774,8 @@ class EditCodeService extends Disposable implements IEditCodeService { weAreAborting = true this._llmMessageService.abort(streamRequestIdRef.current) weAreAborting = false - resMessageDonePromise() } + resMessageDonePromise() this._refreshStylesAndDiffsInURI(uri) return } @@ -2037,7 +2015,7 @@ class EditCodeService extends Disposable implements IEditCodeService { // remove a batch of diffareas all at once (and handle accept/reject of their diffs) - public acceptOrRejectDiffAreas: IEditCodeService['acceptOrRejectDiffAreas'] = async ({ uri, behavior, removeCtrlKs, _addToHistory }) => { + public acceptOrRejectAllDiffAreas: IEditCodeService['acceptOrRejectAllDiffAreas'] = async ({ uri, behavior, removeCtrlKs, _addToHistory }) => { const diffareaids = this.diffAreasOfURI[uri.fsPath] if ((diffareaids?.size ?? 0) === 0) return // do nothing diff --git a/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts b/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts index b23b9302..1acae2c7 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts @@ -52,7 +52,7 @@ export interface IEditCodeService { addCtrlKZone(opts: AddCtrlKOpts): number | undefined; removeCtrlKZone(opts: { diffareaid: number }): void; - acceptOrRejectDiffAreas(opts: { uri: URI, removeCtrlKs: boolean, behavior: 'reject' | 'accept', _addToHistory?: boolean }): void; + acceptOrRejectAllDiffAreas(opts: { uri: URI, removeCtrlKs: boolean, behavior: 'reject' | 'accept', _addToHistory?: boolean }): void; onDidAddOrDeleteDiffZones: Event<{ uri: URI }>; onDidAddOrDeleteDiffInDiffZone: Event<{ uri: URI }>; 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 e1b01e52..62a20997 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 @@ -177,12 +177,12 @@ export const useApplyButtonHTML = ({ codeStr, applyBoxId, uri }: { codeStr: stri const onAccept = useCallback(() => { const uri = getUriBeingApplied() - if (uri) editCodeService.acceptOrRejectDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false }) + if (uri) editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false }) }, [getUriBeingApplied, editCodeService]) const onReject = useCallback(() => { const uri = getUriBeingApplied() - if (uri) editCodeService.acceptOrRejectDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false }) + if (uri) editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false }) }, [getUriBeingApplied, editCodeService]) const onReapply = useCallback(() => { From b26b62998d2545a3857c343d1c0f4115ba3baeec Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Tue, 18 Mar 2025 03:42:53 -0700 Subject: [PATCH 112/173] settings page --- .../react/src/void-settings-tsx/Settings.tsx | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) 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 979ff991..b242f5f4 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 @@ -470,7 +470,8 @@ export const FeaturesTab = () => {

    Feature Options

    -
    + {/* L1 */} +
    {/* FIM */}

    {displayInfoOfFeatureName('Autocomplete')}

    @@ -528,12 +529,13 @@ export const FeaturesTab = () => {
    - {/* Tools Section */} -

    Agent Options

    -
    + {/* L2 */} +
    + + {/* Tools Section */}

    Tools

    -
    Tools that can modify files on your computer require approval by default.
    +
    If a tool can modify files on your computer, it requires approval by default.
    {/* Auto Accept Switch */} @@ -543,13 +545,19 @@ export const FeaturesTab = () => { value={voidSettingsState.globalSettings.autoApprove} onChange={(newVal) => voidSettingsService.setGlobalSetting('autoApprove', newVal)} /> - {voidSettingsState.globalSettings.autoApprove ? 'Auto-approve' : 'Ask for approval'} + {voidSettingsState.globalSettings.autoApprove ? 'Auto-approve' : 'User-approve'}
    + +
    +
    +
    +
    + From 730758cbec220806311395ef2318212f3886bf41 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Tue, 18 Mar 2025 03:43:10 -0700 Subject: [PATCH 113/173] small changes.. --- .../void-command-bar-tsx/VoidCommandBar.tsx | 36 ++++++------------- 1 file changed, 10 insertions(+), 26 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx index 9f494909..6783be26 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------*/ -import { useAccessor, useIsDark } from '../util/services.js'; +import { useAccessor, useIsDark, useUriState } from '../util/services.js'; import '../styles.css' import { DiffZone } from '../../../editCodeService.js'; @@ -39,16 +39,7 @@ const VoidCommandBar = () => { const [diffIdxOfFspath, setDiffIdxOfFspath] = useState>({}) // const [currentUriIdx, setCurrentUriIdx] = useState(-1) // we are doing O(n) search for this - const getCurrentUri = useCallback(() => { - const editor = editorService.getActiveCodeEditor() - if (!editor) return null - const uri = editor.getModel()?.uri - if (!uri) return null - return uri - }, [editorService]) - - const diffZones: DiffZone[] = [] - + const { currentUri } = useUriState() // trigger rerender when diffzone is created (TODO need to also update when diff is accepted/rejected) useEffect(() => { @@ -58,11 +49,7 @@ const VoidCommandBar = () => { return () => disposable.dispose() }, [editCodeService, rerender]) - const getNextDiff = useCallback(({ step }: { step: 1 | -1 }) => { - - const currentUri = getCurrentUri() - if (!currentUri) { return; } @@ -78,9 +65,9 @@ const VoidCommandBar = () => { const nextDiff = sortedDiffs[nextDiffIdx] - return { nextDiff, nextDiffIdx, } + return { nextDiff, nextDiffIdx } - }, [getCurrentUri, editCodeService._sortedDiffsOfFspath, diffIdxOfFspath]) + }, [currentUri, editCodeService._sortedDiffsOfFspath, diffIdxOfFspath]) const getNextUri = useCallback(({ step }: { step: 1 | -1 }) => { @@ -89,8 +76,6 @@ const VoidCommandBar = () => { return; } - const currentUri = getCurrentUri() - const defaultUriIdx = step === 1 ? -1 : 0 // defaults: if next, currentIdx = -1; if prev, currentIdx = 0 let currentUriIdx = -1 if (currentUri) { @@ -104,15 +89,14 @@ const VoidCommandBar = () => { const nextUriIdx = (currentUriIdx + step) % sortedUris.length const nextUri = sortedUris[nextUriIdx] - return { nextUri, nextUriIdx, } - - }, [getCurrentUri, editCodeService._sortedUrisWithDiffs]) + return { nextUri, nextUriIdx } + }, [currentUri, editCodeService._sortedUrisWithDiffs]) const gotoNextDiff = ({ step }: { step: 1 | -1 }) => { // get the next diff - const res = getNextDiff({ step: 1 }) + const res = getNextDiff({ step }) if (!res) return; // scroll to the next diff @@ -132,7 +116,7 @@ const VoidCommandBar = () => { const gotoNextUri = ({ step }: { step: 1 | -1 }) => { // get the next uri - const res = getNextUri({ step: 1 }) + const res = getNextUri({ step }) if (!res) return; const { nextUri, nextUriIdx } = res; @@ -197,8 +181,8 @@ const VoidCommandBar = () => {
    -
    File {(editCodeService._sortedUrisWithDiffs.findIndex(u => u.fsPath === getCurrentUri()?.fsPath) ?? 0) + 1} of {editCodeService._sortedUrisWithDiffs.length}
    -
    Diff {(diffIdxOfFspath[getCurrentUri()?.fsPath ?? ''] ?? 0) + 1} of {editCodeService._sortedDiffsOfFspath[getCurrentUri()?.fsPath ?? '']?.length ?? 0}
    +
    File {(editCodeService._sortedUrisWithDiffs.findIndex(u => u.fsPath === currentUri?.fsPath) ?? 0) + 1} of {editCodeService._sortedUrisWithDiffs.length}
    +
    Diff {(diffIdxOfFspath[currentUri?.fsPath ?? ''] ?? 0) + 1} of {editCodeService._sortedDiffsOfFspath[currentUri?.fsPath ?? '']?.length ?? 0}
    From b9405a76cdee5bcf08526868e001ffdc2746df49 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Tue, 18 Mar 2025 04:51:23 -0700 Subject: [PATCH 114/173] auto-approve --- .../void/browser/react/src/sidebar-tsx/SidebarChat.tsx | 4 ++-- .../void/browser/react/src/void-settings-tsx/Settings.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) 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 45d53956..a03d2787 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 @@ -1204,13 +1204,13 @@ const ToolRequestAcceptRejectButtons = () => { ) const autoApproveToggle = ( -
    +
    - Auto + Auto-approve
    ) 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 b242f5f4..1a6b6d56 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 @@ -545,7 +545,7 @@ export const FeaturesTab = () => { value={voidSettingsState.globalSettings.autoApprove} onChange={(newVal) => voidSettingsService.setGlobalSetting('autoApprove', newVal)} /> - {voidSettingsState.globalSettings.autoApprove ? 'Auto-approve' : 'User-approve'} + {voidSettingsState.globalSettings.autoApprove ? 'Auto-approve' : 'Auto-approve'}
    From 86fddaa0e0e69bff7dfa6801ae2c9bee2de24dcd Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Tue, 18 Mar 2025 04:56:59 -0700 Subject: [PATCH 115/173] text_search --- .../workbench/contrib/void/browser/aiRegexService.ts | 4 ++-- .../contrib/void/browser/chatThreadService.ts | 8 ++++---- .../void/browser/react/src/sidebar-tsx/SidebarChat.tsx | 10 +++++----- .../workbench/contrib/void/common/toolsServiceTypes.ts | 8 ++++---- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/aiRegexService.ts b/src/vs/workbench/contrib/void/browser/aiRegexService.ts index 4b2a919a..6495a310 100644 --- a/src/vs/workbench/contrib/void/browser/aiRegexService.ts +++ b/src/vs/workbench/contrib/void/browser/aiRegexService.ts @@ -34,7 +34,7 @@ // // const result = await new Promise((res, rej) => { // // sendLLMMessage({ // // messages, -// // tools: ['search'], +// // tools: ['text_search'], // // onFinalMessage: ({ result: r, }) => { // // res(r) // // }, @@ -73,7 +73,7 @@ // // const result = new Promise((res, rej) => { // // sendLLMMessage({ // // messages, -// // tools: ['search'], +// // tools: ['text_search'], // // onResult: (r) => { // // res(r) // // } diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 85f74703..0a0da257 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -324,7 +324,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { { role: 'tool', - name: 'search', + name: 'text_search', id: 'tool-4', paramsStr: '{"query": "function main"}', content: 'Found matches in 3 files', @@ -340,15 +340,15 @@ class ChatThreadService extends Disposable implements IChatThreadService { hasNextPage: false } }, - } satisfies ToolMessage<'search'>, + } satisfies ToolMessage<'text_search'>, // { // role: 'tool_request', - // name: 'search', + // name: 'text_search', // params: { queryStr: 'function main', pageNumber: 0 }, // paramsStr: '{"query": "function main"}', // id: 'request-4', - // } satisfies ToolRequestApproval<'search'>, + // } satisfies ToolRequestApproval<'text_search'>, // --- 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 a03d2787..671e4571 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 @@ -1100,7 +1100,7 @@ const toolNameToTitle = { 'read_file': { past: 'Read file', proposed: 'Read file' }, 'list_dir': { past: 'Inspected folder', proposed: 'Inspect folder' }, 'pathname_search': { past: 'Searched by file name', proposed: 'Search by file name' }, - 'search': { past: 'Searched', proposed: 'Search' }, + 'text_search': { past: 'Searched', proposed: 'Search text' }, 'create_uri': { past: (isFolder: boolean) => `Created ${folderFileStr(isFolder)}`, proposed: (isFolder: boolean) => `Create ${folderFileStr(isFolder)}` }, 'delete_uri': { past: (isFolder: boolean) => `Deleted ${folderFileStr(isFolder)}`, proposed: (isFolder: boolean) => `Delete ${folderFileStr(isFolder)}` }, 'edit': { past: 'Edited file', proposed: 'Edit file' }, @@ -1122,8 +1122,8 @@ const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName } else if (toolName === 'pathname_search') { const toolParams = _toolParams as ToolCallParams['pathname_search'] return `"${toolParams.queryStr}"`; - } else if (toolName === 'search') { - const toolParams = _toolParams as ToolCallParams['search'] + } else if (toolName === 'text_search') { + const toolParams = _toolParams as ToolCallParams['text_search'] return `"${toolParams.queryStr}"`; } else if (toolName === 'create_uri') { const toolParams = _toolParams as ToolCallParams['create_uri'] @@ -1380,7 +1380,7 @@ const toolNameToComponent: { [T in ToolName]: { return } }, - 'search': { + 'text_search': { requestWrapper: null, resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() @@ -1881,7 +1881,7 @@ export const SidebarChat = () => { > { chatThreadsService.setCurrentlyFocusedMessageIdx(undefined) }} diff --git a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts index 2c8e72da..ce8b2b95 100644 --- a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts @@ -69,8 +69,8 @@ export const voidTools = { required: ['query'], }, - search: { - name: 'search', + text_search: { + name: 'text_search', description: `Returns pathnames of files with an exact match of the query. The query can be any regex. This does NOT search pathname. As a follow-up, you may want to use read_file to view the full file contents of the results. ${paginationHelper.desc}`, params: { query: { type: 'string', description: undefined }, @@ -145,7 +145,7 @@ export type ToolCallParams = { 'read_file': { uri: URI, pageNumber: number }, 'list_dir': { rootURI: URI, pageNumber: number }, 'pathname_search': { queryStr: string, pageNumber: number }, - 'search': { queryStr: string, pageNumber: number }, + 'text_search': { queryStr: string, pageNumber: number }, // --- 'edit': { uri: URI, changeDescription: string }, 'create_uri': { uri: URI, isFolder: boolean }, @@ -158,7 +158,7 @@ export type ToolResultType = { 'read_file': { fileContents: string, hasNextPage: boolean }, 'list_dir': { children: ToolDirectoryItem[] | null, hasNextPage: boolean, hasPrevPage: boolean, itemsRemaining: number }, 'pathname_search': { uris: URI[], hasNextPage: boolean }, - 'search': { uris: URI[], hasNextPage: boolean }, + 'text_search': { uris: URI[], hasNextPage: boolean }, // --- 'edit': {}, 'create_uri': {}, From ad471a3015d2de1e028f678bf8bd5e83d21ec19a Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Tue, 18 Mar 2025 21:22:13 -0700 Subject: [PATCH 116/173] fixes --- src/vs/workbench/contrib/void/browser/editCodeService.ts | 5 +++++ src/vs/workbench/contrib/void/browser/toolsService.ts | 6 +++--- src/vs/workbench/contrib/void/common/prompt/prompts.ts | 1 + 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index a18f4a2a..ab08106a 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -1775,6 +1775,7 @@ class EditCodeService extends Disposable implements IEditCodeService { this._llmMessageService.abort(streamRequestIdRef.current) weAreAborting = false } + diffZone._streamState.line = 1 resMessageDonePromise() this._refreshStylesAndDiffsInURI(uri) return @@ -1850,6 +1851,10 @@ class EditCodeService extends Disposable implements IEditCodeService { } // writeover the whole file let newCode = originalFileCode + + // IMPORTANT - sort by lineNum + addedTrackingZoneOfBlockNum.sort((a, b) => a.metadata.originalBounds[0] - b.metadata.originalBounds[0]) + for (let blockNum = addedTrackingZoneOfBlockNum.length - 1; blockNum >= 0; blockNum -= 1) { const { originalBounds } = addedTrackingZoneOfBlockNum[blockNum].metadata const finalCode = blocks[blockNum].final diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index 89e4d8ae..ba7a5201 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -220,7 +220,7 @@ export class ToolsService implements IToolsService { return { queryStr, pageNumber } }, - search: async (params: string) => { + text_search: async (params: string) => { const o = validateJSON(params) const { query: queryUnknown, pageNumber: pageNumberUnknown } = o @@ -307,7 +307,7 @@ export class ToolsService implements IToolsService { return { uris, hasNextPage } }, - search: async ({ queryStr, pageNumber }) => { + text_search: async ({ queryStr, pageNumber }) => { const query = queryBuilder.text({ pattern: queryStr, isRegExp: true, @@ -376,7 +376,7 @@ export class ToolsService implements IToolsService { pathname_search: (params, result) => { return result.uris.map(uri => uri.fsPath).join('\n') + nextPageStr(result.hasNextPage) }, - search: (params, result) => { + text_search: (params, result) => { return result.uris.map(uri => uri.fsPath).join('\n') + nextPageStr(result.hasNextPage) }, // --- diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index 4ba61b6d..72f02707 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -42,6 +42,7 @@ ${(mode === 'agent') && runningTerminalIds.length !== 0 ? `\ ${mode === 'agent' || mode === 'gather' /* tool use */ ? `\ You will be given tools you can call. - Only use tools if they help you accomplish the user's goal. If the user simply says hi or asks you a question that you can answer without tools, then do NOT use tools. +- ALWAYS use tools to take actions. For example, if you would like to edit a file, you MUST use a tool. - If you think you should use tools, you do not need to ask for permission. Feel free to call tools whenever you'd like. You can use them to understand the codebase, ${mode === 'agent' ? 'run terminal commands, edit files, ' : 'gather relevant files and information, '}etc. - NEVER refer to a tool by name when speaking with the user (NEVER say something like "I'm going to use \`tool_name\`"). Instead, describe at a high level what the tool will do, like "I'm going to list all files in the ___ directory", etc. Also do not refer to "pages" of results, just say you're getting more results. - Some tools only work if the user has a workspace open.${mode === 'agent' ? ` From ec505b653bc2183c12a200991a12a67133159f00 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Tue, 18 Mar 2025 22:35:57 -0700 Subject: [PATCH 117/173] debug fastapply mutiple changes (unsolved) --- src/vs/workbench/contrib/void/browser/editCodeService.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index ab08106a..799d065e 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -1855,6 +1855,11 @@ class EditCodeService extends Disposable implements IEditCodeService { // IMPORTANT - sort by lineNum addedTrackingZoneOfBlockNum.sort((a, b) => a.metadata.originalBounds[0] - b.metadata.originalBounds[0]) + const { model } = this._voidModelService.getModel(uri) + console.log('CURRENT\n', model?.getValue()) + console.log('ADDED', addedTrackingZoneOfBlockNum) + console.log('BLOX', blocks) + for (let blockNum = addedTrackingZoneOfBlockNum.length - 1; blockNum >= 0; blockNum -= 1) { const { originalBounds } = addedTrackingZoneOfBlockNum[blockNum].metadata const finalCode = blocks[blockNum].final From 744d387fe11ba35e09900b5723160f5f54ac2682 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Wed, 19 Mar 2025 16:14:59 -0700 Subject: [PATCH 118/173] editCodeService and voidCommandBar service!!! + VoidCommandBar.tsx --- ...CheckService.ts => _markerCheckService.ts} | 0 .../contrib/void/browser/editCodeService.ts | 482 +++++------------- .../void/browser/editCodeServiceInterface.ts | 20 +- .../helperServices/consistentItemService.ts | 2 +- .../void/browser/metricsPollService.ts | 5 +- .../src/markdown/ApplyBlockHoverButtons.tsx | 45 +- .../react/src/markdown/ChatMarkdownRender.tsx | 2 +- .../src/quick-edit-tsx/QuickEditChat.tsx | 1 + .../react/src/sidebar-tsx/SidebarChat.tsx | 14 +- .../react/src/util/mountFnGenerator.tsx | 17 +- .../void/browser/react/src/util/services.tsx | 96 ++-- .../void-command-bar-tsx/VoidCommandBar.tsx | 309 ++++++----- .../contrib/void/browser/sidebarActions.ts | 46 +- .../contrib/void/browser/sidebarPane.ts | 6 +- .../contrib/void/browser/toolsService.ts | 1 - .../contrib/void/browser/void.contribution.ts | 2 + .../void/browser/voidCommandBarService.ts | 419 +++++++++++++++ .../contrib/void/browser/voidSettingsPane.ts | 6 +- .../void/browser/voidUriStateService.ts | 57 --- .../contrib/void/common/helpers/colors.ts | 8 + 20 files changed, 864 insertions(+), 674 deletions(-) rename src/vs/workbench/contrib/void/browser/{MarkerCheckService.ts => _markerCheckService.ts} (100%) create mode 100644 src/vs/workbench/contrib/void/browser/voidCommandBarService.ts delete mode 100644 src/vs/workbench/contrib/void/browser/voidUriStateService.ts create mode 100644 src/vs/workbench/contrib/void/common/helpers/colors.ts diff --git a/src/vs/workbench/contrib/void/browser/MarkerCheckService.ts b/src/vs/workbench/contrib/void/browser/_markerCheckService.ts similarity index 100% rename from src/vs/workbench/contrib/void/browser/MarkerCheckService.ts rename to src/vs/workbench/contrib/void/browser/_markerCheckService.ts diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 799d065e..7d3b20f9 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -3,10 +3,10 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { ICodeEditor, IOverlayWidget, IViewZone, OverlayWidgetPositionPreference } from '../../../../editor/browser/editorBrowser.js'; +import { ICodeEditor, IOverlayWidget, IViewZone } from '../../../../editor/browser/editorBrowser.js'; // import { IUndoRedoService } from '../../../../platform/undoRedo/common/undoRedo.js'; import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; @@ -40,12 +40,13 @@ import { ICommandService } from '../../../../platform/commands/common/commands.j import { ILLMMessageService } from '../common/sendLLMMessageService.js'; import { LLMChatMessage, OnError, errorDetails } from '../common/sendLLMMessageTypes.js'; import { IMetricsService } from '../common/metricsService.js'; -import { IEditCodeService, URIStreamState, AddCtrlKOpts, StartApplyingOpts } from './editCodeServiceInterface.js'; +import { IEditCodeService, AddCtrlKOpts, StartApplyingOpts } from './editCodeServiceInterface.js'; import { IVoidSettingsService } from '../common/voidSettingsService.js'; import { FeatureName } from '../common/voidSettingsTypes.js'; import { IVoidModelService } from '../common/voidModelService.js'; import { ITextFileService } from '../../../services/textfile/common/textfiles.js'; import { deepClone } from '../../../../base/common/objects.js'; +import { acceptBg, acceptBorder, buttonFontSize, buttonTextColor, rejectBg, rejectBorder } from '../common/helpers/colors.js'; const configOfBG = (color: Color) => { return { dark: color, light: color, hcDark: color, hcLight: color, } @@ -236,32 +237,29 @@ type StreamLocationMutable = { line: number, col: number, addedSplitYet: boolean class EditCodeService extends Disposable implements IEditCodeService { _serviceBrand: undefined; - // URI <--> model diffAreasOfURI: Record | undefined> = {}; // uri -> diffareaId diffAreaOfId: Record = {}; // diffareaId -> diffArea diffOfId: Record = {}; // diffid -> diff (redundant with diffArea._diffOfId) - _sortedUrisWithDiffs: URI[] = [] // derivative of diffAreaOfId (computed from it) - _sortedDiffsOfFspath: { [uriString: string]: Diff[] | undefined } = {} // derivative of diffAreaOfId (computed from it) - // only applies to diffZones - // streamingDiffZones: Set = new Set() - private readonly _onDidChangeDiffZoneStreaming = new Emitter<{ uri: URI; diffareaid: number }>(); + // events + + + // uri: diffZones // listen on change diffZones private readonly _onDidAddOrDeleteDiffZones = new Emitter<{ uri: URI }>(); onDidAddOrDeleteDiffZones = this._onDidAddOrDeleteDiffZones.event; - private readonly _onDidFinishAddOrDeleteDiffInDiffZone = new Emitter<{ uri: URI }>(); - onDidAddOrDeleteDiffInDiffZone = this._onDidFinishAddOrDeleteDiffInDiffZone.event; - - private readonly _onDidChangeCtrlKZoneStreaming = new Emitter<{ uri: URI; diffareaid: number }>(); - onDidChangeCtrlKZoneStreaming = this._onDidChangeCtrlKZoneStreaming.event - - private readonly _onDidChangeURIStreamState = new Emitter<{ uri: URI; state: URIStreamState }>(); - onDidChangeURIStreamState = this._onDidChangeURIStreamState.event - + // diffZone: [uri], diffs, isStreaming // listen on change diffs, change streaming (uri is const) + private readonly _onDidChangeDiffsInDiffZone = new Emitter<{ uri: URI, diffareaid: number }>(); + private readonly _onDidChangeStreamingInDiffZone = new Emitter<{ uri: URI, diffareaid: number }>(); + onDidChangeDiffsInDiffZone = this._onDidChangeDiffsInDiffZone.event; + onDidChangeStreamingInDiffZone = this._onDidChangeStreamingInDiffZone.event; + // ctrlKZone: [uri], isStreaming // listen on change streaming + private readonly _onDidChangeStreamingInCtrlKZone = new Emitter<{ uri: URI; diffareaid: number }>(); + onDidChangeStreamingInCtrlKZone = this._onDidChangeStreamingInCtrlKZone.event constructor( @@ -284,14 +282,14 @@ class EditCodeService extends Disposable implements IEditCodeService { super(); // this function initializes data structures and listens for changes - const registeredModelListeners = new Set() + const registeredModelURIs = new Set() const initializeModel = async (model: ITextModel) => { await this._voidModelService.initializeModel(model.uri) // do not add listeners to the same model twice - important, or will see duplicates - if (registeredModelListeners.has(model.uri.fsPath)) return - registeredModelListeners.add(model.uri.fsPath) + if (registeredModelURIs.has(model.uri.fsPath)) return + registeredModelURIs.add(model.uri.fsPath) if (!(model.uri.fsPath in this.diffAreasOfURI)) { this.diffAreasOfURI[model.uri.fsPath] = new Set(); @@ -307,30 +305,6 @@ class EditCodeService extends Disposable implements IEditCodeService { }) ) - // when a stream starts or ends, fire the event for onDidChangeURIStreamState - let prevStreamState = this.getURIStreamState({ uri: null }) - const updateAcceptRejectAllUI = () => { - const state = this.getURIStreamState({ uri: model.uri }) - let prevStateActual = prevStreamState - prevStreamState = state - if (state === prevStateActual) return - this._onDidChangeURIStreamState.fire({ uri: model.uri, state }) - } - - - let _removeAcceptRejectAllUI: (() => void) | null = null - this._register(this._onDidChangeURIStreamState.event(({ uri, state }) => { - if (uri.fsPath !== model.uri.fsPath) return - if (state === 'acceptRejectAll') { - if (!_removeAcceptRejectAllUI) - _removeAcceptRejectAllUI = this._addAcceptRejectAllUI(model.uri) ?? null - } else { - _removeAcceptRejectAllUI?.() - _removeAcceptRejectAllUI = null - } - })) - this._register(this._onDidChangeDiffZoneStreaming.event(({ uri: uri_ }) => { if (uri_.fsPath === model.uri.fsPath) updateAcceptRejectAllUI() })) - this._register(this._onDidAddOrDeleteDiffZones.event(({ uri: uri_ }) => { if (uri_.fsPath === model.uri.fsPath) updateAcceptRejectAllUI() })) // when the model first mounts, refresh any diffs that might be on it (happens if diffs were added in the BG) this._refreshStylesAndDiffsInURI(model.uri) } @@ -350,44 +324,6 @@ class EditCodeService extends Disposable implements IEditCodeService { this._register(this._codeEditorService.onCodeEditorAdd(editor => { initializeEditor(editor) })) - // update `_sortedUrisWithDiffs` and `_sortedDiffsOfUri` on all changes to diffzones - this._register(this._onDidFinishAddOrDeleteDiffInDiffZone.event(({ uri }) => { - - - // 1. Update _sortedUrisWithDiffs - const hasDiffZones = Array.from(this.diffAreasOfURI[uri.fsPath] || []) - .some(diffAreaId => this.diffAreaOfId[diffAreaId]?.type === 'DiffZone'); - - // Add or remove this URI from _sortedUrisWithDiffs - const currentIndex = this._sortedUrisWithDiffs.findIndex(u => u.fsPath === uri.fsPath); - if (hasDiffZones && currentIndex === -1) { - // Add URI and maintain sort - this._sortedUrisWithDiffs.push(uri); - this._sortedUrisWithDiffs.sort((a, b) => a.fsPath.localeCompare(b.fsPath)); - } else if (!hasDiffZones && currentIndex !== -1) { - // Remove URI - this._sortedUrisWithDiffs.splice(currentIndex, 1); - } - - // 2. Update _sortedDiffsOfUri only for this URI - const diffsInUri: Diff[] = []; - - // Collect all diffs from DiffZones in this URI - for (const diffAreaId of this.diffAreasOfURI[uri.fsPath] || []) { - const diffArea = this.diffAreaOfId[diffAreaId]; - if (diffArea?.type === 'DiffZone') { - diffsInUri.push(...Object.values(diffArea._diffOfId)); - } - } - - // Update or remove the entry for this URI - if (diffsInUri.length > 0) { - this._sortedDiffsOfFspath[uri.fsPath] = diffsInUri.sort((a, b) => a.startLine - b.startLine); - } else { - delete this._sortedDiffsOfFspath[uri.fsPath]; - } - })); - } @@ -494,40 +430,6 @@ class EditCodeService extends Disposable implements IEditCodeService { } } - private _addAcceptRejectAllUI(uri: URI) { - - // find all diffzones that aren't streaming - const diffZones: DiffZone[] = [] - for (let diffareaid of this.diffAreasOfURI[uri.fsPath] || []) { - const diffArea = this.diffAreaOfId[diffareaid] - if (diffArea.type !== 'DiffZone') continue - if (diffArea._streamState.isStreaming) continue - diffZones.push(diffArea) - } - if (diffZones.length === 0) return - - const consistentItemId = this._consistentItemService.addConsistentItemToURI({ - uri, - fn: (editor) => { - const buttonsWidget = new AcceptAllRejectAllWidget({ - editor, - onAcceptAll: () => { - this.acceptOrRejectAllDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false, _addToHistory: true }) - this._metricsService.capture('Accept All', {}) - }, - onRejectAll: () => { - this.acceptOrRejectAllDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false, _addToHistory: true }) - this._metricsService.capture('Reject All', {}) - }, - instantiationService: this._instantiationService, - }) - return () => { buttonsWidget.dispose() } - } - }) - - - return () => { this._consistentItemService.removeConsistentItemFromURI(consistentItemId) } - } mostRecentTextOfCtrlKZoneId: Record = {} @@ -564,9 +466,9 @@ class EditCodeService extends Disposable implements IEditCodeService { }) // mount react - let disposablesRef: IDisposable[] | undefined = undefined + let disposeFn: (() => void) | undefined = undefined this._instantiationService.invokeFunction(accessor => { - disposablesRef = mountCtrlK(domNode, accessor, { + disposeFn = mountCtrlK(domNode, accessor, { diffareaid: ctrlKZone.diffareaid, @@ -591,14 +493,13 @@ class EditCodeService extends Disposable implements IEditCodeService { this.mostRecentTextOfCtrlKZoneId[ctrlKZone.diffareaid] = text; }, initText: this.mostRecentTextOfCtrlKZoneId[ctrlKZone.diffareaid] ?? null, - } satisfies QuickEditPropsType) - + } satisfies QuickEditPropsType)?.dispose }) // cleanup return () => { editor.changeViewZones(accessor => { if (zoneId) accessor.removeZone(zoneId) }) - disposablesRef?.forEach(d => d.dispose()) + disposeFn?.() } }) @@ -624,7 +525,7 @@ class EditCodeService extends Disposable implements IEditCodeService { if (diffArea.type !== 'CtrlKZone') continue if (!diffArea._mountInfo) { diffArea._mountInfo = this._addCtrlKZoneInput(diffArea) - // console.log('MOUNTED', diffArea.diffareaid) + console.log('MOUNTED CTRLK', diffArea.diffareaid) } else { diffArea._mountInfo.refresh() @@ -748,15 +649,15 @@ class EditCodeService extends Disposable implements IEditCodeService { } else { throw new Error('Void 1') } - const buttonsWidget = new AcceptRejectWidget({ + const buttonsWidget = new AcceptRejectInlineWidget({ editor, onAccept: () => { this.acceptDiff({ diffid }) - this._metricsService.capture('Accept Diff', {}) + this._metricsService.capture('Accept Diff', { diffid }) }, onReject: () => { this.rejectDiff({ diffid }) - this._metricsService.capture('Reject Diff', {}) + this._metricsService.capture('Reject Diff', { diffid }) }, diffid: diffid.toString(), startLine, @@ -927,10 +828,6 @@ class EditCodeService extends Disposable implements IEditCodeService { if (diffArea.type !== 'DiffZone') return delete diffArea._diffOfId[diff.diffid] delete this.diffOfId[diff.diffid] - - if (!diffArea._streamState.isStreaming) { - this._onDidFinishAddOrDeleteDiffInDiffZone.fire({ uri: diffArea._URI }) - } } private _deleteDiffs(diffZone: DiffZone) { @@ -1023,10 +920,6 @@ class EditCodeService extends Disposable implements IEditCodeService { this.diffOfId[diffid] = newDiff diffZone._diffOfId[diffid] = newDiff - if (!diffZone._streamState.isStreaming) { - this._onDidFinishAddOrDeleteDiffInDiffZone.fire({ uri }) - } - return newDiff } @@ -1094,6 +987,19 @@ class EditCodeService extends Disposable implements IEditCodeService { } + + private _fireChangeDiffsIfNotStreaming(uri: URI) { + for (const diffareaid of this.diffAreasOfURI[uri.fsPath] || []) { + const diffArea = this.diffAreaOfId[diffareaid] + if (diffArea?.type !== 'DiffZone') continue + // fire changed diffs (this is the only place Diffs are added) + if (!diffArea._streamState.isStreaming) { + this._onDidChangeDiffsInDiffZone.fire({ uri, diffareaid: diffArea.diffareaid }) + } + } + } + + private _refreshStylesAndDiffsInURI(uri: URI) { // 1. clear DiffArea styles and Diffs @@ -1107,6 +1013,9 @@ class EditCodeService extends Disposable implements IEditCodeService { // 4. refresh ctrlK zones this._refreshCtrlKInputs(uri) + + // 5. this is the only place where diffs are changed, so can fire here only + this._fireChangeDiffsIfNotStreaming(uri) } @@ -1301,34 +1210,48 @@ class EditCodeService extends Disposable implements IEditCodeService { private _startStreamingDiffZone({ uri, - startRange, startBehavior, streamRequestIdRef, onUndo, + linkedCtrlKZone, }: { uri: URI, - startRange: 'fullFile' | [number, number], startBehavior: 'accept-conflicts' | 'reject-conflicts' | 'keep-conflicts', streamRequestIdRef: { current: string | null }, + linkedCtrlKZone: CtrlKZone | null, onUndo: () => void, }) { const { model } = this._voidModelService.getModel(uri) if (!model) return - const startLine = startRange === 'fullFile' ? 1 : startRange[0] - const endLine = startRange === 'fullFile' ? model.getLineCount() : startRange[1] - let originalCode = startRange === 'fullFile' ? model.getValue(EndOfLinePreference.LF) : model.getValue(EndOfLinePreference.LF).split('\n').slice((startLine - 1), (endLine - 1) + 1).join('\n') + // treat like full file, unless linkedCtrlKZone was provided in which case use its diff's range + + + + const startLine = linkedCtrlKZone ? linkedCtrlKZone.startLine : 1 + const endLine = linkedCtrlKZone ? linkedCtrlKZone.endLine : model.getLineCount() + const range = { startLineNumber: startLine, startColumn: 1, endLineNumber: endLine, endColumn: Number.MAX_SAFE_INTEGER } + + const originalFileStr = model.getValue(EndOfLinePreference.LF) + let originalCode = model.getValueInRange(range, EndOfLinePreference.LF) // add to history as a checkpoint, before we start modifying const { onFinishEdit } = this._addToHistory(uri, { onUndo }) // clear diffZones so no conflict if (startBehavior === 'keep-conflicts') { - // delete them then re-apply their change - this.acceptOrRejectAllDiffAreas({ uri, removeCtrlKs: true, behavior: 'reject', _addToHistory: false }) - const originalCodeReplacement = model.getValue(EndOfLinePreference.LF) // use this as original code - this._writeURIText(uri, originalCode, 'wholeFileRange', { shouldRealignDiffAreas: false }) // un-revert - originalCode = originalCodeReplacement + if (linkedCtrlKZone) { + // ctrlkzone should never have any conflicts + } + else { + // keep conflict on whole file - to keep conflict, revert the change and use those contents as original, then un-revert the change + const currentFileStr = originalFileStr + this.acceptOrRejectAllDiffAreas({ uri, removeCtrlKs: true, behavior: 'reject', _addToHistory: false }) + const oldFileStr = model.getValue(EndOfLinePreference.LF) // use this as original code + this._writeURIText(uri, currentFileStr, 'wholeFileRange', { shouldRealignDiffAreas: false }) // un-revert + originalCode = oldFileStr + } + } else if (startBehavior === 'accept-conflicts' || startBehavior === 'reject-conflicts') { const behavior = startBehavior === 'accept-conflicts' ? 'accept' : 'reject' @@ -1351,11 +1274,18 @@ class EditCodeService extends Disposable implements IEditCodeService { } const diffZone = this._addDiffArea(adding) - this._onDidChangeDiffZoneStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) + this._onDidChangeStreamingInDiffZone.fire({ uri, diffareaid: diffZone.diffareaid }) this._onDidAddOrDeleteDiffZones.fire({ uri }) + // a few items related to the ctrlKZone that started streaming this diffZone + if (linkedCtrlKZone) { + const ctrlKZone = linkedCtrlKZone + ctrlKZone._linkedStreamingDiffZone = diffZone.diffareaid + this._onDidChangeStreamingInCtrlKZone.fire({ uri, diffareaid: ctrlKZone.diffareaid }) + } - console.log('DONE _STREAMING', diffZone) + + console.log('DONE WITH _STARTSTREAMING', diffZone) return { diffZone, onFinishEdit } } @@ -1372,6 +1302,8 @@ class EditCodeService extends Disposable implements IEditCodeService { let uri: URI let startRange: 'fullFile' | [number, number] + let ctrlKZoneIfQuickEdit: CtrlKZone | null = null + if (from === 'ClickApply') { const uri_ = this._getActiveEditorURI() if (!uri_) return @@ -1381,7 +1313,8 @@ class EditCodeService extends Disposable implements IEditCodeService { else if (from === 'QuickEdit') { const { diffareaid } = opts const ctrlKZone = this.diffAreaOfId[diffareaid] - if (ctrlKZone.type !== 'CtrlKZone') return + if (ctrlKZone?.type !== 'CtrlKZone') return + ctrlKZoneIfQuickEdit = ctrlKZone const { startLine: startLine_, endLine: endLine_, _URI } = ctrlKZone uri = _URI startRange = [startLine_, endLine_] @@ -1390,10 +1323,13 @@ class EditCodeService extends Disposable implements IEditCodeService { throw new Error(`Void: diff.type not recognized on: ${from}`) } + console.log('q1', this.diffAreaOfId) await this._voidModelService.initializeModel(uri) + console.log('q2', this.diffAreaOfId) const { model } = this._voidModelService.getModel(uri) if (!model) return + console.log('q3', this.diffAreaOfId) // promise that resolves when the apply is done let resApplyPromise: () => void @@ -1401,16 +1337,6 @@ class EditCodeService extends Disposable implements IEditCodeService { const applyPromise = new Promise((res_, rej_) => { resApplyPromise = res_; rejApplyPromise = rej_ }) let streamRequestIdRef: { current: string | null } = { current: null } // can use this as a proxy to set the diffArea's stream state requestId - // start diffzone - const res = this._startStreamingDiffZone({ - uri, - streamRequestIdRef, - startRange, - startBehavior: opts.startBehavior, - onUndo: () => { if (diffZone._streamState.isStreaming) rejApplyPromise(new Error('Edit was interrupted by pressing undo.')) }, - }) - if (!res) return - const { diffZone, onFinishEdit } = res // build messages const quickEditFIMTags = defaultQuickEditFimTags // TODO can eventually let users customize modelFimTags @@ -1426,11 +1352,13 @@ class EditCodeService extends Disposable implements IEditCodeService { ] } else if (from === 'QuickEdit') { - const { diffareaid } = opts - const ctrlKZone = this.diffAreaOfId[diffareaid] - if (ctrlKZone.type !== 'CtrlKZone') return - const { _mountInfo } = ctrlKZone + console.log('aaa') + if (!ctrlKZoneIfQuickEdit) return + console.log('bbb', ctrlKZoneIfQuickEdit) + const { _mountInfo } = ctrlKZoneIfQuickEdit + console.log('ccc', _mountInfo) const instructions = _mountInfo?.textAreaRef.current?.value ?? '' + console.log('ddd', instructions) const startLine = startRange === 'fullFile' ? 1 : startRange[0] const endLine = startRange === 'fullFile' ? model.getLineCount() : startRange[1] @@ -1445,27 +1373,30 @@ class EditCodeService extends Disposable implements IEditCodeService { else { throw new Error(`featureName ${from} is invalid`) } - // a few items related to writeover streams and quickEdits - if (from === 'QuickEdit') { - const { diffareaid } = opts - const ctrlKZone = this.diffAreaOfId[diffareaid] - if (ctrlKZone.type !== 'CtrlKZone') return + // start diffzone + const res = this._startStreamingDiffZone({ + uri, + streamRequestIdRef, + startBehavior: opts.startBehavior, + onUndo: () => { if (diffZone._streamState.isStreaming) rejApplyPromise(new Error('Edit was interrupted by pressing undo.')) }, + linkedCtrlKZone: ctrlKZoneIfQuickEdit, + }) + if (!res) return + const { diffZone, onFinishEdit } = res + - ctrlKZone._linkedStreamingDiffZone = diffZone.diffareaid - this._onDidChangeCtrlKZoneStreaming.fire({ uri, diffareaid: ctrlKZone.diffareaid }) - } // helpers const onDone = () => { console.log('called onDone') diffZone._streamState = { isStreaming: false, } - this._onDidChangeDiffZoneStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) + this._onDidChangeStreamingInDiffZone.fire({ uri, diffareaid: diffZone.diffareaid }) - if (from === 'QuickEdit') { - const ctrlKZone = this.diffAreaOfId[opts.diffareaid] as CtrlKZone + if (ctrlKZoneIfQuickEdit) { + const ctrlKZone = ctrlKZoneIfQuickEdit ctrlKZone._linkedStreamingDiffZone = null - this._onDidChangeCtrlKZoneStreaming.fire({ uri, diffareaid: ctrlKZone.diffareaid }) + this._onDidChangeStreamingInCtrlKZone.fire({ uri, diffareaid: ctrlKZone.diffareaid }) this._deleteCtrlKZone(ctrlKZone) } this._refreshStylesAndDiffsInURI(uri) @@ -1593,16 +1524,6 @@ class EditCodeService extends Disposable implements IEditCodeService { const applyDonePromise = new Promise((res_, rej_) => { resApplyDonePromise = res_; rejApplyDonePromise = rej_ }) let streamRequestIdRef: { current: string | null } = { current: null } // can use this as a proxy to set the diffArea's stream state requestId - // start diffzone - const res = this._startStreamingDiffZone({ - uri, - streamRequestIdRef, - startRange: 'fullFile', - startBehavior: opts.startBehavior, - onUndo: () => { if (diffZone._streamState.isStreaming) rejApplyDonePromise(new Error('Edit was interrupted by user pressing undo.')) }, - }) - if (!res) return - const { diffZone, onFinishEdit } = res // build messages - ask LLM to generate search/replace block text const originalFileCode = model.getValue(EndOfLinePreference.LF) @@ -1612,6 +1533,18 @@ class EditCodeService extends Disposable implements IEditCodeService { { role: 'user', content: userMessageContent }, ] + // start diffzone + const res = this._startStreamingDiffZone({ + uri, + streamRequestIdRef, + startBehavior: opts.startBehavior, + linkedCtrlKZone: null, + onUndo: () => { if (diffZone._streamState.isStreaming) rejApplyDonePromise(new Error('Edit was interrupted by user pressing undo.')) }, + }) + if (!res) return + const { diffZone, onFinishEdit } = res + + // helpers type SearchReplaceDiffAreaMetadata = { originalBounds: [number, number], // 1-indexed @@ -1656,7 +1589,7 @@ class EditCodeService extends Disposable implements IEditCodeService { const onDone = () => { diffZone._streamState = { isStreaming: false, } - this._onDidChangeDiffZoneStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) + this._onDidChangeStreamingInDiffZone.fire({ uri, diffareaid: diffZone.diffareaid }) this._refreshStylesAndDiffsInURI(uri) // delete the tracking zones @@ -1929,7 +1862,7 @@ class EditCodeService extends Disposable implements IEditCodeService { this._llmMessageService.abort(streamRequestId) diffZone._streamState = { isStreaming: false, } - this._onDidChangeDiffZoneStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) + this._onDidChangeStreamingInDiffZone.fire({ uri, diffareaid: diffZone.diffareaid }) } _undoHistory(uri: URI) { @@ -1972,24 +1905,6 @@ class EditCodeService extends Disposable implements IEditCodeService { } - private _getDiffZonesOnURI(uri: URI) { - const diffZones = [...this.diffAreasOfURI[uri.fsPath]?.values() ?? []] - .map(diffareaid => this.diffAreaOfId[diffareaid]) - .filter(diffArea => !!diffArea && diffArea.type === 'DiffZone') - - return diffZones - } - - getURIStreamState = ({ uri }: { uri: URI | null }) => { - if (uri === null) return 'idle' - const diffZones = this._getDiffZonesOnURI(uri) - - const isStreaming = diffZones.find(diffZone => !!diffZone._streamState.isStreaming) - - const state: URIStreamState = isStreaming ? 'streaming' : (diffZones.length === 0 ? 'idle' : 'acceptRejectAll') - return state - } - interruptURIStreaming({ uri }: { uri: URI }) { // brute force for now is OK for (const diffareaid of this.diffAreasOfURI[uri.fsPath] || []) { @@ -2013,14 +1928,12 @@ class EditCodeService extends Disposable implements IEditCodeService { // onFinishEdit() // } - private _revertAndDeleteDiffZone(diffZone: DiffZone) { + private _revertDiffZone(diffZone: DiffZone) { const uri = diffZone._URI const writeText = diffZone.originalCode const toRange: IRange = { startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER } this._writeURIText(uri, writeText, toRange, { shouldRealignDiffAreas: true }) - - this._deleteDiffZone(diffZone) } @@ -2037,7 +1950,10 @@ class EditCodeService extends Disposable implements IEditCodeService { if (!diffArea) continue if (diffArea.type === 'DiffZone') { - if (behavior === 'reject') this._revertAndDeleteDiffZone(diffArea) + if (behavior === 'reject') { + this._revertDiffZone(diffArea) + this._deleteDiffZone(diffArea) + } else if (behavior === 'accept') this._deleteDiffZone(diffArea) } else if (diffArea.type === 'CtrlKZone' && removeCtrlKs) { @@ -2209,62 +2125,18 @@ class EditCodeService extends Disposable implements IEditCodeService { } - - - - // testDiffs(): DiffZone | undefined { - // const uri = this._getActiveEditorURI() - // if (!uri) return - - // const startLine = 1 - // const endLine = 4 - - // const currentFileStr = this._readURI(uri) - // if (currentFileStr === null) return - // const originalCode = currentFileStr.split('\n').slice((startLine - 1), (endLine - 1) + 1).join('\n') - - // const { onFinishEdit } = this._addToHistory(uri) - // const adding: Omit = { - // type: 'DiffZone', - // originalCode, - // startLine, - // endLine, - // _URI: uri, - // _streamState: { isStreaming: false, }, - // _diffOfId: {}, // added later - // _removeStylesFns: new Set(), - // } - // const diffZone = this._addDiffArea(adding) - // const endResult = `\ - // const x = 1; - // if (x > 0) { - // console.log('hi!') - // }` - // this._writeText(uri, endResult, - // { startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed - // { shouldRealignDiffAreas: true } - // ) - // diffZone._streamState = { isStreaming: false, } - // this._refreshStylesAndDiffsInURI(uri) - // onFinishEdit() - - // return diffZone - // } - } registerSingleton(IEditCodeService, EditCodeService, InstantiationType.Eager); -const acceptBg = '#1a7431' -const acceptAllBg = '#1e8538' -const acceptBorder = '1px solid #145626' -const rejectBg = '#b42331' -const rejectAllBg = '#cf2838' -const rejectBorder = '1px solid #8e1c27' -const buttonFontSize = '11px' -const buttonTextColor = 'white' -class AcceptRejectWidget extends Widget implements IOverlayWidget { + + + + + + +class AcceptRejectInlineWidget extends Widget implements IOverlayWidget { public getId() { return this.ID } public getDomNode() { return this._domNode; } @@ -2380,97 +2252,3 @@ class AcceptRejectWidget extends Widget implements IOverlayWidget { - -class AcceptAllRejectAllWidget extends Widget implements IOverlayWidget { - private readonly _domNode: HTMLElement; - private readonly editor: ICodeEditor; - private readonly ID: string; - private readonly _instantiationService: IInstantiationService; - - constructor({ editor, onAcceptAll, onRejectAll, instantiationService }: { - editor: ICodeEditor, - onAcceptAll: () => void, - onRejectAll: () => void, - instantiationService: IInstantiationService - }) { - super(); - - this.ID = editor.getModel()?.uri.fsPath + ''; - this.editor = editor; - this._instantiationService = instantiationService; - - // Create container div with buttons - const { voidCommandBar, acceptButton, rejectButton, buttons } = dom.h('div@buttons', [ - dom.h('div@voidCommandBar', []), - dom.h('button@acceptButton', []), - dom.h('button@rejectButton', []) - ]); - - // Style the container - buttons.style.zIndex = '2'; - buttons.style.padding = '4px'; - buttons.style.display = 'flex'; - buttons.style.gap = '4px'; - buttons.style.alignItems = 'center'; - - // Mount command bar using mountVoidCommandBar - this._instantiationService.invokeFunction(accessor => { - console.log(voidCommandBar) - if (voidCommandBar) { // remove this - Math.random() - } - // mountVoidCommandBar(voidCommandBar, accessor, {}) - }); - - // Style accept button - acceptButton.addEventListener('click', onAcceptAll) - acceptButton.textContent = 'Accept All'; - acceptButton.style.backgroundColor = acceptAllBg; - acceptButton.style.border = acceptBorder; - acceptButton.style.color = buttonTextColor; - acceptButton.style.fontSize = buttonFontSize; - acceptButton.style.padding = '4px 8px'; - acceptButton.style.borderRadius = '6px'; - acceptButton.style.cursor = 'pointer'; - - // Style reject button - rejectButton.addEventListener('click', onRejectAll) - rejectButton.textContent = 'Reject All'; - rejectButton.style.backgroundColor = rejectAllBg; - rejectButton.style.border = rejectBorder; - rejectButton.style.color = buttonTextColor; - rejectButton.style.fontSize = buttonFontSize; - rejectButton.style.color = 'white'; - rejectButton.style.padding = '4px 8px'; - rejectButton.style.borderRadius = '6px'; - rejectButton.style.cursor = 'pointer'; - - this._domNode = buttons; - - // Mount the widget - editor.addOverlayWidget(this); - } - - - public getId(): string { - return this.ID; - } - - public getDomNode(): HTMLElement { - return this._domNode; - } - - public getPosition() { - return { - preference: OverlayWidgetPositionPreference.BOTTOM_RIGHT_CORNER, - } - } - - public override dispose(): void { - this.editor.removeOverlayWidget(this); - super.dispose(); - } -} - - - diff --git a/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts b/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts index 1acae2c7..2e0239e4 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts @@ -32,7 +32,7 @@ export type AddCtrlKOpts = { editor: ICodeEditor, } -export type URIStreamState = 'idle' | 'acceptRejectAll' | 'streaming' +export type URIAcceptRejectState = 'idle' | 'acceptRejectAll' | 'streaming' export const IEditCodeService = createDecorator('editCodeService'); @@ -40,32 +40,28 @@ export const IEditCodeService = createDecorator('editCodeServi export interface IEditCodeService { readonly _serviceBrand: undefined; - // main entrypoints (initialize things for the functions below to be called): startApplying(opts: StartApplyingOpts): Promise<[URI, Promise] | null>; - _sortedUrisWithDiffs: URI[]; - _sortedDiffsOfFspath: { [fsPath: string]: Diff[] | undefined }; + addCtrlKZone(opts: AddCtrlKOpts): number | undefined; + removeCtrlKZone(opts: { diffareaid: number }): void; diffAreaOfId: Record; + diffAreasOfURI: Record | undefined>; diffOfId: Record; - - addCtrlKZone(opts: AddCtrlKOpts): number | undefined; - - removeCtrlKZone(opts: { diffareaid: number }): void; acceptOrRejectAllDiffAreas(opts: { uri: URI, removeCtrlKs: boolean, behavior: 'reject' | 'accept', _addToHistory?: boolean }): void; + // events onDidAddOrDeleteDiffZones: Event<{ uri: URI }>; - onDidAddOrDeleteDiffInDiffZone: Event<{ uri: URI }>; + onDidChangeDiffsInDiffZone: Event<{ uri: URI; diffareaid: number }>; // only fires when not streaming!!! streaming would be too much + onDidChangeStreamingInDiffZone: Event<{ uri: URI; diffareaid: number }>; + onDidChangeStreamingInCtrlKZone: Event<{ uri: URI; diffareaid: number }>; // CtrlKZone streaming state isCtrlKZoneStreaming(opts: { diffareaid: number }): boolean; interruptCtrlKStreaming(opts: { diffareaid: number }): void; - onDidChangeCtrlKZoneStreaming: Event<{ uri: URI; diffareaid: number }>; // // DiffZone codeBoxId streaming state - getURIStreamState(opts: { uri: URI | null }): URIStreamState; interruptURIStreaming(opts: { uri: URI }): void; - onDidChangeURIStreamState: Event<{ uri: URI; state: URIStreamState }>; // testDiffs(): void; } diff --git a/src/vs/workbench/contrib/void/browser/helperServices/consistentItemService.ts b/src/vs/workbench/contrib/void/browser/helperServices/consistentItemService.ts index 3165e57f..ba906ff5 100644 --- a/src/vs/workbench/contrib/void/browser/helperServices/consistentItemService.ts +++ b/src/vs/workbench/contrib/void/browser/helperServices/consistentItemService.ts @@ -25,7 +25,7 @@ export interface IConsistentItemService { export const IConsistentItemService = createDecorator('ConsistentItemService'); -export class ConsistentItemService extends Disposable { +export class ConsistentItemService extends Disposable implements IConsistentItemService { readonly _serviceBrand: undefined diff --git a/src/vs/workbench/contrib/void/browser/metricsPollService.ts b/src/vs/workbench/contrib/void/browser/metricsPollService.ts index 92bbd16c..037aa1d6 100644 --- a/src/vs/workbench/contrib/void/browser/metricsPollService.ts +++ b/src/vs/workbench/contrib/void/browser/metricsPollService.ts @@ -33,12 +33,11 @@ class MetricsPollService extends Disposable implements IMetricsPollService { // initial state const { window } = dom.getActiveWindow() - let i = 1 + let i = 0 this.intervalID = window.setInterval(() => { - this.metricsService.capture('Alive', { i }) + this.metricsService.capture('Alive', { iv1: i }) i += 1 - console.log('ping', i) }, PING_EVERY_MS) 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 62a20997..a4fe34bd 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 @@ -1,5 +1,5 @@ import { useState, useEffect, useCallback } from 'react' -import { useAccessor, useURIStreamState, useSettingsState } from '../util/services.js' +import { useAccessor, useCommandBarState, useCommandBarURIListener, useSettingsState } from '../util/services.js' import { usePromise, useRefState } from '../util/helpers.js' import { isFeatureNameDisabled } from '../../../../common/voidSettingsTypes.js' import { URI } from '../../../../../../../base/common/uri.js' @@ -128,28 +128,37 @@ export const useApplyButtonHTML = ({ codeStr, applyBoxId, uri }: { codeStr: stri const accessor = useAccessor() const editCodeService = accessor.get('IEditCodeService') + const voidCommandBarService = accessor.get('IVoidCommandBarService') const metricsService = accessor.get('IMetricsService') const [_, rerender] = useState(0) - const getUriBeingApplied = useCallback(() => applyingURIOfApplyBoxIdRef.current[applyBoxId] ?? null, [applyBoxId]) - const getStreamState = useCallback(() => editCodeService.getURIStreamState({ uri: getUriBeingApplied() }), [editCodeService, getUriBeingApplied]) + const getUriBeingApplied = useCallback(() => { + return applyingURIOfApplyBoxIdRef.current[applyBoxId] ?? null + }, [applyBoxId]) + + const getStreamState = useCallback(() => { + const uri = getUriBeingApplied() + if (!uri) return 'idle-no-changes' + return voidCommandBarService.getStreamState(uri) + }, [voidCommandBarService, getUriBeingApplied]) // listen for stream updates on this box - useURIStreamState( - useCallback((uri_, newStreamState) => { - const shouldUpdate = ( - getUriBeingApplied()?.fsPath === uri_.fsPath - || (uri === 'current' ? false : uri.fsPath === uri_.fsPath) - ) - if (!shouldUpdate) return - rerender(c => c + 1) - }, [applyBoxId, editCodeService, getUriBeingApplied, uri]) + + + useCommandBarURIListener(useCallback((uri_) => { + const shouldUpdate = ( + getUriBeingApplied()?.fsPath === uri_.fsPath + || (uri !== 'current' && uri.fsPath === uri_.fsPath) + ) + if (!shouldUpdate) return + rerender(c => c + 1) + }, [applyBoxId, editCodeService, getUriBeingApplied, uri]) ) const onClickSubmit = useCallback(async () => { if (isDisabled) return - if (getStreamState() === 'streaming') return + if (getStreamState()) return const [newApplyingUri, _] = await editCodeService.startApplying({ from: 'ClickApply', applyStr: codeStr, @@ -167,7 +176,7 @@ export const useApplyButtonHTML = ({ codeStr, applyBoxId, uri }: { codeStr: stri const onInterrupt = useCallback(() => { - if (getStreamState() !== 'streaming') return + if (!getStreamState()) return const uri = getUriBeingApplied() if (!uri) return @@ -250,7 +259,7 @@ export const useApplyButtonHTML = ({ codeStr, applyBoxId, uri }: { codeStr: stri } - if (currStreamState === 'idle') { + if (currStreamState === 'idle-no-changes') { buttonsHTML = <> {copyButton} @@ -258,7 +267,7 @@ export const useApplyButtonHTML = ({ codeStr, applyBoxId, uri }: { codeStr: stri } - if (currStreamState === 'acceptRejectAll') { + if (currStreamState === 'idle-has-changes') { buttonsHTML = <> {reapplyButton} @@ -270,9 +279,9 @@ export const useApplyButtonHTML = ({ codeStr, applyBoxId, uri }: { codeStr: stri const statusIndicatorHTML =
    5 && isAbsolute(s) && !s.startsWith('//') // common case that is a false positive is comments like // + return s.length > 5 && isAbsolute(s) && !s.includes('//') && !s.includes('/*') // common case that is a false positive is comments like // } const Codespan = ({ text, className, onClick }: { text: string, className?: string, onClick?: () => void }) => { diff --git a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx index 0443e5c5..205ef3a9 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx @@ -66,6 +66,7 @@ export const QuickEditChat = ({ editCodeService.startApplying({ from: 'QuickEdit', diffareaid, + startBehavior: 'keep-conflicts', }) }, [isStreamingRef, isDisabled, editCodeService, diffareaid]) 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 671e4571..c913f6f6 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 @@ -10,7 +10,7 @@ import React, { ButtonHTMLAttributes, FormEvent, FormHTMLAttributes, Fragment, KeyboardEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { useAccessor, useSidebarState, useChatThreadsState, useChatThreadsStreamState, useUriState, useSettingsState } from '../util/services.js'; +import { useAccessor, useSidebarState, useChatThreadsState, useChatThreadsStreamState, useSettingsState, useActiveURI } from '../util/services.js'; import { ChatMarkdownRender, ChatMessageLocation, getApplyBoxId } from '../markdown/ChatMarkdownRender.js'; import { URI } from '../../../../../../../base/common/uri.js'; @@ -208,7 +208,7 @@ const ReasoningOptionSlider = ({ featureName }: { featureName: FeatureName }) => const nameOfChatMode = { - 'normal': 'Normal', + 'normal': 'Chat', 'gather': 'Gather', 'agent': 'Agent', } @@ -488,18 +488,18 @@ export const SelectedFiles = ( const modelReferenceService = accessor.get('IVoidModelService') // state for tracking prospective files - const { currentUri } = useUriState() + const { uri: currentURI } = useActiveURI() const [recentUris, setRecentUris] = useState([]) const maxRecentUris = 10 const maxProspectiveFiles = 3 useEffect(() => { // handle recent files - if (!currentUri) return + if (!currentURI) return setRecentUris(prev => { - const withoutCurrent = prev.filter(uri => uri.fsPath !== currentUri.fsPath) // remove duplicates - const withCurrent = [currentUri, ...withoutCurrent] + const withoutCurrent = prev.filter(uri => uri.fsPath !== currentURI.fsPath) // remove duplicates + const withCurrent = [currentURI, ...withoutCurrent] return withCurrent.slice(0, maxRecentUris) }) - }, [currentUri]) + }, [currentURI]) const [prospectiveSelections, setProspectiveSelections] = useState([]) diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/mountFnGenerator.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/mountFnGenerator.tsx index 5eba467d..6114c8ff 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/mountFnGenerator.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/mountFnGenerator.tsx @@ -19,7 +19,20 @@ export const mountFnGenerator = (Component: (params: any) => React.ReactNode) => const disposables = _registerServices(accessor) const root = ReactDOM.createRoot(rootElement) - root.render(); // tailwind dark theme indicator - return disposables + const rerender = (props?: any) => { + root.render(); // tailwind dark theme indicator + } + const dispose = () => { + root.unmount(); + disposables.forEach(d => d.dispose()); + } + + rerender(props) + + const returnVal = { + rerender, + dispose, + } + return returnVal } 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 08a52acf..95060cb2 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 @@ -9,7 +9,6 @@ import { IDisposable } from '../../../../../../../base/common/lifecycle.js' import { VoidSidebarState } from '../../../sidebarStateService.js' import { VoidSettingsState } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js' import { ColorScheme } from '../../../../../../../platform/theme/common/theme.js' -import { VoidUriState } from '../../../voidUriStateService.js'; import { RefreshModelStateOfProvider } from '../../../../../../../workbench/contrib/void/common/refreshModelService.js' import { ServicesAccessor } from '../../../../../../../editor/browser/editorExtensions.js'; @@ -23,9 +22,8 @@ import { IThemeService } from '../../../../../../../platform/theme/common/themeS import { ILLMMessageService } from '../../../../common/sendLLMMessageService.js'; import { IRefreshModelService } from '../../../../../../../workbench/contrib/void/common/refreshModelService.js'; import { IVoidSettingsService } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js'; -import { IEditCodeService, URIStreamState } from '../../../editCodeServiceInterface.js' +import { IEditCodeService } from '../../../editCodeServiceInterface.js' -import { IVoidUriStateService } from '../../../voidUriStateService.js'; import { ISidebarStateService } from '../../../sidebarStateService.js'; import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js' import { ICodeEditorService } from '../../../../../../../editor/browser/services/codeEditorService.js' @@ -47,15 +45,13 @@ import { ITerminalToolService } from '../../../terminalToolService.js' import { ILanguageService } from '../../../../../../../editor/common/languages/language.js' import { IVoidModelService } from '../../../../common/voidModelService.js' import { IWorkspaceContextService } from '../../../../../../../platform/workspace/common/workspace.js' - +import { IVoidCommandBarService } from '../../../voidCommandBarService.js' // normally to do this you'd use a useEffect that calls .onDidChangeState(), but useEffect mounts too late and misses initial state changes // even if React hasn't mounted yet, the variables are always updated to the latest state. // React listens by adding a setState function to these listeners. -let uriState: VoidUriState -const uriStateListeners: Set<(s: VoidUriState) => void> = new Set() let sidebarState: VoidSidebarState const sidebarStateListeners: Set<(s: VoidSidebarState) => void> = new Set() @@ -77,8 +73,8 @@ let colorThemeState: ColorScheme const colorThemeStateListeners: Set<(s: ColorScheme) => void> = new Set() const ctrlKZoneStreamingStateListeners: Set<(diffareaid: number, s: boolean) => void> = new Set() -const uriStreamingStateListeners: Set<(uri: URI, s: URIStreamState) => void> = new Set() - +const commandBarURIStateListeners: Set<(uri: URI) => void> = new Set(); +const activeURIListeners: Set<(uri: URI | null) => void> = new Set(); // must call this before you can use any of the hooks below @@ -90,24 +86,17 @@ export const _registerServices = (accessor: ServicesAccessor) => { _registerAccessor(accessor) const stateServices = { - uriStateService: accessor.get(IVoidUriStateService), sidebarStateService: accessor.get(ISidebarStateService), chatThreadsStateService: accessor.get(IChatThreadService), settingsStateService: accessor.get(IVoidSettingsService), refreshModelService: accessor.get(IRefreshModelService), themeService: accessor.get(IThemeService), editCodeService: accessor.get(IEditCodeService), + voidCommandBarService: accessor.get(IVoidCommandBarService), + modelService: accessor.get(IModelService), } - const { uriStateService, sidebarStateService, settingsStateService, chatThreadsStateService, refreshModelService, themeService, editCodeService } = stateServices - - uriState = uriStateService.state - disposables.push( - uriStateService.onDidChangeState(() => { - uriState = uriStateService.state - uriStateListeners.forEach(l => l(uriState)) - }) - ) + const { sidebarStateService, settingsStateService, chatThreadsStateService, refreshModelService, themeService, editCodeService, voidCommandBarService, modelService } = stateServices sidebarState = sidebarStateService.state disposables.push( @@ -161,15 +150,21 @@ export const _registerServices = (accessor: ServicesAccessor) => { // no state disposables.push( - editCodeService.onDidChangeCtrlKZoneStreaming(({ diffareaid }) => { + editCodeService.onDidChangeStreamingInCtrlKZone(({ diffareaid }) => { const isStreaming = editCodeService.isCtrlKZoneStreaming({ diffareaid }) ctrlKZoneStreamingStateListeners.forEach(l => l(diffareaid, isStreaming)) }) ) + disposables.push( - editCodeService.onDidChangeURIStreamState(({ uri }) => { - const isStreaming = editCodeService.getURIStreamState({ uri }) - uriStreamingStateListeners.forEach(l => l(uri, isStreaming)) + voidCommandBarService.onDidChangeState(({ uri }) => { + commandBarURIStateListeners.forEach(l => l(uri)); + }) + ) + + disposables.push( + voidCommandBarService.onDidChangeActiveURI(({ uri }) => { + activeURIListeners.forEach(l => l(uri)); }) ) @@ -193,7 +188,6 @@ const getReactAccessor = (accessor: ServicesAccessor) => { IRefreshModelService: accessor.get(IRefreshModelService), IVoidSettingsService: accessor.get(IVoidSettingsService), IEditCodeService: accessor.get(IEditCodeService), - IVoidUriStateService: accessor.get(IVoidUriStateService), ISidebarStateService: accessor.get(ISidebarStateService), IChatThreadService: accessor.get(IChatThreadService), @@ -218,6 +212,8 @@ const getReactAccessor = (accessor: ServicesAccessor) => { IVoidModelService: accessor.get(IVoidModelService), IWorkspaceContextService: accessor.get(IWorkspaceContextService), + IVoidCommandBarService: accessor.get(IVoidCommandBarService), + } as const return reactAccessor } @@ -244,16 +240,6 @@ export const useAccessor = () => { // -- state of services -- -export const useUriState = () => { - const [s, ss] = useState(uriState) - useEffect(() => { - ss(uriState) - uriStateListeners.add(ss) - return () => { uriStateListeners.delete(ss) } - }, [ss]) - return s -} - export const useSidebarState = () => { const [s, ss] = useState(sidebarState) useEffect(() => { @@ -340,14 +326,6 @@ export const useCtrlKZoneStreamingState = (listener: (diffareaid: number, s: boo }, [listener, ctrlKZoneStreamingStateListeners]) } -export const useURIStreamState = (listener: (uri: URI, s: URIStreamState) => void) => { - useEffect(() => { - uriStreamingStateListeners.add(listener) - return () => { uriStreamingStateListeners.delete(listener) } - }, [listener, uriStreamingStateListeners]) -} - - export const useIsDark = () => { const [s, ss] = useState(colorThemeState) useEffect(() => { @@ -359,6 +337,40 @@ export const useIsDark = () => { // s is the theme, return isDark instead of s const isDark = s === ColorScheme.DARK || s === ColorScheme.HIGH_CONTRAST_DARK return isDark - } +export const useCommandBarURIListener = (listener: (uri: URI) => void) => { + useEffect(() => { + commandBarURIStateListeners.add(listener); + return () => { commandBarURIStateListeners.delete(listener) }; + }, [listener]); +}; +export const useCommandBarState = () => { + const accessor = useAccessor() + const commandBarService = accessor.get('IVoidCommandBarService') + const [s, ss] = useState({ state: commandBarService.stateOfURI, sortedURIs: commandBarService.sortedURIs }); + const listener = useCallback(() => { + ss({ state: commandBarService.stateOfURI, sortedURIs: commandBarService.sortedURIs }); + }, [commandBarService]) + useCommandBarURIListener(listener) + + return s; +} + + + +// roughly gets the active URI - this is used to get the history of recent URIs +export const useActiveURI = () => { + const accessor = useAccessor() + const commandBarService = accessor.get('IVoidCommandBarService') + const [s, ss] = useState(commandBarService.activeURI) + useEffect(() => { + const listener = () => { ss(commandBarService.activeURI) } + activeURIListeners.add(listener); + return () => { activeURIListeners.delete(listener) }; + }, []) + return { uri: s } +} + + + diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx index 6783be26..e97aa26a 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx @@ -4,186 +4,241 @@ *--------------------------------------------------------------------------------------*/ -import { useAccessor, useIsDark, useUriState } from '../util/services.js'; +import { useAccessor, useCommandBarState, useIsDark } from '../util/services.js'; import '../styles.css' -import { DiffZone } from '../../../editCodeService.js'; import { useCallback, useEffect, useState } from 'react'; +import { URI } from '../../../../../../../base/common/uri.js'; +import { ICodeEditor } from '../../../../../../../editor/browser/editorBrowser.js'; import { ScrollType } from '../../../../../../../editor/common/editorCommon.js'; -import { getBasename } from '../sidebar-tsx/SidebarChat.js'; +import { acceptAllBg, acceptBorder, buttonFontSize, buttonTextColor, rejectAllBg, rejectBorder } from '../../../../common/helpers/colors.js'; -export const VoidCommandBarMain = ({ className }: { className: string }) => { +export type VoidCommandBarProps = { + uri: URI | null; + editor: ICodeEditor; +} + +export const VoidCommandBarMain = ({ uri, editor }: VoidCommandBarProps) => { const isDark = useIsDark() + console.log('VoidCommandBarMain', uri?.fsPath) return
    - +
    } -const VoidCommandBar = () => { + +const stepIdx = (currIdx: number | null, len: number, step: -1 | 1) => { + if (len === 0) return null + if (len === 1 && step === -1) return null // don't step backwards if just 1 element + return ((currIdx ?? 0) + step) % len +} + + + +const VoidCommandBar = ({ uri, editor }: { uri: URI | null, editor: ICodeEditor }) => { const accessor = useAccessor() const editCodeService = accessor.get('IEditCodeService') const editorService = accessor.get('ICodeEditorService') + const metricsService = accessor.get('IMetricsService') const commandService = accessor.get('ICommandService') + const commandBarService = accessor.get('IVoidCommandBarService') + const voidModelService = accessor.get('IVoidModelService') + const { state: commandBarState, sortedURIs: sortedCommandBarURIs } = useCommandBarState() - const [_, rerender] = useState(0) - // Add a state variable to track focus - const [isFocused, setIsFocused] = useState(false) - console.log('rerender count: ', _) - // state for what the user is currently focused on (both URI and diff) - const [diffIdxOfFspath, setDiffIdxOfFspath] = useState>({}) - // const [currentUriIdx, setCurrentUriIdx] = useState(-1) // we are doing O(n) search for this - - const { currentUri } = useUriState() - - // trigger rerender when diffzone is created (TODO need to also update when diff is accepted/rejected) + // changes if the user clicks left/right or if the user goes on a uri with changes + const [currUriIdx, setUriIdx] = useState(null) + const [currUriHasChanges, setCurrUriHasChanges] = useState(false) useEffect(() => { - const disposable = editCodeService.onDidAddOrDeleteDiffInDiffZone(() => { - rerender(c => c + 1) // rerender - }) - return () => disposable.dispose() - }, [editCodeService, rerender]) - - const getNextDiff = useCallback(({ step }: { step: 1 | -1 }) => { - if (!currentUri) { - return; + const i = sortedCommandBarURIs.findIndex(e => e.fsPath === uri?.fsPath) + if (i !== -1) { + setUriIdx(i) + setCurrUriHasChanges(true) } - - const sortedDiffs = editCodeService._sortedDiffsOfFspath[currentUri.fsPath] - - if (!sortedDiffs || sortedDiffs.length === 0) { - return; + else { + setCurrUriHasChanges(false) } + }, [sortedCommandBarURIs, uri]) - const currentDiffIdx = diffIdxOfFspath[currentUri.fsPath] || 0 - const nextDiffIdx = (currentDiffIdx + step) % sortedDiffs.length + // just for style + const [isFocused, setIsFocused] = useState(false) - const nextDiff = sortedDiffs[nextDiffIdx] - - return { nextDiff, nextDiffIdx } - - }, [currentUri, editCodeService._sortedDiffsOfFspath, diffIdxOfFspath]) - - const getNextUri = useCallback(({ step }: { step: 1 | -1 }) => { - - const sortedUris = editCodeService._sortedUrisWithDiffs - if (sortedUris.length === 0) { - return; + const getNextDiffIdx = (step: 1 | -1) => { + // check undefined + if (!uri) return null + const s = commandBarState[uri.fsPath] + if (!s) return null + const { diffIdx, sortedDiffIds } = s + // get next idx + const nextDiffIdx = stepIdx(diffIdx, sortedDiffIds.length, step) + return nextDiffIdx + } + const goToDiffIdx = (idx: number | null) => { + // check undefined + if (!uri) return + const s = commandBarState[uri.fsPath] + if (!s) return + const { sortedDiffIds } = s + // reveal + if (idx) { + const diffid = sortedDiffIds[idx] + const diff = editCodeService.diffOfId[diffid] + const range = { startLineNumber: diff.startLine, endLineNumber: diff.startLine, startColumn: 1, endColumn: 1 }; + editor.revealRange(range, ScrollType.Immediate) } - - const defaultUriIdx = step === 1 ? -1 : 0 // defaults: if next, currentIdx = -1; if prev, currentIdx = 0 - let currentUriIdx = -1 - if (currentUri) { - currentUriIdx = sortedUris.findIndex(u => u.fsPath === currentUri.fsPath) - } - - if (currentUriIdx === -1) { // not found - currentUriIdx = defaultUriIdx // set to default - } - - const nextUriIdx = (currentUriIdx + step) % sortedUris.length - const nextUri = sortedUris[nextUriIdx] - - return { nextUri, nextUriIdx } - - }, [currentUri, editCodeService._sortedUrisWithDiffs]) - - const gotoNextDiff = ({ step }: { step: 1 | -1 }) => { - - // get the next diff - const res = getNextDiff({ step }) - if (!res) return; - - // scroll to the next diff - const { nextDiff, nextDiffIdx } = res; - const editor = editorService.getActiveCodeEditor() - if (!editor) return; - - const range = { startLineNumber: nextDiff.startLine, endLineNumber: nextDiff.startLine, startColumn: 1, endColumn: 1 }; - editor.revealRange(range, ScrollType.Immediate) - - // update state - const diffArea = editCodeService.diffAreaOfId[nextDiff.diffareaid] - setDiffIdxOfFspath(v => ({ ...v, [diffArea._URI.fsPath]: nextDiffIdx })) - } - const gotoNextUri = ({ step }: { step: 1 | -1 }) => { - // get the next uri - const res = getNextUri({ step }) - if (!res) return; - - const { nextUri, nextUriIdx } = res; - - // open the uri and scroll to diff - const sortedDiffs = editCodeService._sortedDiffsOfFspath[nextUri.fsPath] - if (!sortedDiffs) return; - - const diffIdx = diffIdxOfFspath[nextUri.fsPath] || 0 - const diff = sortedDiffs[diffIdx] - - const range = { startLineNumber: diff.startLine, endLineNumber: diff.startLine, startColumn: 1, endColumn: 1 }; - - commandService.executeCommand('vscode.open', nextUri).then(() => { - - // select the text - setTimeout(() => { - - const editor = editorService.getActiveCodeEditor() - if (!editor) return; - - editor.revealRange(range, ScrollType.Immediate) - - }, 50) - - }) + const getNextUriIdx = (step: 1 | -1) => { + return stepIdx(currUriIdx, sortedCommandBarURIs.length, step) + } + const goToURIIdx = async (idx: number | null) => { + if (idx === null) return + const nextURI = sortedCommandBarURIs[idx] + editCodeService.diffAreasOfURI + const { model } = await voidModelService.getModelSafe(nextURI) + if (model) { editor.setModel(model) } // switch to the URI } - return
    setIsFocused(true)} - onBlurCapture={() => setIsFocused(false)} + + + // when change URI, scroll to the proper spot + useEffect(() => { + setTimeout(() => { + // check undefined + if (!uri) return + const s = commandBarState[uri.fsPath] + if (!s) return + const { diffIdx } = s + goToDiffIdx(diffIdx) + }, 50) + + }, [uri]) + + + const currDiffIdx = uri ? commandBarState[uri.fsPath]?.diffIdx ?? null : null + const sortedDiffIds = uri ? commandBarState[uri.fsPath]?.sortedDiffIds ?? null : null + + const nextDiffIdx = getNextDiffIdx(1) + const prevDiffIdx = getNextDiffIdx(-1) + const nextURIIdx = getNextUriIdx(1) + const prevURIIdx = getNextUriIdx(-1) + + + + if (sortedCommandBarURIs.length === 0) return null // if there are absolutely no changes + + const navPanel =
    setIsFocused(true)} + onBlur={() => setIsFocused(false)} >
    -
    File {(editCodeService._sortedUrisWithDiffs.findIndex(u => u.fsPath === currentUri?.fsPath) ?? 0) + 1} of {editCodeService._sortedUrisWithDiffs.length}
    -
    Diff {(diffIdxOfFspath[currentUri?.fsPath ?? ''] ?? 0) + 1} of {editCodeService._sortedDiffsOfFspath[currentUri?.fsPath ?? '']?.length ?? 0}
    +
    + File {(currUriIdx ?? 0) + 1} of {sortedCommandBarURIs.length} +
    +
    + Diff {(currDiffIdx ?? 0) + 1} of {sortedDiffIds?.length ?? 0} +
    + + + const onAcceptAll = () => { + if (!uri) return + editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false, _addToHistory: true }) + metricsService.capture('Accept All', {}) + } + const onRejectAll = () => { + if (!uri) return + editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false, _addToHistory: true }) + metricsService.capture('Reject All', {}) + } + + const acceptRejectButtons = currUriHasChanges &&
    + + +
    + + + return <> + {navPanel} + {acceptRejectButtons} + } diff --git a/src/vs/workbench/contrib/void/browser/sidebarActions.ts b/src/vs/workbench/contrib/void/browser/sidebarActions.ts index 16a3be18..a6da7283 100644 --- a/src/vs/workbench/contrib/void/browser/sidebarActions.ts +++ b/src/vs/workbench/contrib/void/browser/sidebarActions.ts @@ -15,19 +15,15 @@ import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextke import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; import { IRange } from '../../../../editor/common/core/range.js'; import { ITextModel } from '../../../../editor/common/model.js'; -import { VOID_VIEW_CONTAINER_ID, VOID_VIEW_ID } from './sidebarPane.js'; +import { VOID_VIEW_ID } from './sidebarPane.js'; import { IMetricsService } from '../common/metricsService.js'; import { ISidebarStateService } from './sidebarStateService.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { VOID_TOGGLE_SETTINGS_ACTION_ID } from './voidSettingsPane.js'; import { VOID_CTRL_L_ACTION_ID } from './actionIDs.js'; -import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { localize2 } from '../../../../nls.js'; -import { IViewsService } from '../../../services/views/common/viewsService.js'; -import { IVoidUriStateService } from './voidUriStateService.js'; import { StagingSelectionItem } from '../common/chatThreadServiceTypes.js'; import { IChatThreadService } from './chatThreadService.js'; @@ -286,43 +282,3 @@ export class TabSwitchListener extends Disposable { this._register(this._editorService.onCodeEditorAdd(editor => { initializeEditor(editor) })) } } - - -class TabSwitchContribution extends Disposable implements IWorkbenchContribution { - static readonly ID = 'workbench.contrib.void.tabswitch' - - constructor( - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IViewsService private readonly viewsService: IViewsService, - @IVoidUriStateService private readonly uriStateService: IVoidUriStateService, - @ICodeEditorService private readonly codeEditorService: ICodeEditorService, - // @ICommandService private readonly commandService: ICommandService, - ) { - super() - - // sidebarIsVisible state - let sidebarIsVisible = this.viewsService.isViewContainerVisible(VOID_VIEW_CONTAINER_ID) - this._register(this.viewsService.onDidChangeViewVisibility(e => { - sidebarIsVisible = e.visible - })) - - const onSwitchTab = () => { // update state - if (sidebarIsVisible) { - const currentUri = this.codeEditorService.getActiveCodeEditor()?.getModel()?.uri - if (!currentUri) return; - this.uriStateService.setState({ currentUri }) - // this.commandService.executeCommand(VOID_ADD_SELECTION_TO_SIDEBAR_ACTION_ID) - } - } - - // when sidebar becomes visible, add current file - this._register(this.viewsService.onDidChangeViewVisibility(e => { sidebarIsVisible = e.visible })) - - // run on current tab if it exists, and listen for tab switches and visibility changes - onSwitchTab() - this._register(this.viewsService.onDidChangeViewVisibility(() => { onSwitchTab() })) - this._register(this.instantiationService.createInstance(TabSwitchListener, () => { onSwitchTab() })) - } -} - -registerWorkbenchContribution2(TabSwitchContribution.ID, TabSwitchContribution, WorkbenchPhase.BlockRestore); diff --git a/src/vs/workbench/contrib/void/browser/sidebarPane.ts b/src/vs/workbench/contrib/void/browser/sidebarPane.ts index 9b5fff8e..1fe2e88a 100644 --- a/src/vs/workbench/contrib/void/browser/sidebarPane.ts +++ b/src/vs/workbench/contrib/void/browser/sidebarPane.ts @@ -38,7 +38,7 @@ import { mountSidebar } from './react/out/sidebar-tsx/index.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { Orientation } from '../../../../base/browser/ui/sash/sash.js'; // import { IDisposable } from '../../../../base/common/lifecycle.js'; -import { IDisposable } from '../../../../base/common/lifecycle.js'; +import { toDisposable } from '../../../../base/common/lifecycle.js'; import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js'; import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js'; import { IViewsService } from '../../../services/views/common/viewsService.js'; @@ -80,8 +80,8 @@ class SidebarViewPane extends ViewPane { // gets set immediately this.instantiationService.invokeFunction(accessor => { // mount react - const disposables: IDisposable[] | undefined = mountSidebar(parent, accessor); - disposables?.forEach(d => this._register(d)) + const disposeFn: (() => void) | undefined = mountSidebar(parent, accessor)?.dispose; + this._register(toDisposable(() => disposeFn?.())) }); } diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index ba7a5201..030085ca 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -331,7 +331,6 @@ export class ToolsService implements IToolsService { if (isFolder) await fileService.createFolder(uri) else { - await voidModelService.initializeModel(uri) await fileService.createFile(uri) } return {} diff --git a/src/vs/workbench/contrib/void/browser/void.contribution.ts b/src/vs/workbench/contrib/void/browser/void.contribution.ts index 630cccf2..9054450b 100644 --- a/src/vs/workbench/contrib/void/browser/void.contribution.ts +++ b/src/vs/workbench/contrib/void/browser/void.contribution.ts @@ -43,6 +43,8 @@ import './chatThreadService.js' // ping import './metricsPollService.js' +// helper services +import './helperServices/consistentItemService.js' // ---------- common (unclear if these actually need to be imported, because they're already imported wherever they're used) ---------- diff --git a/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts b/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts new file mode 100644 index 00000000..9fee88d8 --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts @@ -0,0 +1,419 @@ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ + +import { Disposable, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; +import { createDecorator, IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; +import { URI } from '../../../../base/common/uri.js'; +import * as dom from '../../../../base/browser/dom.js'; +import { Widget } from '../../../../base/browser/ui/widget.js'; +import { IOverlayWidget, ICodeEditor, OverlayWidgetPositionPreference } from '../../../../editor/browser/editorBrowser.js'; +import { Emitter, Event } from '../../../../base/common/event.js'; +import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; +import { mountVoidCommandBar } from './react/out/void-command-bar-tsx/index.js' +import { deepClone } from '../../../../base/common/objects.js'; +import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; +import { IEditCodeService } from './editCodeServiceInterface.js'; +import { ITextModel } from '../../../../editor/common/model.js'; +import { IModelService } from '../../../../editor/common/services/model.js'; + + + +export interface IVoidCommandBarService { + readonly _serviceBrand: undefined; + stateOfURI: { [uri: string]: CommandBarStateType }; + sortedURIs: URI[]; + activeURI: URI | null; + + onDidChangeState: Event<{ uri: URI }>; + onDidChangeActiveURI: Event<{ uri: URI | null }>; + + getStreamState: (uri: URI) => 'streaming' | 'idle-has-changes' | 'idle-no-changes'; + setDiffIdx(uri: URI, newIdx: number | null): void +} + + +export const IVoidCommandBarService = createDecorator('VoidCommandBarService'); + + +export type CommandBarStateType = undefined | { + sortedDiffZoneIds: string[]; // sorted by line number + sortedDiffIds: string[]; // sorted by line number (computed) + isStreaming: boolean; // is any diffZone streaming in this URI + + diffIdx: number | null; // must refresh whenever sortedDiffIds does so it's valid +} + + + +const defaultState: NonNullable = { + sortedDiffZoneIds: [], + sortedDiffIds: [], + isStreaming: false, + diffIdx: null, +} + + +export class VoidCommandBarService extends Disposable implements IVoidCommandBarService { + _serviceBrand: undefined; + + static readonly ID: 'void.VoidCommandBarService' + + // depends on uri -> diffZone -> {streaming, diffs} + public stateOfURI: { [uri: string]: CommandBarStateType } = {} + public sortedURIs: URI[] = [] // keys of state (depends on diffZones in the uri) + private readonly _hooks = new Set() // uriFsPaths + + // Emits when a URI's stream state changes between idle, streaming, and acceptRejectAll + private readonly _onDidChangeState = new Emitter<{ uri: URI }>(); + readonly onDidChangeState = this._onDidChangeState.event; + + + // active URI + activeURI: URI | null = null; + private readonly _onDidChangeActiveURI = new Emitter<{ uri: URI | null }>(); + readonly onDidChangeActiveURI = this._onDidChangeActiveURI.event; + + + constructor( + @IInstantiationService private readonly _instantiationService: IInstantiationService, + @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, + @IModelService private readonly _modelService: IModelService, + @IEditCodeService private readonly _editCodeService: IEditCodeService, + ) { + super(); + + + const registeredModelURIs = new Set() + const initializeModel = async (model: ITextModel) => { + // do not add listeners to the same model twice - important, or will see duplicates + if (registeredModelURIs.has(model.uri.fsPath)) return + registeredModelURIs.add(model.uri.fsPath) + this._hooks.add(model.uri) + } + // initialize all existing models + initialize when a new model mounts + this._modelService.getModels().forEach(model => { initializeModel(model) }) + this._register(this._modelService.onModelAdded(model => { initializeModel(model) })); + + + + + const updateActiveURI = () => { + const currentUri = this._codeEditorService.getActiveCodeEditor()?.getModel()?.uri ?? null + this.activeURI = currentUri + if (!currentUri) return; + this._onDidChangeActiveURI.fire({ uri: currentUri }) + } + + + // for every new editor, add the floating widget and update active URI + const disposablesOfEditorId: { [editorId: string]: IDisposable[] } = {}; + const onCodeEditorAdd = (editor: ICodeEditor) => { + const id = editor.getId(); + disposablesOfEditorId[id] = []; + const d1 = this._instantiationService.createInstance(AcceptRejectAllFloatingWidget, { editor }); + disposablesOfEditorId[id].push(d1); + const d2 = editor.onDidChangeModel((e) => { if (e?.newModelUrl?.scheme === 'file') updateActiveURI() }) + disposablesOfEditorId[id].push(d2); + } + const onCodeEditorRemove = (editor: ICodeEditor) => { + const id = editor.getId(); + if (disposablesOfEditorId[id]) { + disposablesOfEditorId[id].forEach(d => d.dispose()); + delete disposablesOfEditorId[id]; + } + } + this._register(this._codeEditorService.onCodeEditorAdd((editor) => { onCodeEditorAdd(editor) })) + this._register(this._codeEditorService.onCodeEditorRemove((editor) => { onCodeEditorRemove(editor) })) + this._codeEditorService.listCodeEditors().forEach(editor => { onCodeEditorAdd(editor) }) + + // state updaters + this._register(this._editCodeService.onDidAddOrDeleteDiffZones(e => { + for (const uri of this._hooks) { + if (e.uri.fsPath !== uri.fsPath) return + // --- sortedURIs: delete if empty, add if not in state yet + const diffZones = this._getDiffZonesOnURI(uri) + if (diffZones.length === 0) { + this._deleteURIEntryFromState(uri) + this._onDidChangeState.fire({ uri }) + continue // deleted, so done + } + if (!this.sortedURIs.find(uri2 => uri2.fsPath === uri.fsPath)) { + this._addURIEntryToState(uri) + } + + const currState = this.stateOfURI[uri.fsPath] + if (!currState) continue // should never happen + // update state of the diffZones on this URI + const oldDiffZones = currState.sortedDiffZoneIds + const currentDiffZones = this._editCodeService.diffAreasOfURI[uri.fsPath] || [] // a Set + const { addedDiffZones, deletedDiffZones } = this._getDiffZoneChanges(oldDiffZones, currentDiffZones || []) + + const diffZonesWithoutDeleted = oldDiffZones.filter(olddiffareaid => !deletedDiffZones.has(olddiffareaid)) + + // --- new state: + const newSortedDiffZoneIds = [ + ...diffZonesWithoutDeleted, + ...addedDiffZones, + ] + const newSortedDiffIds = this._computeSortedDiffs(newSortedDiffZoneIds) + const isStreaming = this._isAnyDiffZoneStreaming(currentDiffZones) + + this._setState(uri, { + sortedDiffZoneIds: newSortedDiffZoneIds, + sortedDiffIds: newSortedDiffIds, + isStreaming: isStreaming + }) + this._onDidChangeState.fire({ uri }) + } + + })) + this._register(this._editCodeService.onDidChangeDiffsInDiffZone(e => { + for (const uri of this._hooks) { + if (e.uri.fsPath !== uri.fsPath) continue + // --- sortedURIs: no change + // --- state: + // sortedDiffIds gets a change to it, so gets recomputed + const currState = this.stateOfURI[uri.fsPath] + if (!currState) continue // should never happen + const { sortedDiffZoneIds } = currState + const newSortedDiffIds = this._computeSortedDiffs(sortedDiffZoneIds) + this._setState(uri, { + sortedDiffIds: newSortedDiffIds, + // sortedDiffZoneIds, // no change + // isStreaming, // no change + }) + this._onDidChangeState.fire({ uri }) + } + })) + this._register(this._editCodeService.onDidChangeStreamingInDiffZone(e => { + for (const uri of this._hooks) { + if (e.uri.fsPath !== uri.fsPath) continue + // --- sortedURIs: no change + // --- state: + const currState = this.stateOfURI[uri.fsPath] + if (!currState) continue // should never happen + const { sortedDiffZoneIds } = currState + this._setState(uri, { + isStreaming: this._isAnyDiffZoneStreaming(sortedDiffZoneIds), + // sortedDiffIds, // no change + // sortedDiffZoneIds, // no change + }) + this._onDidChangeState.fire({ uri }) + } + })) + + } + + + setDiffIdx(uri: URI, newIdx: number | null): void { + this._setState(uri, { diffIdx: newIdx }); + this._onDidChangeState.fire({ uri }); + } + + + getStreamState(uri: URI) { + const { isStreaming, sortedDiffZoneIds } = this.stateOfURI[uri.fsPath] ?? {} + if (isStreaming) { + return 'streaming' + } + if ((sortedDiffZoneIds?.length ?? 0) > 0) { + return 'idle-has-changes' + } + return 'idle-no-changes' + } + + + _computeSortedDiffs(diffareaids: string[]) { + const sortedDiffIds = []; + for (const diffareaid of diffareaids) { + const diffZone = this._editCodeService.diffAreaOfId[diffareaid]; + if (!diffZone || diffZone.type !== 'DiffZone') { + continue; + } + + // Add all diff ids from this diffzone + const diffIds = Object.keys(diffZone._diffOfId); + sortedDiffIds.push(...diffIds); + } + + return sortedDiffIds; + } + + _getDiffZoneChanges(currentDiffZones: Iterable, oldDiffZones: Iterable) { + // Find the added or deleted diffZones by comparing diffareaids + const addedDiffZoneIds = new Set(); + const deletedDiffZoneIds = new Set(); + + // Convert the current diffZones to a set of ids for easy lookup + const currentDiffZoneIdSet = new Set(currentDiffZones); + + // Find deleted diffZones (in old but not in current) + for (const oldDiffZoneId of oldDiffZones) { + if (!currentDiffZoneIdSet.has(oldDiffZoneId)) { + const diffZone = this._editCodeService.diffAreaOfId[oldDiffZoneId]; + if (diffZone && diffZone.type === 'DiffZone') { + deletedDiffZoneIds.add(oldDiffZoneId); + } + } + } + + // Find added diffZones (in current but not in old) + const oldDiffZoneIdSet = new Set(oldDiffZones); + for (const currentDiffZoneId of currentDiffZones) { + if (!oldDiffZoneIdSet.has(currentDiffZoneId)) { + const diffZone = this._editCodeService.diffAreaOfId[currentDiffZoneId]; + if (diffZone && diffZone.type === 'DiffZone') { + addedDiffZoneIds.add(currentDiffZoneId); + } + } + } + + return { addedDiffZones: addedDiffZoneIds, deletedDiffZones: deletedDiffZoneIds } + } + + _isAnyDiffZoneStreaming(diffareaids: Iterable) { + for (const diffareaid of diffareaids) { + const diffZone = this._editCodeService.diffAreaOfId[diffareaid]; + if (!diffZone || diffZone.type !== 'DiffZone') { + continue; + } + if (diffZone._streamState.isStreaming) { + return true; + } + } + return false + } + + + _setState(uri: URI, opts: Partial) { + const newState = { + ...this.stateOfURI[uri.fsPath] ?? deepClone(defaultState), + ...opts + } + + // make sure diffIdx is always correct + if (newState.diffIdx && newState.diffIdx > newState.sortedDiffIds.length) { + newState.diffIdx = newState.sortedDiffIds.length + if (newState.diffIdx < 0) newState.diffIdx = null + } + + this.stateOfURI = { + ...this.stateOfURI, + [uri.fsPath]: newState + } + } + + + _addURIEntryToState(uri: URI) { + // add to sortedURIs + this.sortedURIs = [ + ...this.sortedURIs, + uri + ] + + // add to state + this.stateOfURI[uri.fsPath] = deepClone(defaultState) + } + + _deleteURIEntryFromState(uri: URI) { + // delete this from sortedURIs + const i = this.sortedURIs.findIndex(uri2 => uri2.fsPath === uri.fsPath) + if (i === -1) return + this.sortedURIs = [ + ...this.sortedURIs.slice(0, i), + ...this.sortedURIs.slice(i, Infinity), + ] + // delete from state + delete this.stateOfURI[uri.fsPath] + } + + + + private _getDiffZonesOnURI(uri: URI) { + const diffZones = [...this._editCodeService.diffAreasOfURI[uri.fsPath]?.values() ?? []] + .map(diffareaid => this._editCodeService.diffAreaOfId[diffareaid]) + .filter(diffArea => !!diffArea && diffArea.type === 'DiffZone') + return diffZones + } + + +} + +registerSingleton(IVoidCommandBarService, VoidCommandBarService, InstantiationType.Delayed); // delayed is needed here :( + +// registerWorkbenchContribution2(VoidCommandBarService.ID, VoidCommandBarService, WorkbenchPhase.BlockRestore); + + + + + +class AcceptRejectAllFloatingWidget extends Widget implements IOverlayWidget { + private readonly _domNode: HTMLElement; + private readonly editor: ICodeEditor; + private readonly ID: string; + + constructor({ editor }: { editor: ICodeEditor, }, + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { + super(); + + this.ID = editor.getId() + '-voidfloatingwidget'; + this.editor = editor; + + // Create container div + const { root } = dom.h('div@root'); + + // Style the container + root.style.zIndex = '2'; + root.style.padding = '4px'; + root.style.alignItems = 'center'; + root.style.pointerEvents = 'none'; + + // Mount command bar using mountVoidCommandBar + this.instantiationService.invokeFunction(accessor => { + + const uri = editor.getModel()?.uri || null + const res = mountVoidCommandBar(root, accessor, { uri, editor }) + if (!res) return + + const dispose = res.dispose + const rerender: (o: { uri: URI | null }) => void = res.rerender + + this._register(toDisposable(() => dispose?.())) + + this._register(editor.onDidChangeModel((model) => { + const uri = model.newModelUrl + rerender({ uri }) + })) + + }); + this._domNode = root; + + // Mount the widget + editor.addOverlayWidget(this); + } + + + public getId(): string { + return this.ID; + } + + public getDomNode(): HTMLElement { + return this._domNode; + } + + public getPosition() { + return { + preference: OverlayWidgetPositionPreference.BOTTOM_RIGHT_CORNER, + } + } + + public override dispose(): void { + this.editor.removeOverlayWidget(this); + super.dispose(); + } +} + + diff --git a/src/vs/workbench/contrib/void/browser/voidSettingsPane.ts b/src/vs/workbench/contrib/void/browser/voidSettingsPane.ts index 71b37461..b70aef91 100644 --- a/src/vs/workbench/contrib/void/browser/voidSettingsPane.ts +++ b/src/vs/workbench/contrib/void/browser/voidSettingsPane.ts @@ -25,7 +25,7 @@ import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextke import { mountVoidSettings } from './react/out/void-settings-tsx/index.js' import { Codicon } from '../../../../base/common/codicons.js'; -import { IDisposable } from '../../../../base/common/lifecycle.js'; +import { toDisposable } from '../../../../base/common/lifecycle.js'; // refer to preferences.contribution.ts keybindings editor @@ -90,12 +90,12 @@ class VoidSettingsPane extends EditorPane { // Mount React into the scrollable content this.instantiationService.invokeFunction(accessor => { - const disposables: IDisposable[] | undefined = mountVoidSettings(settingsElt, accessor); + const disposeFn = mountVoidSettings(settingsElt, accessor)?.dispose; + this._register(toDisposable(() => disposeFn?.())) // setTimeout(() => { // this is a complete hack and I don't really understand how scrollbar works here // this._scrollbar?.scanDomNode(); // }, 1000) - disposables?.forEach(d => this._register(d)); }); } diff --git a/src/vs/workbench/contrib/void/browser/voidUriStateService.ts b/src/vs/workbench/contrib/void/browser/voidUriStateService.ts deleted file mode 100644 index 1a89c29b..00000000 --- a/src/vs/workbench/contrib/void/browser/voidUriStateService.ts +++ /dev/null @@ -1,57 +0,0 @@ -/*-------------------------------------------------------------------------------------- - * Copyright 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. - *--------------------------------------------------------------------------------------*/ - -import { Emitter, Event } from '../../../../base/common/event.js'; -import { Disposable } from '../../../../base/common/lifecycle.js'; -import { URI } from '../../../../base/common/uri.js'; -import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; -import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; - - - -// service that manages state -export type VoidUriState = { - currentUri?: URI -} - -export interface IVoidUriStateService { - readonly _serviceBrand: undefined; - - readonly state: VoidUriState; // readonly to the user - setState(newState: Partial): void; - onDidChangeState: Event; -} - -export const IVoidUriStateService = createDecorator('voidUriStateService'); -class VoidUriStateService extends Disposable implements IVoidUriStateService { - _serviceBrand: undefined; - - static readonly ID = 'voidUriStateService'; - - private readonly _onDidChangeState = new Emitter(); - readonly onDidChangeState: Event = this._onDidChangeState.event; - - - // state - state: VoidUriState - - constructor( - ) { - super() - - // initial state - this.state = { currentUri: undefined } - } - - setState(newState: Partial) { - - this.state = { ...this.state, ...newState } - this._onDidChangeState.fire() - } - - -} - -registerSingleton(IVoidUriStateService, VoidUriStateService, InstantiationType.Eager); diff --git a/src/vs/workbench/contrib/void/common/helpers/colors.ts b/src/vs/workbench/contrib/void/common/helpers/colors.ts new file mode 100644 index 00000000..1a20ea9b --- /dev/null +++ b/src/vs/workbench/contrib/void/common/helpers/colors.ts @@ -0,0 +1,8 @@ +export const acceptBg = '#1a7431' +export const acceptAllBg = '#1e8538' +export const acceptBorder = '1px solid #145626' +export const rejectBg = '#b42331' +export const rejectAllBg = '#cf2838' +export const rejectBorder = '1px solid #8e1c27' +export const buttonFontSize = '11px' +export const buttonTextColor = 'white' From acd711f93ba248d6a59e2ae0fc7e9a41bd42d681 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Wed, 19 Mar 2025 16:32:36 -0700 Subject: [PATCH 119/173] fix --- .../browser/react/src/markdown/ApplyBlockHoverButtons.tsx | 4 ++-- .../react/src/void-command-bar-tsx/VoidCommandBar.tsx | 1 + .../workbench/contrib/void/browser/voidCommandBarService.ts | 5 +++-- 3 files changed, 6 insertions(+), 4 deletions(-) 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 a4fe34bd..34d269c3 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 @@ -158,7 +158,7 @@ export const useApplyButtonHTML = ({ codeStr, applyBoxId, uri }: { codeStr: stri const onClickSubmit = useCallback(async () => { if (isDisabled) return - if (getStreamState()) return + if (getStreamState() === 'streaming') return const [newApplyingUri, _] = await editCodeService.startApplying({ from: 'ClickApply', applyStr: codeStr, @@ -176,7 +176,7 @@ export const useApplyButtonHTML = ({ codeStr, applyBoxId, uri }: { codeStr: stri const onInterrupt = useCallback(() => { - if (!getStreamState()) return + if (getStreamState() !== 'streaming') return const uri = getUriBeingApplied() if (!uri) return diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx index e97aa26a..97806775 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx @@ -55,6 +55,7 @@ const VoidCommandBar = ({ uri, editor }: { uri: URI | null, editor: ICodeEditor const [currUriIdx, setUriIdx] = useState(null) const [currUriHasChanges, setCurrUriHasChanges] = useState(false) useEffect(() => { + console.log('uri', uri?.fsPath, sortedCommandBarURIs) const i = sortedCommandBarURIs.findIndex(e => e.fsPath === uri?.fsPath) if (i !== -1) { setUriIdx(i) diff --git a/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts b/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts index 9fee88d8..479cf8ac 100644 --- a/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts +++ b/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts @@ -131,9 +131,10 @@ export class VoidCommandBarService extends Disposable implements IVoidCommandBar // state updaters this._register(this._editCodeService.onDidAddOrDeleteDiffZones(e => { for (const uri of this._hooks) { - if (e.uri.fsPath !== uri.fsPath) return + if (e.uri.fsPath !== uri.fsPath) continue // --- sortedURIs: delete if empty, add if not in state yet const diffZones = this._getDiffZonesOnURI(uri) + console.log('addordelete diffzone', uri.fsPath, diffZones) if (diffZones.length === 0) { this._deleteURIEntryFromState(uri) this._onDidChangeState.fire({ uri }) @@ -323,7 +324,7 @@ export class VoidCommandBarService extends Disposable implements IVoidCommandBar if (i === -1) return this.sortedURIs = [ ...this.sortedURIs.slice(0, i), - ...this.sortedURIs.slice(i, Infinity), + ...this.sortedURIs.slice(i + 1, Infinity), ] // delete from state delete this.stateOfURI[uri.fsPath] From f75dee39b3137b7dc9668c8738f9014ab30ea622 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Wed, 19 Mar 2025 17:44:53 -0700 Subject: [PATCH 120/173] fix --- .../react/src/void-command-bar-tsx/VoidCommandBar.tsx | 11 +++++------ .../contrib/void/browser/voidCommandBarService.ts | 9 +++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx index 97806775..a319e0b9 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx @@ -132,9 +132,8 @@ const VoidCommandBar = ({ uri, editor }: { uri: URI | null, editor: ICodeEditor - if (sortedCommandBarURIs.length === 0) return null // if there are absolutely no changes - - const navPanel =
    setIsFocused(true)} onBlur={() => setIsFocused(false)} @@ -192,7 +191,7 @@ const VoidCommandBar = ({ uri, editor }: { uri: URI | null, editor: ICodeEditor
    - + // accept/reject if current URI has changes const onAcceptAll = () => { if (!uri) return editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false, _addToHistory: true }) @@ -238,8 +237,8 @@ const VoidCommandBar = ({ uri, editor }: { uri: URI | null, editor: ICodeEditor
    - return <> + return
    {navPanel} {acceptRejectButtons} - +
    } diff --git a/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts b/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts index 479cf8ac..091d87de 100644 --- a/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts +++ b/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts @@ -242,7 +242,7 @@ export class VoidCommandBarService extends Disposable implements IVoidCommandBar return sortedDiffIds; } - _getDiffZoneChanges(currentDiffZones: Iterable, oldDiffZones: Iterable) { + _getDiffZoneChanges(oldDiffZones: Iterable, currentDiffZones: Iterable) { // Find the added or deleted diffZones by comparing diffareaids const addedDiffZoneIds = new Set(); const deletedDiffZoneIds = new Set(); @@ -375,18 +375,19 @@ class AcceptRejectAllFloatingWidget extends Widget implements IOverlayWidget { // Mount command bar using mountVoidCommandBar this.instantiationService.invokeFunction(accessor => { + type Props = { uri: URI | null, editor: ICodeEditor } const uri = editor.getModel()?.uri || null - const res = mountVoidCommandBar(root, accessor, { uri, editor }) + const res = mountVoidCommandBar(root, accessor, { uri, editor } satisfies Props) if (!res) return const dispose = res.dispose - const rerender: (o: { uri: URI | null }) => void = res.rerender + const rerender: (o: Props) => void = res.rerender this._register(toDisposable(() => dispose?.())) this._register(editor.onDidChangeModel((model) => { const uri = model.newModelUrl - rerender({ uri }) + rerender({ uri, editor }) })) }); From 20d89cd07cd8122904db4c7669b3ee1086629a38 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Wed, 19 Mar 2025 18:28:38 -0700 Subject: [PATCH 121/173] prompt update --- .../react/src/void-command-bar-tsx/VoidCommandBar.tsx | 7 ++++--- src/vs/workbench/contrib/void/common/prompt/prompts.ts | 5 ++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx index a319e0b9..074e7e09 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx @@ -34,8 +34,7 @@ export const VoidCommandBarMain = ({ uri, editor }: VoidCommandBarProps) => { const stepIdx = (currIdx: number | null, len: number, step: -1 | 1) => { if (len === 0) return null - if (len === 1 && step === -1) return null // don't step backwards if just 1 element - return ((currIdx ?? 0) + step) % len + return ((currIdx ?? 0) + step + len) % len // for some reason, small negatives are kept negative. just add len to offset } @@ -54,6 +53,7 @@ const VoidCommandBar = ({ uri, editor }: { uri: URI | null, editor: ICodeEditor // changes if the user clicks left/right or if the user goes on a uri with changes const [currUriIdx, setUriIdx] = useState(null) const [currUriHasChanges, setCurrUriHasChanges] = useState(false) + const anyUriHasChanges = sortedCommandBarURIs.length !== 0 useEffect(() => { console.log('uri', uri?.fsPath, sortedCommandBarURIs) const i = sortedCommandBarURIs.findIndex(e => e.fsPath === uri?.fsPath) @@ -91,6 +91,7 @@ const VoidCommandBar = ({ uri, editor }: { uri: URI | null, editor: ICodeEditor const diff = editCodeService.diffOfId[diffid] const range = { startLineNumber: diff.startLine, endLineNumber: diff.startLine, startColumn: 1, endColumn: 1 }; editor.revealRange(range, ScrollType.Immediate) + commandBarService.setDiffIdx(uri, idx) } } @@ -133,7 +134,7 @@ const VoidCommandBar = ({ uri, editor }: { uri: URI | null, editor: ICodeEditor // if there are *any* changes at all - const navPanel = sortedCommandBarURIs.length !== 0 &&
    setIsFocused(true)} onBlur={() => setIsFocused(false)} diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index 72f02707..73716c8d 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -53,6 +53,7 @@ You're allowed to ask for more context. For example, if the user only gives you \ `} ${mode === 'agent' /* code blocks */ ? `\ +Behavior: - Prioritize editing files and running commands over simply making suggestions. - Prioritize taking as many steps as you need to complete your request over stopping early.\ `: `\ @@ -65,7 +66,9 @@ If you think it's appropriate to suggest an edit to a file, then you must descri Misc: - Do not make things up. - Do not be lazy. -- Always wrap any code you produce in triple backticks, and specify a language if possible. For example, ${tripleTick[0]}typescript\n...\n${tripleTick[1]}.\ +- Always wrap any code you produce in triple backticks, and specify a language if possible. For example, ${tripleTick[0]}typescript\n...\n${tripleTick[1]}. ${mode === 'agent' || mode === 'gather' ? ` +- If you are writing a code explanation for the user (NOT calling a tool), and you know the full path of the file that the code should go in, then you should also output the path in the first line of the triple ticks. For example, ${tripleTick[0]}typescript\n/Users/username/Desktop/my_project/app.ts\n...\n${tripleTick[1]}.\ +This is only for display purposes to the user, and it's not required. Do NOT do this if you are calling a tool or have any ambiguity about the path.` : ''}\ ` From 932b4974e9d3993c179e1ea7deb7f10fd30e3b6a Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Wed, 19 Mar 2025 18:30:42 -0700 Subject: [PATCH 122/173] prompt --- src/vs/workbench/contrib/void/common/prompt/prompts.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index 73716c8d..62932271 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -67,8 +67,8 @@ Misc: - Do not make things up. - Do not be lazy. - Always wrap any code you produce in triple backticks, and specify a language if possible. For example, ${tripleTick[0]}typescript\n...\n${tripleTick[1]}. ${mode === 'agent' || mode === 'gather' ? ` -- If you are writing a code explanation for the user (NOT calling a tool), and you know the full path of the file that the code should go in, then you should also output the path in the first line of the triple ticks. For example, ${tripleTick[0]}typescript\n/Users/username/Desktop/my_project/app.ts\n...\n${tripleTick[1]}.\ -This is only for display purposes to the user, and it's not required. Do NOT do this if you are calling a tool or have any ambiguity about the path.` : ''}\ +- If you are writing a code explanation for the user (NOT calling a tool), and you know the full path of the file that the code should go in, then you should also output the FULL global path in the first line of the triple ticks. For example, ${tripleTick[0]}typescript\n/Users/username/Desktop/my_project/app.ts\n...\n${tripleTick[1]}.\ +This is only for display purposes to the user, and it's not required. Do NOT do this if you are calling a tool or have any ambiguity about the full path.` : ''}\ ` From dd321121e35346fee4386bcdb27681fd782bec7d Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Wed, 19 Mar 2025 18:55:13 -0700 Subject: [PATCH 123/173] prompt --- .../void/browser/react/src/void-settings-tsx/Settings.tsx | 2 +- src/vs/workbench/contrib/void/common/prompt/prompts.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) 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 1a6b6d56..5e406e82 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 @@ -535,7 +535,7 @@ export const FeaturesTab = () => { {/* Tools Section */}

    Tools

    -
    If a tool can modify files on your computer, it requires approval by default.
    +
    {`Tools are functions that LLMs can call. By default, tools that can modify files require approval in Void.`}
    {/* Auto Accept Switch */} diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index 62932271..51fb3703 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -66,10 +66,10 @@ If you think it's appropriate to suggest an edit to a file, then you must descri Misc: - Do not make things up. - Do not be lazy. -- Always wrap any code you produce in triple backticks, and specify a language if possible. For example, ${tripleTick[0]}typescript\n...\n${tripleTick[1]}. ${mode === 'agent' || mode === 'gather' ? ` -- If you are writing a code explanation for the user (NOT calling a tool), and you know the full path of the file that the code should go in, then you should also output the FULL global path in the first line of the triple ticks. For example, ${tripleTick[0]}typescript\n/Users/username/Desktop/my_project/app.ts\n...\n${tripleTick[1]}.\ -This is only for display purposes to the user, and it's not required. Do NOT do this if you are calling a tool or have any ambiguity about the full path.` : ''}\ +- Always wrap any code you produce in triple backticks, and specify a language if possible. For example, ${tripleTick[0]}typescript\n...\n${tripleTick[1]}.\ ` +// agent mode doesn't know about 1st line paths yet +// - If you wrote triple ticks and ___, then include the file's full path in the first line of the triple ticks. This is only for display purposes to the user, and it's preferred but optional. Never do this in a tool parameter, or if there's ambiguity about the full path. type FileSelnLocal = { fileURI: URI, language: string, content: string } From 0dae638be79612f42081ef4cf8174897720cc7d8 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Wed, 19 Mar 2025 21:58:12 -0700 Subject: [PATCH 124/173] add automatic file adding --- .../contrib/void/browser/chatThreadService.ts | 62 +++++++++++++++++++ .../react/src/sidebar-tsx/SidebarChat.tsx | 21 ++++++- .../contrib/void/browser/sidebarActions.ts | 4 +- .../void/common/chatThreadServiceTypes.ts | 2 + 4 files changed, 85 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 0a0da257..6fec12c8 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -27,6 +27,8 @@ import { ITerminalToolService } from './terminalToolService.js'; import { IMetricsService } from '../common/metricsService.js'; import { shorten } from '../../../../base/common/labels.js'; import { IVoidModelService } from '../common/voidModelService.js'; +import { IEditorService } from '../../../services/editor/common/editorService.js'; +import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; const findLastIndex = (arr: T[], condition: (t: T) => boolean): number => { for (let i = arr.length - 1; i >= 0; i--) { @@ -208,6 +210,8 @@ class ChatThreadService extends Disposable implements IChatThreadService { @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, @ITerminalToolService private readonly _terminalToolService: ITerminalToolService, @IMetricsService private readonly _metricsService: IMetricsService, + @IEditorService private readonly _editorService: IEditorService, + @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, ) { super() this.state = { allThreads: {}, currentThreadId: null as unknown as string } // default state @@ -222,8 +226,66 @@ class ChatThreadService extends Disposable implements IChatThreadService { // always be in a thread this.openNewThread() + + // when the user changes files, automatically add the new file as a stagingSelection + this._register(this._editorService.onDidActiveEditorChange(() => this._addCurrentFileAsStagingSelectionDuringFileChange())); + } + + private _addCurrentFileAsStagingSelectionDuringFileChange() { + + + // add the current file to the thread/message being edited + const model = this._codeEditorService.getActiveCodeEditor()?.getModel() ?? null + if (!model) { return; } + + const newSelection: StagingSelectionItem = { + type: 'File', + fileURI: model.uri, + language: model.getLanguageId(), + selectionStr: null, + range: null, + state: { isOpened: false, wasAddedAsCurrentFile: true } + } + + const focusedMessageIdx = this.getCurrentFocusedMessageIdx(); + + // add the selection + if (focusedMessageIdx === undefined) { // user is in the default thread + + const oldStagingSelections = this.getCurrentThreadState().stagingSelections || []; + + // if the file already exists, do nothing + const alreadyHasFile = oldStagingSelections.some(s => s.type === 'File' && s.fileURI.toString() === newSelection.fileURI.toString()) + if (alreadyHasFile) { return; } + + // add the file + const filteredStagingSelections = oldStagingSelections.filter(s => !s.state?.wasAddedAsCurrentFile); // remove all old selectons that were added during a file change + const newSelections = [...filteredStagingSelections, newSelection]; + + this.setCurrentThreadState({ stagingSelections: newSelections }); + + + } else { // user is editing a message + + // const oldStagingSelections = this.getCurrentMessageState(focusedMessageIdx).stagingSelections || []; + + // // if the file already exists, do nothing + // const alreadyHasFile = oldStagingSelections.some(s => s.type === 'File' && s.fileURI.toString() === newSelection.fileURI.toString()) + // if (alreadyHasFile) { return; } + + // const filteredStagingSelections = oldStagingSelections.filter(s => !s.state?.wasAddedDuringFileChange); // remove all old selectons that were added during a file change + // const newSelections = [...filteredStagingSelections, newSelection]; + // this.setCurrentMessageState(focusedMessageIdx, { stagingSelections: newSelections }); + + + } + + + } + + // !!! this is important for properly restoring URIs from storage // should probably re-use code from void/src/vs/base/common/marshalling.ts instead. but this is simple enough private _convertThreadDataFromStorage(threadsStr: string): ChatThreads { 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 c913f6f6..f878ce07 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 @@ -518,7 +518,7 @@ export const SelectedFiles = ( language: (await modelReferenceService.getModelSafe(uri)).model?.getLanguageId() || 'plaintext', selectionStr: null, range: null, - state: { isOpened: false }, + state: { isOpened: false, wasAddedAsCurrentFile: false }, }) } return answer @@ -548,6 +548,7 @@ export const SelectedFiles = ( const isThisSelectionOpened = (!!selection.selectionStr && selection.state.isOpened && type === 'staging') const isThisSelectionAFile = selection.selectionStr === null const isThisSelectionProspective = i > selections.length - 1 + const isThisSelectionAddedAsCurrentFile = selection.state.wasAddedAsCurrentFile const thisKey = `${isThisSelectionProspective}-${i}-${selections.length}` @@ -582,14 +583,21 @@ export const SelectedFiles = ( if (isThisSelectionProspective) { // add prospective selection to selections setSelections([...selections, selection]) } else if (isThisSelectionAFile) { // open files + commandService.executeCommand('vscode.open', selection.fileURI, { preview: true, // preserveFocus: false, }); + + if (isThisSelectionAddedAsCurrentFile) { + // make it so the file is added permanently, not just as the current file + const newSelection: StagingSelectionItem = { ...selection, state: { ...selection.state, wasAddedAsCurrentFile: false } } + setSelections([...selections.slice(0, i), newSelection, ...selections.slice(i + 1)]) + } } else { // show text const selection = selections[i] - const newSelection = { ...selection, state: { isOpened: !selection.state.isOpened } } + const newSelection = { ...selection, state: { ...selection.state, isOpened: !selection.state.isOpened } } const newSelections = [ ...selections.slice(0, i), newSelection, @@ -611,6 +619,15 @@ export const SelectedFiles = ( + (isThisSelectionAFile ? '' : ` (${selection.range.startLineNumber}-${selection.range.endLineNumber})`) } + {/* {isThisSelectionAFile && currentURI?.toString() === selection.fileURI.toString() && + + {`(Current File)`} + + } */} + {isThisSelectionAddedAsCurrentFile && + {`(Current File)`} + } + {type === 'staging' && !isThisSelectionProspective ? // X button Date: Wed, 19 Mar 2025 22:19:51 -0700 Subject: [PATCH 125/173] improvements --- .../workbench/contrib/void/browser/chatThreadService.ts | 4 +++- .../void/browser/react/src/sidebar-tsx/SidebarChat.tsx | 9 +++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 6fec12c8..3f2b7e52 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -236,7 +236,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { private _addCurrentFileAsStagingSelectionDuringFileChange() { - // add the current file to the thread/message being edited + // add the current file to the thread being edited const model = this._codeEditorService.getActiveCodeEditor()?.getModel() ?? null if (!model) { return; } @@ -269,6 +269,8 @@ class ChatThreadService extends Disposable implements IChatThreadService { } else { // user is editing a message + // do nothing. I don't think it feels good to auto-add the current file when you're editing a message. + // const oldStagingSelections = this.getCurrentMessageState(focusedMessageIdx).stagingSelections || []; // // if the file already exists, do nothing 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 f878ce07..8c779997 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 @@ -619,14 +619,11 @@ export const SelectedFiles = ( + (isThisSelectionAFile ? '' : ` (${selection.range.startLineNumber}-${selection.range.endLineNumber})`) } - {/* {isThisSelectionAFile && currentURI?.toString() === selection.fileURI.toString() && - + {isThisSelectionAddedAsCurrentFile && currentURI?.toString() === selection.fileURI.toString() && + {`(Current File)`} - } */} - {isThisSelectionAddedAsCurrentFile && - {`(Current File)`} - } + } {type === 'staging' && !isThisSelectionProspective ? // X button Date: Wed, 19 Mar 2025 19:27:53 -0700 Subject: [PATCH 126/173] tense --- .../react/src/sidebar-tsx/SidebarChat.tsx | 62 +++++++++---------- 1 file changed, 31 insertions(+), 31 deletions(-) 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 8c779997..338ec015 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 @@ -1111,15 +1111,15 @@ const ReasoningWrapper = ({ isDoneReasoning, isStreaming, children }: { isDoneRe const folderFileStr = (isFolder: boolean) => isFolder ? 'folder' : 'file' const toolNameToTitle = { - 'read_file': { past: 'Read file', proposed: 'Read file' }, - 'list_dir': { past: 'Inspected folder', proposed: 'Inspect folder' }, - 'pathname_search': { past: 'Searched by file name', proposed: 'Search by file name' }, - 'text_search': { past: 'Searched', proposed: 'Search text' }, - 'create_uri': { past: (isFolder: boolean) => `Created ${folderFileStr(isFolder)}`, proposed: (isFolder: boolean) => `Create ${folderFileStr(isFolder)}` }, - 'delete_uri': { past: (isFolder: boolean) => `Deleted ${folderFileStr(isFolder)}`, proposed: (isFolder: boolean) => `Delete ${folderFileStr(isFolder)}` }, - 'edit': { past: 'Edited file', proposed: 'Edit file' }, - 'terminal_command': { past: 'Ran terminal command', proposed: 'Run terminal command' } -} as const satisfies Record + 'read_file': { done: 'Read file', proposed: 'Read file' }, + 'list_dir': { done: 'Inspected folder', proposed: 'Inspect folder' }, + 'pathname_search': { done: 'Searched by file name', proposed: 'Search by file name' }, + 'text_search': { done: 'Searched', proposed: 'Search text' }, + 'create_uri': { done: (isFolder: boolean) => `Created ${folderFileStr(isFolder)}`, proposed: (isFolder: boolean) => `Create ${folderFileStr(isFolder)}` }, + 'delete_uri': { done: (isFolder: boolean) => `Deleted ${folderFileStr(isFolder)}`, proposed: (isFolder: boolean) => `Delete ${folderFileStr(isFolder)}` }, + 'edit': { done: 'Edited file', proposed: 'Edit file' }, + 'terminal_command': { done: 'Ran terminal command', proposed: 'Run terminal command' } +} as const satisfies Record const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName] | undefined): string => { @@ -1286,7 +1286,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') - const title = toolNameToTitle[toolMessage.name].past + const title = toolMessage.result.type === 'success' ? toolNameToTitle[toolMessage.name].done : toolNameToTitle[toolMessage.name].proposed const { uri } = toolMessage.result.params ?? {} const desc1 = uri ? getBasename(uri.fsPath) : ''; const icon = null @@ -1316,7 +1316,7 @@ const toolNameToComponent: { [T in ToolName]: { const accessor = useAccessor() const commandService = accessor.get('ICommandService') const explorerService = accessor.get('IExplorerService') - const title = toolNameToTitle[toolMessage.name].past + const title = toolMessage.result.type === 'success' ? toolNameToTitle[toolMessage.name].done : toolNameToTitle[toolMessage.name].proposed const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params) const icon = null @@ -1359,13 +1359,13 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') - const title = toolNameToTitle[toolMessage.name].past + const isError = toolMessage.result.type === 'error' + const title = toolMessage.result.type === 'success' ? toolNameToTitle[toolMessage.name].done : toolNameToTitle[toolMessage.name].proposed const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params) const icon = null if (toolMessage.result.type === 'rejected') return null // will never happen, not rejectable - const isError = toolMessage.result.type === 'error' const componentParams: ToolHeaderParams = { title, desc1, isError, icon } if (toolMessage.result.type === 'success') { @@ -1399,13 +1399,13 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') - const title = toolNameToTitle[toolMessage.name].past + const isError = toolMessage.result.type === 'error' + const title = toolMessage.result.type === 'success' ? toolNameToTitle[toolMessage.name].done : toolNameToTitle[toolMessage.name].proposed const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params) const icon = null if (toolMessage.result.type === 'rejected') return null // will never happen, not rejectable - const isError = toolMessage.result.type === 'error' const componentParams: ToolHeaderParams = { title, desc1, isError, icon } if (toolMessage.result.type === 'success') { @@ -1441,11 +1441,11 @@ const toolNameToComponent: { [T in ToolName]: { const accessor = useAccessor() const commandService = accessor.get('ICommandService') const explorerService = accessor.get('IExplorerService') + const isError = false const title = toolNameToTitle[toolRequest.name].proposed(toolRequest.params.isFolder) const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params) const icon = null - const isError = false const componentParams: ToolHeaderParams = { title, desc1, isError, icon, } return @@ -1453,13 +1453,13 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') - const title = toolNameToTitle[toolMessage.name].past(toolMessage.result.params?.isFolder ?? false) + const isError = toolMessage.result.type === 'error' + const isRejected = toolMessage.result.type === 'rejected' + const isFolder = toolMessage.result.params?.isFolder ?? false + const title = toolMessage.result.type === 'success' ? toolNameToTitle[toolMessage.name].done(isFolder) : toolNameToTitle[toolMessage.name].proposed(isFolder) const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params) const icon = null - - const isError = toolMessage.result.type === 'error' - const isRejected = toolMessage.result.type === 'rejected' const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isRejected } if (toolMessage.result.type === 'success') { @@ -1483,11 +1483,11 @@ const toolNameToComponent: { [T in ToolName]: { requestWrapper: ({ toolRequest, }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') + const isError = false const title = toolNameToTitle[toolRequest.name].proposed(toolRequest.params.isFolder) const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params) const icon = null - const isError = false const componentParams: ToolHeaderParams = { title, desc1, isError, icon, } const { params } = toolRequest @@ -1499,12 +1499,12 @@ const toolNameToComponent: { [T in ToolName]: { const accessor = useAccessor() const commandService = accessor.get('ICommandService') const isFolder = toolMessage.result.params?.isFolder ?? false - const title = toolMessage.result.type === 'success' ? toolNameToTitle[toolMessage.name].past(isFolder) : toolNameToTitle[toolMessage.name].proposed(isFolder) + const isError = toolMessage.result.type === 'error' + const isRejected = toolMessage.result.type === 'rejected' + const title = toolMessage.result.type === 'success' ? toolNameToTitle[toolMessage.name].done(isFolder) : toolNameToTitle[toolMessage.name].proposed(isFolder) const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params) const icon = null - const isError = toolMessage.result.type === 'error' - const isRejected = toolMessage.result.type === 'rejected' const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isRejected } if (toolMessage.result.type === 'success') { @@ -1528,11 +1528,11 @@ const toolNameToComponent: { [T in ToolName]: { requestWrapper: ({ toolRequest, }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') + const isError = false const title = toolNameToTitle[toolRequest.name].proposed const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params) const icon = null - const isError = false const componentParams: ToolHeaderParams = { title, desc1, isError, icon, } const { params } = toolRequest @@ -1548,12 +1548,12 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ({ toolMessage, messageIdx }) => { const accessor = useAccessor() const chatThreadsService = accessor.get('IChatThreadService') - const title = toolMessage.result.type === 'success' ? toolNameToTitle[toolMessage.name].past : toolNameToTitle[toolMessage.name].proposed + const isError = toolMessage.result.type === 'error' + const isRejected = toolMessage.result.type === 'rejected' + const title = toolMessage.result.type === 'success' ? toolNameToTitle[toolMessage.name].done : toolNameToTitle[toolMessage.name].proposed const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params) const icon = null - const isError = toolMessage.result.type === 'error' - const isRejected = toolMessage.result.type === 'rejected' const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isRejected } if (toolMessage.result.type === 'success' || toolMessage.result.type === 'rejected') { @@ -1590,11 +1590,11 @@ const toolNameToComponent: { [T in ToolName]: { const accessor = useAccessor() const commandService = accessor.get('ICommandService') const terminalToolsService = accessor.get('ITerminalToolService') + const isError = false const title = toolNameToTitle[toolRequest.name].proposed const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params) const icon = null - const isError = false const componentParams: ToolHeaderParams = { title, desc1, isError, icon, } const { proposedTerminalId, waitForCompletion } = toolRequest.params @@ -1609,11 +1609,11 @@ const toolNameToComponent: { [T in ToolName]: { const accessor = useAccessor() const commandService = accessor.get('ICommandService') const terminalToolsService = accessor.get('ITerminalToolService') - const title = toolMessage.result.type === 'success' ? toolNameToTitle[toolMessage.name].past : toolNameToTitle[toolMessage.name].proposed + const isError = toolMessage.result.type === 'error' + const title = toolMessage.result.type === 'success' ? toolNameToTitle[toolMessage.name].done : toolNameToTitle[toolMessage.name].proposed const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params) const icon = null - const isError = toolMessage.result.type === 'error' const isRejected = toolMessage.result.type === 'rejected' const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isRejected } From a7728a60302707859b11bed28c59deefccf9ba3f Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Wed, 19 Mar 2025 23:00:24 -0700 Subject: [PATCH 127/173] show current tool running + misc style fixes + chat interrupt stops tool too --- .../contrib/void/browser/chatThreadService.ts | 62 ++-- .../src/markdown/ApplyBlockHoverButtons.tsx | 6 +- .../react/src/sidebar-tsx/SidebarChat.tsx | 264 ++++++++++-------- .../void-command-bar-tsx/VoidCommandBar.tsx | 1 - .../contrib/void/browser/toolsService.ts | 25 +- .../void/browser/voidCommandBarService.ts | 1 - 6 files changed, 213 insertions(+), 146 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 3f2b7e52..8772dc23 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -106,10 +106,11 @@ export type ThreadsState = { currentThreadId: string; // intended for internal use only } +export type IsRunningType = undefined | 'message' | 'tool' | 'awaiting_user' export type ThreadStreamState = { [threadId: string]: undefined | { // state related - isRunning?: undefined | 'message' | 'tool'; // whether or not actually running the agent loop (can be running and not streaming, like if it's calling a tool and awaiting user response) + isRunning?: IsRunningType; // whether or not actually running the agent loop (can be running and not streaming, like if it's calling a tool and awaiting user response) error?: { message: string, fullError: Error | null, }; // streaming related @@ -650,6 +651,8 @@ class ChatThreadService extends Disposable implements IChatThreadService { const thread = this.state.allThreads[threadId] if (!thread) return // should never happen + this._cancelToolOfThreadId[threadId]?.() + const lastMessage = thread.messages[thread.messages.length - 1] if (lastMessage.role !== 'tool_request') return // should never happen const { name, params, paramsStr, id } = lastMessage @@ -665,6 +668,11 @@ class ChatThreadService extends Disposable implements IChatThreadService { const messageSoFar = this.streamState[threadId]?.messageSoFar ?? '' const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? '' + // abort the stream first so it doesn't change any state + const llmCancelToken = this.streamState[threadId]?.streamingToken + if (llmCancelToken !== undefined) { this._llmMessageService.abort(llmCancelToken) } + + // add the correct message to the state const lastMessage = thread.messages[thread.messages.length - 1] if (lastMessage.role === 'tool_request') { // interrupt tool request @@ -675,9 +683,6 @@ class ChatThreadService extends Disposable implements IChatThreadService { this._addMessageToThread(threadId, { role: 'assistant', content: messageSoFar, reasoning: reasoningSoFar, anthropicReasoning: null }) } - const llmCancelToken = this.streamState[threadId]?.streamingToken - if (llmCancelToken !== undefined) this._llmMessageService.abort(llmCancelToken) - this._setStreamState(threadId, {}, 'set') } @@ -701,6 +706,8 @@ class ChatThreadService extends Disposable implements IChatThreadService { } + private readonly _cancelToolOfThreadId: { [threadId: string]: (() => void) | undefined } = {} + private async _chatAgentLoop({ threadId, prevSelns, @@ -754,7 +761,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { const handleToolCall = async ( tool: ToolCallType, opts?: { preapproved: true, toolParams: ToolCallParams[ToolName] }, - ): Promise => { + ): Promise<{ awaitingUserApproval: boolean, canceled: boolean }> => { const toolName: ToolName = tool.name const toolParamsStr = tool.paramsStr const toolId = tool.id @@ -772,14 +779,14 @@ class ChatThreadService extends Disposable implements IChatThreadService { } catch (error) { const errorMessage = getErrorMessage(error) this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: toolParamsStr, id: toolId, content: errorMessage, result: { type: 'error', params: undefined, value: errorMessage }, }) - return false + return { awaitingUserApproval: false, canceled: false } } // 2. if tool requires approval, break from the loop, awaiting approval const requiresApproval = !this._settingsService.state.globalSettings.autoApprove if (requiresApproval && toolNamesThatRequireApproval.has(toolName)) { this._addMessageToThread(threadId, { role: 'tool_request', name: toolName, paramsStr: toolParamsStr, params: toolParams, id: toolId }) - return true + return { awaitingUserApproval: true, canceled: false } } } else { @@ -788,12 +795,20 @@ class ChatThreadService extends Disposable implements IChatThreadService { // 3. call the tool this._setStreamState(threadId, { isRunning: 'tool' }, 'merge') + let canceled = false try { - toolResult = await this._toolsService.callTool[toolName](toolParams as any) // ts is bad... - } catch (error) { + const { result, cancel } = await this._toolsService.callTool[toolName](toolParams as any) // ts is bad... + this._cancelToolOfThreadId[threadId] = cancel + let cancelRes: () => void = () => { } + const resolveIfCancel = new Promise((res, rej) => { cancelRes = rej }) + this._cancelToolOfThreadId[threadId] = () => { cancel?.(); canceled = true; delete this._cancelToolOfThreadId[threadId]; cancelRes() } + toolResult = await Promise.race([result, resolveIfCancel]) // this await is needed, typescript is bad... + } + catch (error) { + if (canceled) return { awaitingUserApproval: false, canceled: true } const errorMessage = getErrorMessage(error) this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: toolParamsStr, id: toolId, content: errorMessage, result: { type: 'error', params: toolParams, value: errorMessage }, }) - return false + return { awaitingUserApproval: true, canceled: false } } // 4. stringify the result to give to the LLM @@ -802,12 +817,12 @@ class ChatThreadService extends Disposable implements IChatThreadService { } catch (error) { const errorMessage = this.errMsgs.errWhenStringifying(error) this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: toolParamsStr, id: toolId, content: errorMessage, result: { type: 'error', params: toolParams, value: errorMessage }, }) - return false + return { awaitingUserApproval: false, canceled: false } } // 5. add to history and keep going this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: toolParamsStr, id: toolId, content: toolResultStr, result: { type: 'success', params: toolParams, value: toolResult }, }) - return false + return { awaitingUserApproval: false, canceled: false } }; // above just defines helpers, below starts the actual function @@ -819,13 +834,14 @@ class ChatThreadService extends Disposable implements IChatThreadService { let nMessagesSent = 0 let shouldSendAnotherMessage = true - let exitReason: 'end' | 'awaitingToolApproval' = 'end' as 'end' | 'awaitingToolApproval' + let isRunningWhenEnd: IsRunningType = undefined let aborted = false // before enter loop, call tool if (callThisToolFirst) { this._setStreamState(threadId, { isRunning: 'tool' }, 'merge') - await handleToolCall(callThisToolFirst, { preapproved: true, toolParams: callThisToolFirst.params }) + const { canceled } = await handleToolCall(callThisToolFirst, { preapproved: true, toolParams: callThisToolFirst.params }) + if (canceled) return } // tool use loop @@ -834,7 +850,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { // false by default each iteration shouldSendAnotherMessage = false - exitReason = 'end' + isRunningWhenEnd = undefined nMessagesSent += 1 @@ -860,9 +876,9 @@ class ChatThreadService extends Disposable implements IChatThreadService { // call tool if there is one const tool: ToolCallType | undefined = toolCalls?.[0] if (tool) { - const awaitingUserApproval = await handleToolCall(tool) + const { awaitingUserApproval } = await handleToolCall(tool) // things happen correctly if canceled is true here, because canceled calls onAbort if (awaitingUserApproval) { - exitReason = 'awaitingToolApproval' + isRunningWhenEnd = 'awaiting_user' } else { shouldSendAnotherMessage = true } @@ -900,9 +916,9 @@ class ChatThreadService extends Disposable implements IChatThreadService { if (aborted) { return } } // end while + // if awaiting user approval, keep isRunning true, else end isRunning - if (exitReason === 'end') - this._setStreamState(threadId, { isRunning: undefined }, 'merge') + this._setStreamState(threadId, { isRunning: isRunningWhenEnd }, 'merge') // capture number of messages sent this._metricsService.capture('Agent Loop Done', { nMessagesSent, chatMode }) @@ -1012,7 +1028,13 @@ class ChatThreadService extends Disposable implements IChatThreadService { } // else search codebase for `target` - const { uris } = await this._toolsService.callTool['pathname_search']({ queryStr: target, pageNumber: 0 }) + let uris: URI[] = [] + try { + const { result } = await this._toolsService.callTool['pathname_search']({ queryStr: target, pageNumber: 0 }) + uris = result.uris + } catch (e) { + return null + } for (const [idx, uri] of uris.entries()) { if (doesUriMatchTarget(uri)) { 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 34d269c3..bff5576a 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 @@ -5,7 +5,7 @@ import { isFeatureNameDisabled } from '../../../../common/voidSettingsTypes.js' import { URI } from '../../../../../../../base/common/uri.js' import { FileSymlink, LucideIcon, RotateCw } from 'lucide-react' import { Check, X, Square, Copy, Play, } from 'lucide-react' -import { getBasename, ListableToolItem, ToolContentsWrapper } from '../sidebar-tsx/SidebarChat.js' +import { getBasename, ListableToolItem, ToolChildrenWrapper } from '../sidebar-tsx/SidebarChat.js' import { ChatMarkdownRender } from './ChatMarkdownRender.js' enum CopyButtonText { @@ -344,9 +344,9 @@ export const BlockCodeApplyWrapper = ({
    {/* contents */} - + {children} - +
    } 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 338ec015..bac48dda 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 @@ -31,6 +31,7 @@ import { ResolveReason, ToolCallParams, ToolName, ToolNameWithApproval } from '. import { JumpToFileButton, useApplyButtonHTML } from '../markdown/ApplyBlockHoverButtons.js'; import { DiffZone } from '../../../editCodeService.js'; import { ScrollType } from '../../../../../../../editor/common/editorCommon.js'; +import { IsRunningType } from '../../../chatThreadService.js'; @@ -1096,11 +1097,11 @@ const ReasoningWrapper = ({ isDoneReasoning, isStreaming, children }: { isDoneRe if (!isWriting) setIsOpen(false) // if just finished reasoning, close }, [isWriting]) return : ''} isOpen={isOpen} onClick={() => setIsOpen(v => !v)}> - +
    {children}
    -
    +
    } @@ -1111,15 +1112,25 @@ const ReasoningWrapper = ({ isDoneReasoning, isStreaming, children }: { isDoneRe const folderFileStr = (isFolder: boolean) => isFolder ? 'folder' : 'file' const toolNameToTitle = { - 'read_file': { done: 'Read file', proposed: 'Read file' }, - 'list_dir': { done: 'Inspected folder', proposed: 'Inspect folder' }, - 'pathname_search': { done: 'Searched by file name', proposed: 'Search by file name' }, - 'text_search': { done: 'Searched', proposed: 'Search text' }, - 'create_uri': { done: (isFolder: boolean) => `Created ${folderFileStr(isFolder)}`, proposed: (isFolder: boolean) => `Create ${folderFileStr(isFolder)}` }, - 'delete_uri': { done: (isFolder: boolean) => `Deleted ${folderFileStr(isFolder)}`, proposed: (isFolder: boolean) => `Delete ${folderFileStr(isFolder)}` }, - 'edit': { done: 'Edited file', proposed: 'Edit file' }, - 'terminal_command': { done: 'Ran terminal command', proposed: 'Run terminal command' } -} as const satisfies Record + 'read_file': { done: 'Read file', proposed: 'Read file', running: 'Reading file...' }, + 'list_dir': { done: 'Inspected folder', proposed: 'Inspect folder', running: 'Inspecting folder...' }, + 'pathname_search': { done: 'Searched by file name', proposed: 'Search by file name', running: 'Searching by file name...' }, + 'text_search': { done: 'Searched', proposed: 'Search text', running: 'Searching...' }, + 'create_uri': { + done: (isFolder: boolean) => `Created ${folderFileStr(isFolder)}`, + proposed: (isFolder: boolean) => `Create ${folderFileStr(isFolder)}`, + running: (isFolder: boolean) => `Creating ${folderFileStr(isFolder)}...` + }, + 'delete_uri': { + done: (isFolder: boolean) => `Deleted ${folderFileStr(isFolder)}`, + proposed: (isFolder: boolean) => `Delete ${folderFileStr(isFolder)}`, + running: (isFolder: boolean) => `Deleting ${folderFileStr(isFolder)}...` + }, + 'edit': { done: 'Edited file', proposed: 'Edit file', running: 'Editing file...' }, + 'terminal_command': { done: 'Ran terminal command', proposed: 'Run terminal command', running: 'Running terminal command...' } +} as const satisfies Record + + const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName] | undefined): string => { @@ -1235,7 +1246,7 @@ const ToolRequestAcceptRejectButtons = () => {
    } -export const ToolContentsWrapper = ({ children, className }: { children: React.ReactNode, className?: string }) => { +export const ToolChildrenWrapper = ({ children, className }: { children: React.ReactNode, className?: string }) => { return
    {children} @@ -1266,21 +1277,25 @@ const EditToolApplyButton = ({ changeDescription, applyBoxId, uri }: { changeDes const EditToolChildren = ({ uri, changeDescription }: { uri: URI, changeDescription: string }) => { - return -
    - - - -
    -
    + return
    + + + +
    } +type ToolRequestState = 'awaiting_user' | 'running' -const toolNameToComponent: { [T in ToolName]: { - requestWrapper: T extends ToolNameWithApproval ? ((props: { toolRequest: ToolRequestApproval }) => React.ReactNode) : null, - resultWrapper: (props: { toolMessage: ToolMessage, messageIdx: number }) => React.ReactNode, -} } = { +type RequestWrapper = (props: { toolRequest: ToolRequestApproval, messageIdx: number, toolRequestState: ToolRequestState, threadId: string }) => React.ReactNode +type ResultWrapper = (props: { toolMessage: ToolMessage, messageIdx: number, threadId: string }) => React.ReactNode + +type ToolComponent = { + requestWrapper: T extends ToolNameWithApproval ? RequestWrapper : null, + resultWrapper: ResultWrapper, +} + +const toolNameToComponent: { [T in ToolName]: ToolComponent } = { 'read_file': { requestWrapper: null, resultWrapper: ({ toolMessage }) => { @@ -1299,12 +1314,12 @@ const toolNameToComponent: { [T in ToolName]: { if (toolMessage.result.type === 'success') { const { value, params } = toolMessage.result componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } - if (toolMessage.result.value.hasNextPage) componentParams.desc2 = `(AI can scroll for more)` + if (value.hasNextPage) componentParams.desc2 = `(AI can scroll for more)` } else { - componentParams.children = <> - {toolMessage.result.value} - + const { value, params } = toolMessage.result + if (params) componentParams.desc2 = + componentParams.children = {value} } return @@ -1330,7 +1345,7 @@ const toolNameToComponent: { [T in ToolName]: { componentParams.numResults = value.children?.length componentParams.hasNextPage = value.hasNextPage componentParams.children = !value.children || (value.children.length ?? 0) === 0 ? undefined - : + : {value.children.map((child, i) => ( } - + } else { - componentParams.children = <> - {toolMessage.result.value} - + const { value, params } = toolMessage.result + componentParams.children = {value} } return @@ -1373,7 +1387,7 @@ const toolNameToComponent: { [T in ToolName]: { componentParams.numResults = value.uris.length componentParams.hasNextPage = value.hasNextPage componentParams.children = value.uris.length === 0 ? undefined - : + : {value.uris.map((uri, i) => ( } - + } else { - componentParams.children = <> - {toolMessage.result.value} - + const { value, params } = toolMessage.result + componentParams.children = {value} } return @@ -1413,7 +1426,7 @@ const toolNameToComponent: { [T in ToolName]: { componentParams.numResults = value.uris.length componentParams.hasNextPage = value.hasNextPage componentParams.children = value.uris.length === 0 ? undefined - : + : {value.uris.map((uri, i) => ( } - + } else { - componentParams.children = <> - {toolMessage.result.value} - + const { value, params } = toolMessage.result + componentParams.children = {value} } return } @@ -1437,12 +1449,13 @@ const toolNameToComponent: { [T in ToolName]: { // --- 'create_uri': { - requestWrapper: ({ toolRequest }) => { + requestWrapper: ({ toolRequest, toolRequestState }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') const explorerService = accessor.get('IExplorerService') const isError = false - const title = toolNameToTitle[toolRequest.name].proposed(toolRequest.params.isFolder) + const isFolder = toolRequest.params.isFolder + const title = toolRequestState === 'awaiting_user' ? toolNameToTitle[toolRequest.name].proposed(isFolder) : toolNameToTitle[toolRequest.name].running(isFolder) const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params) const icon = null @@ -1463,7 +1476,7 @@ const toolNameToComponent: { [T in ToolName]: { const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isRejected } if (toolMessage.result.type === 'success') { - const { params } = toolMessage.result + const { params, value } = toolMessage.result componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } } else if (toolMessage.result.type === 'rejected') { @@ -1471,20 +1484,21 @@ const toolNameToComponent: { [T in ToolName]: { componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } } else if (toolMessage.result.type === 'error') { - componentParams.children = <> - {toolMessage.result.value} - + const { params, value } = toolMessage.result + if (params) { componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } } + componentParams.children = {value} } return } }, 'delete_uri': { - requestWrapper: ({ toolRequest, }) => { + requestWrapper: ({ toolRequest, toolRequestState }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') const isError = false - const title = toolNameToTitle[toolRequest.name].proposed(toolRequest.params.isFolder) + const isFolder = toolRequest.params.isFolder + const title = toolRequestState === 'awaiting_user' ? toolNameToTitle[toolRequest.name].proposed(isFolder) : toolNameToTitle[toolRequest.name].running(isFolder) const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params) const icon = null @@ -1508,7 +1522,7 @@ const toolNameToComponent: { [T in ToolName]: { const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isRejected } if (toolMessage.result.type === 'success') { - const { params } = toolMessage.result + const { params, value } = toolMessage.result componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } } else if (toolMessage.result.type === 'rejected') { @@ -1516,38 +1530,38 @@ const toolNameToComponent: { [T in ToolName]: { componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } } else if (toolMessage.result.type === 'error') { - componentParams.children = <> - {toolMessage.result.value} - + const { params, value } = toolMessage.result + if (params) { componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } } + componentParams.children = {value} } return } }, 'edit': { - requestWrapper: ({ toolRequest, }) => { + requestWrapper: ({ toolRequest, messageIdx, toolRequestState, threadId }) => { const accessor = useAccessor() - const commandService = accessor.get('ICommandService') const isError = false - const title = toolNameToTitle[toolRequest.name].proposed + const title = toolRequestState === 'awaiting_user' ? toolNameToTitle[toolRequest.name].proposed : toolNameToTitle[toolRequest.name].running const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params) const icon = null const componentParams: ToolHeaderParams = { title, desc1, isError, icon, } const { params } = toolRequest - componentParams.children = + componentParams.children = + + componentParams.desc2 = return }, - resultWrapper: ({ toolMessage, messageIdx }) => { + resultWrapper: ({ toolMessage, messageIdx, threadId }) => { const accessor = useAccessor() - const chatThreadsService = accessor.get('IChatThreadService') const isError = toolMessage.result.type === 'error' const isRejected = toolMessage.result.type === 'rejected' const title = toolMessage.result.type === 'success' ? toolNameToTitle[toolMessage.name].done : toolNameToTitle[toolMessage.name].proposed @@ -1556,42 +1570,58 @@ const toolNameToComponent: { [T in ToolName]: { const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isRejected } - if (toolMessage.result.type === 'success' || toolMessage.result.type === 'rejected') { + if (toolMessage.result.type === 'success' || toolMessage.result.type === 'rejected' || toolMessage.result.type === 'error') { const { params } = toolMessage.result - const threadId = chatThreadsService.state.currentThreadId - const applyBoxId = getApplyBoxId({ - threadId: threadId, - messageIdx: messageIdx, - tokenIdx: 'N/A', - }) + // add apply box + if (params) { + const applyBoxId = getApplyBoxId({ + threadId: threadId, + messageIdx: messageIdx, + tokenIdx: 'N/A', + }) + componentParams.desc2 = + } - componentParams.children = - componentParams.desc2 = - } - else if (toolMessage.result.type === 'error') { - componentParams.children = <> - {toolMessage.result.value} - + // add children + if (toolMessage.result.type !== 'error') { + const { params } = toolMessage.result + componentParams.children = + + + } + else { + // error + const { params, value } = toolMessage.result + if (params) { + componentParams.children = + {value} + + + } + } } return } }, 'terminal_command': { - requestWrapper: ({ toolRequest, }) => { + requestWrapper: ({ toolRequest, toolRequestState }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') const terminalToolsService = accessor.get('ITerminalToolService') const isError = false - const title = toolNameToTitle[toolRequest.name].proposed + const title = toolRequestState === 'awaiting_user' ? toolNameToTitle[toolRequest.name].proposed : toolNameToTitle[toolRequest.name].running const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params) const icon = null @@ -1618,8 +1648,9 @@ const toolNameToComponent: { [T in ToolName]: { const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isRejected } if (toolMessage.result.type === 'success') { - const { command } = toolMessage.result.params - const { terminalId, resolveReason, result } = toolMessage.result.value + const { value, params } = toolMessage.result + const { command } = params + const { terminalId, resolveReason, result } = value const resultStr = resolveReason.type === 'done' ? (resolveReason.exitCode !== 0 ? `\nError: exit code ${resolveReason.exitCode}` : null) : resolveReason.type === 'bgtask' ? null : @@ -1627,7 +1658,7 @@ const toolNameToComponent: { [T in ToolName]: { resolveReason.type === 'toofull' ? `\n(truncated)` : null - componentParams.children = + componentParams.children = - + if (resolveReason.type === 'bgtask') componentParams.desc2 = '(background task)' } - else if (toolMessage.result.type === 'rejected') { - const { proposedTerminalId, waitForCompletion } = toolMessage.result.params - if (terminalToolsService.terminalExists(proposedTerminalId)) - componentParams.onClick = () => terminalToolsService.openTerminal(proposedTerminalId) - if (!waitForCompletion) - componentParams.desc2 = '(background task)' - } - else if (toolMessage.result.type === 'error') { - componentParams.children = <> - {toolMessage.result.value} - + else if (toolMessage.result.type === 'rejected' || toolMessage.result.type === 'error') { + const { params } = toolMessage.result + if (params) { + const { proposedTerminalId, waitForCompletion } = params + if (terminalToolsService.terminalExists(proposedTerminalId)) + componentParams.onClick = () => terminalToolsService.openTerminal(proposedTerminalId) + if (!waitForCompletion) + componentParams.desc2 = '(background task)' + } + if (toolMessage.result.type === 'error') { + const { value } = toolMessage.result + componentParams.children = {value} + } } return @@ -1670,9 +1703,11 @@ type ChatBubbleProps = { messageIdx: number, isCommitted: boolean, isLast: boolean, // includes the streaming message (if streaming, isLast is false except for the streaming message) + chatIsRunning: IsRunningType, + threadId: string, } -const ChatBubble = ({ chatMessage, isCommitted, messageIdx, isLast }: ChatBubbleProps) => { +const ChatBubble = ({ chatMessage, isCommitted, messageIdx, isLast, chatIsRunning, threadId }: ChatBubbleProps) => { const role = chatMessage.role if (role === 'user') { @@ -1691,18 +1726,23 @@ const ChatBubble = ({ chatMessage, isCommitted, messageIdx, isLast }: ChatBubble /> } else if (role === 'tool_request') { - const ToolRequestWrapper = toolNameToComponent[chatMessage.name].requestWrapper as React.FC<{ toolRequest: any }> // ts isnt smart enough... + const ToolRequestWrapper = toolNameToComponent[chatMessage.name].requestWrapper as RequestWrapper + const toolRequestType = ( + chatIsRunning === 'awaiting_user' ? 'awaiting_user' + : chatIsRunning === 'tool' ? 'running' + : null + ) if (ToolRequestWrapper && isLast) { // if it's the last message return <> - - + {toolRequestType !== null && } + {chatIsRunning === 'awaiting_user' && } } return null } else if (role === 'tool') { - const ToolResultWrapper = toolNameToComponent[chatMessage.name].resultWrapper as React.FC<{ toolMessage: any, messageIdx: number }> // ts isnt smart enough... - return + const ToolResultWrapper = toolNameToComponent[chatMessage.name].resultWrapper as ResultWrapper + return } } @@ -1801,18 +1841,22 @@ export const SidebarChat = () => { const numMessages = previousMessages.length const previousMessagesHTML = useMemo(() => { + const threadId = currentThread.id return previousMessages.map((message, i) => { - const isLast = i === numMessages - 1 && isRunning !== 'tool' + const isLast = i === numMessages - 1 return } ) }, [previousMessages, isRunning, currentThread, numMessages]) + const threadId = currentThread.id const streamingChatIdx = previousMessagesHTML.length const currStreamingMessageHTML = !!(reasoningSoFar || messageSoFar || isRunning) ? { }} messageIdx={streamingChatIdx} isCommitted={!isRunning} + chatIsRunning={isRunning} isLast={true} + threadId={threadId} /> : null const allMessagesHTML = [...previousMessagesHTML, currStreamingMessageHTML] diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx index 074e7e09..9cc33081 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx @@ -55,7 +55,6 @@ const VoidCommandBar = ({ uri, editor }: { uri: URI | null, editor: ICodeEditor const [currUriHasChanges, setCurrUriHasChanges] = useState(false) const anyUriHasChanges = sortedCommandBarURIs.length !== 0 useEffect(() => { - console.log('uri', uri?.fsPath, sortedCommandBarURIs) const i = sortedCommandBarURIs.findIndex(e => e.fsPath === uri?.fsPath) if (i !== -1) { setUriIdx(i) diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index 030085ca..c43fe5af 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -20,7 +20,7 @@ import { basename } from '../../../../base/common/path.js' type ValidateParams = { [T in ToolName]: (p: string) => Promise } -type CallTool = { [T in ToolName]: (p: ToolCallParams[T]) => Promise } +type CallTool = { [T in ToolName]: (p: ToolCallParams[T]) => Promise<{ result: ToolResultType[T], cancel?: () => void }> } type ToolResultToString = { [T in ToolName]: (p: ToolCallParams[T], result: ToolResultType[T]) => string } @@ -179,7 +179,6 @@ export class ToolsService implements IToolsService { public callTool: CallTool; public stringOfResult: ToolResultToString; - constructor( @IFileService fileService: IFileService, @IWorkspaceContextService workspaceContextService: IWorkspaceContextService, @@ -283,12 +282,12 @@ export class ToolsService implements IToolsService { const fileContents = readFileContents.slice(fromIdx, toIdx + 1) // paginate const hasNextPage = (readFileContents.length - 1) - toIdx >= 1 - return { fileContents, hasNextPage } + return { result: { fileContents, hasNextPage } } }, list_dir: async ({ rootURI, pageNumber }) => { const dirResult = await computeDirectoryResult(fileService, rootURI, pageNumber) - return dirResult + return { result: dirResult } }, pathname_search: async ({ queryStr, pageNumber }) => { @@ -304,7 +303,7 @@ export class ToolsService implements IToolsService { .map(({ resource, results }) => resource) const hasNextPage = (data.results.length - 1) - toIdx >= 1 - return { uris, hasNextPage } + return { result: { uris, hasNextPage } } }, text_search: async ({ queryStr, pageNumber }) => { @@ -322,7 +321,7 @@ export class ToolsService implements IToolsService { .map(({ resource, results }) => resource) const hasNextPage = (data.results.length - 1) - toIdx >= 1 - return { queryStr, uris, hasNextPage } + return { result: { queryStr, uris, hasNextPage } } }, // --- @@ -333,12 +332,12 @@ export class ToolsService implements IToolsService { else { await fileService.createFile(uri) } - return {} + return { result: {} } }, delete_uri: async ({ uri, isRecursive }) => { await fileService.del(uri, { recursive: isRecursive }) - return {} + return { result: {} } }, edit: async ({ uri, changeDescription }) => { @@ -350,13 +349,15 @@ export class ToolsService implements IToolsService { startBehavior: 'keep-conflicts', }) if (!res) throw new Error(`The Apply model did not start running on ${basename(uri.fsPath)}. Please try again.`) - const [_, applyDonePromise] = res - await applyDonePromise - return {} + const [diffZoneURI, applyDonePromise] = res + + const cancel = () => editCodeService.interruptURIStreaming({ uri: diffZoneURI }) + + return { result: applyDonePromise, cancel } }, terminal_command: async ({ command, proposedTerminalId, waitForCompletion }) => { const { terminalId, didCreateTerminal, result, resolveReason } = await this.terminalToolService.runCommand(command, proposedTerminalId, waitForCompletion) - return { terminalId, didCreateTerminal, result, resolveReason } + return { result: { terminalId, didCreateTerminal, result, resolveReason } } }, } diff --git a/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts b/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts index 091d87de..4b3b8645 100644 --- a/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts +++ b/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts @@ -134,7 +134,6 @@ export class VoidCommandBarService extends Disposable implements IVoidCommandBar if (e.uri.fsPath !== uri.fsPath) continue // --- sortedURIs: delete if empty, add if not in state yet const diffZones = this._getDiffZonesOnURI(uri) - console.log('addordelete diffzone', uri.fsPath, diffZones) if (diffZones.length === 0) { this._deleteURIEntryFromState(uri) this._onDidChangeState.fire({ uri }) From 69697542bac0f94142313bff3130d507ecdb7f04 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 20 Mar 2025 00:46:10 -0700 Subject: [PATCH 128/173] misc fixes --- .../react/src/void-command-bar-tsx/VoidCommandBar.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx index 9cc33081..36d13173 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx @@ -85,7 +85,7 @@ const VoidCommandBar = ({ uri, editor }: { uri: URI | null, editor: ICodeEditor if (!s) return const { sortedDiffIds } = s // reveal - if (idx) { + if (idx !== null) { const diffid = sortedDiffIds[idx] const diff = editCodeService.diffOfId[diffid] const range = { startLineNumber: diff.startLine, endLineNumber: diff.startLine, startColumn: 1, endColumn: 1 }; @@ -103,7 +103,10 @@ const VoidCommandBar = ({ uri, editor }: { uri: URI | null, editor: ICodeEditor const nextURI = sortedCommandBarURIs[idx] editCodeService.diffAreasOfURI const { model } = await voidModelService.getModelSafe(nextURI) - if (model) { editor.setModel(model) } // switch to the URI + if (model) { + // switch to the URI + editorService.openCodeEditor({ resource: nextURI, options: { revealIfVisible: true } }, editor) + } } From 1326f19a5e6308677a9cced441fd31ca2fda36b3 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 20 Mar 2025 02:37:04 -0700 Subject: [PATCH 129/173] add openrouter claude thinking --- .../react/src/sidebar-tsx/SidebarChat.tsx | 19 +- .../void-command-bar-tsx/VoidCommandBar.tsx | 6 +- .../contrib/void/common/modelCapabilities.ts | 181 +++++++++++++----- .../llmMessage/sendLLMMessage.impl.ts | 74 +++---- 4 files changed, 187 insertions(+), 93 deletions(-) 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 bac48dda..b6090aba 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 @@ -24,13 +24,11 @@ import { VOID_CTRL_L_ACTION_ID } from '../../../actionIDs.js'; import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js'; import { ChatMode, FeatureName, isFeatureNameDisabled } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'; import { WarningBox } from '../void-settings-tsx/WarningBox.js'; -import { getModelSelectionState, getModelCapabilities } from '../../../../common/modelCapabilities.js'; +import { getModelCapabilities, getIsResoningEnabledState } from '../../../../common/modelCapabilities.js'; import { AlertTriangle, Ban, ChevronRight, Dot, Pencil, X } from 'lucide-react'; import { ChatMessage, StagingSelectionItem, ToolMessage, ToolRequestApproval } from '../../../../common/chatThreadServiceTypes.js'; -import { ResolveReason, ToolCallParams, ToolName, ToolNameWithApproval } from '../../../../common/toolsServiceTypes.js'; +import { ToolCallParams, ToolName, ToolNameWithApproval } from '../../../../common/toolsServiceTypes.js'; import { JumpToFileButton, useApplyButtonHTML } from '../markdown/ApplyBlockHoverButtons.js'; -import { DiffZone } from '../../../editCodeService.js'; -import { ScrollType } from '../../../../../../../editor/common/editorCommon.js'; import { IsRunningType } from '../../../chatThreadService.js'; @@ -160,11 +158,12 @@ const ReasoningOptionSlider = ({ featureName }: { featureName: FeatureName }) => if (!modelSelection) return null const { modelName, providerName } = modelSelection - const { canToggleReasoning, reasoningBudgetSlider } = getModelCapabilities(providerName, modelName).supportsReasoning || {} + const { reasoningCapabilities } = getModelCapabilities(providerName, modelName) + const { canTurnOffReasoning, reasoningBudgetSlider } = reasoningCapabilities || {} - const { isReasoningEnabled } = getModelSelectionState(providerName, modelName, voidSettingsState.optionsOfModelSelection[providerName]?.[modelName]) - - if (canToggleReasoning && !reasoningBudgetSlider) { // if it's just a on/off toggle without a power slider (no models right now) + const modelSelectionOptions = voidSettingsState.optionsOfModelSelection[providerName]?.[modelName] + const isReasoningEnabled = getIsResoningEnabledState(providerName, modelName, modelSelectionOptions) + if (canTurnOffReasoning && !reasoningBudgetSlider) { // if it's just a on/off toggle without a power slider (no models right now) return null // unused right now // return
    // {isReasoningEnabled ? 'Thinking' : 'Thinking'} @@ -183,7 +182,7 @@ const ReasoningOptionSlider = ({ featureName }: { featureName: FeatureName }) => const nSteps = 8 // only used in calculating stepSize, stepSize is what actually matters const stepSize = Math.round((max - min_) / nSteps) - const min = canToggleReasoning ? min_ - stepSize : min_ + const min = canTurnOffReasoning ? min_ - stepSize : min_ return
    Thinking @@ -195,7 +194,7 @@ const ReasoningOptionSlider = ({ featureName }: { featureName: FeatureName }) => step={stepSize} value={value} onChange={(newVal) => { - const disabled = newVal === min && canToggleReasoning + const disabled = newVal === min && canTurnOffReasoning voidSettingsService.setOptionsOfModelSelection(modelSelection.providerName, modelSelection.modelName, { reasoningEnabled: !disabled, reasoningBudget: newVal }) }} /> diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx index 36d13173..be9fb5d2 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx @@ -185,10 +185,12 @@ const VoidCommandBar = ({ uri, editor }: { uri: URI | null, editor: ICodeEditor
    - File {(currUriIdx ?? 0) + 1} of {sortedCommandBarURIs.length} + {`File ${(currUriIdx ?? 0) + 1} of ${sortedCommandBarURIs.length}`}
    - Diff {(currDiffIdx ?? 0) + 1} of {sortedDiffIds?.length ?? 0} + {sortedDiffIds?.length ?? 0 === 0 ? + '(No changes)' + : `Diff ${(currDiffIdx ?? 0) + 1} of ${sortedDiffIds?.length ?? 0}`}
    diff --git a/src/vs/workbench/contrib/void/common/modelCapabilities.ts b/src/vs/workbench/contrib/void/common/modelCapabilities.ts index 95de9604..88a45939 100644 --- a/src/vs/workbench/contrib/void/common/modelCapabilities.ts +++ b/src/vs/workbench/contrib/void/common/modelCapabilities.ts @@ -40,6 +40,8 @@ export const defaultModelsOfProvider = { vLLM: [ // autodetected ], openRouter: [ // https://openrouter.ai/models + 'anthropic/claude-3.7-sonnet:thinking', + 'anthropic/claude-3.7-sonnet', 'anthropic/claude-3.5-sonnet', 'deepseek/deepseek-r1', 'mistralai/codestral-2501', @@ -79,10 +81,11 @@ type ModelOptions = { supportsTools: false | 'anthropic-style' | 'openai-style'; supportsFIM: boolean; - supportsReasoning: false | { + reasoningCapabilities: false | { + readonly supportsReasoning: true; // reasoning options if supports reasoning - readonly canToggleReasoning: boolean; // whether or not the user can disable reasoning mode (false if the model only supports reasoning) - readonly canIOReasoning: boolean; // whether or not the model actually outputs reasoning + readonly canTurnOffReasoning: boolean; // whether or not the user can disable reasoning mode (false if the model only supports reasoning) + readonly canIOReasoning: boolean; // whether or not the model actually outputs reasoning (eg o1 lets us control reasoning but not output it) readonly reasoningMaxOutputTokens?: number; // overrides normal maxOutputTokens // <-- UNUSED (except anthropic) readonly reasoningBudgetSlider?: { type: 'slider'; min: number; max: number; default: number }; @@ -95,7 +98,7 @@ type ModelOptions = { type ProviderReasoningIOSettings = { // include this in payload to get reasoning - input?: { includeInPayload?: { [key: string]: any }, }; + input?: { includeInPayload?: (reasoningState: SendableReasoningInfo) => null | { [key: string]: any }, }; // nameOfFieldInDelta: reasoning output is in response.choices[0].delta[deltaReasoningField] // needsManualParse: whether we must manually parse out the tags output?: @@ -118,7 +121,7 @@ const modelOptionsDefaults: ModelOptions = { supportsSystemMessage: false, supportsTools: false, supportsFIM: false, - supportsReasoning: false, + reasoningCapabilities: false, } @@ -127,70 +130,70 @@ const openSourceModelOptions_assumingOAICompat = { supportsFIM: false, supportsSystemMessage: false, supportsTools: false, - supportsReasoning: { canToggleReasoning: false, canIOReasoning: true, openSourceThinkTags: ['', ''] }, + reasoningCapabilities: { supportsReasoning: true, canTurnOffReasoning: false, canIOReasoning: true, openSourceThinkTags: ['', ''] }, }, 'deepseekCoderV2': { supportsFIM: false, supportsSystemMessage: false, // unstable supportsTools: false, - supportsReasoning: false, + reasoningCapabilities: false, }, 'codestral': { supportsFIM: true, supportsSystemMessage: 'system-role', supportsTools: 'openai-style', - supportsReasoning: false, + reasoningCapabilities: false, }, // llama 'llama3': { supportsFIM: false, supportsSystemMessage: 'system-role', supportsTools: 'openai-style', - supportsReasoning: false, + reasoningCapabilities: false, }, 'llama3.1': { supportsFIM: false, supportsSystemMessage: 'system-role', supportsTools: 'openai-style', - supportsReasoning: false, + reasoningCapabilities: false, }, 'llama3.2': { supportsFIM: false, supportsSystemMessage: 'system-role', supportsTools: 'openai-style', - supportsReasoning: false, + reasoningCapabilities: false, }, 'llama3.3': { supportsFIM: false, supportsSystemMessage: 'system-role', supportsTools: 'openai-style', - supportsReasoning: false, + reasoningCapabilities: false, }, // qwen 'qwen2.5coder': { supportsFIM: true, supportsSystemMessage: 'system-role', supportsTools: 'openai-style', - supportsReasoning: false, + reasoningCapabilities: false, }, 'qwq': { supportsFIM: false, // no FIM, yes reasoning supportsSystemMessage: 'system-role', supportsTools: 'openai-style', - supportsReasoning: { canToggleReasoning: false, canIOReasoning: true, openSourceThinkTags: ['', ''] }, + reasoningCapabilities: { supportsReasoning: true, canTurnOffReasoning: false, canIOReasoning: true, openSourceThinkTags: ['', ''] }, }, // FIM only 'starcoder2': { supportsFIM: true, supportsSystemMessage: false, supportsTools: false, - supportsReasoning: false, + reasoningCapabilities: false, }, 'codegemma:2b': { supportsFIM: true, supportsSystemMessage: false, supportsTools: false, - supportsReasoning: false, + reasoningCapabilities: false, }, } as const satisfies { [s: string]: Partial } @@ -233,8 +236,9 @@ const anthropicModelOptions = { supportsFIM: false, supportsSystemMessage: 'separated', supportsTools: 'anthropic-style', - supportsReasoning: { - canToggleReasoning: true, + reasoningCapabilities: { + supportsReasoning: true, + canTurnOffReasoning: true, canIOReasoning: true, reasoningMaxOutputTokens: 64_000, // can bump it to 128_000 with beta mode output-128k-2025-02-19 reasoningBudgetSlider: { type: 'slider', min: 1024, max: 32_000, default: 1024 }, // they recommend batching if max > 32_000 @@ -247,7 +251,7 @@ const anthropicModelOptions = { supportsFIM: false, supportsSystemMessage: 'separated', supportsTools: 'anthropic-style', - supportsReasoning: false, + reasoningCapabilities: false, }, 'claude-3-5-haiku-20241022': { contextWindow: 200_000, @@ -256,7 +260,7 @@ const anthropicModelOptions = { supportsFIM: false, supportsSystemMessage: 'separated', supportsTools: 'anthropic-style', - supportsReasoning: false, + reasoningCapabilities: false, }, 'claude-3-opus-20240229': { contextWindow: 200_000, @@ -265,7 +269,7 @@ const anthropicModelOptions = { supportsFIM: false, supportsSystemMessage: 'separated', supportsTools: 'anthropic-style', - supportsReasoning: false, + reasoningCapabilities: false, }, 'claude-3-sonnet-20240229': { // no point of using this, but including this for people who put it in contextWindow: 200_000, cost: { input: 3.00, output: 15.00 }, @@ -273,11 +277,21 @@ const anthropicModelOptions = { supportsFIM: false, supportsSystemMessage: 'separated', supportsTools: 'anthropic-style', - supportsReasoning: false, + reasoningCapabilities: false, } } as const satisfies { [s: string]: ModelOptions } const anthropicSettings: ProviderSettings = { + providerReasoningIOSettings: { + input: { + includeInPayload: (reasoningInfo) => { + if (reasoningInfo?.type === 'budgetEnabled') { + return { thinking: { type: 'enabled', budget_tokens: reasoningInfo.reasoningBudget } } + } + return null + } + }, + }, modelOptions: anthropicModelOptions, modelOptionsFallback: (modelName) => { let fallbackName: keyof typeof anthropicModelOptions | null = null @@ -288,7 +302,7 @@ const anthropicSettings: ProviderSettings = { if (modelName.includes('claude-3-sonnet')) fallbackName = 'claude-3-sonnet-20240229' if (fallbackName) return { modelName: fallbackName, ...anthropicModelOptions[fallbackName] } return { modelName, ...modelOptionsDefaults, maxOutputTokens: 4_096 } - } + }, } @@ -301,7 +315,7 @@ const openAIModelOptions = { // https://platform.openai.com/docs/pricing supportsFIM: false, supportsTools: false, supportsSystemMessage: 'developer-role', - supportsReasoning: { canIOReasoning: false, canToggleReasoning: false }, // it doesn't actually output reasoning, but our logic is fine with it + reasoningCapabilities: { supportsReasoning: true, canIOReasoning: false, canTurnOffReasoning: false }, // it doesn't actually output reasoning, but our logic is fine with it }, 'o3-mini': { contextWindow: 200_000, @@ -310,7 +324,7 @@ const openAIModelOptions = { // https://platform.openai.com/docs/pricing supportsFIM: false, supportsTools: false, supportsSystemMessage: 'developer-role', - supportsReasoning: { canIOReasoning: false, canToggleReasoning: false }, + reasoningCapabilities: { supportsReasoning: true, canIOReasoning: false, canTurnOffReasoning: false }, }, 'gpt-4o': { contextWindow: 128_000, @@ -319,7 +333,7 @@ const openAIModelOptions = { // https://platform.openai.com/docs/pricing supportsFIM: false, supportsTools: 'openai-style', supportsSystemMessage: 'system-role', - supportsReasoning: false, + reasoningCapabilities: false, }, 'o1-mini': { contextWindow: 128_000, @@ -328,7 +342,7 @@ const openAIModelOptions = { // https://platform.openai.com/docs/pricing supportsFIM: false, supportsTools: false, supportsSystemMessage: false, // does not support any system - supportsReasoning: { canIOReasoning: false, canToggleReasoning: false }, + reasoningCapabilities: { supportsReasoning: true, canIOReasoning: false, canTurnOffReasoning: false }, }, 'gpt-4o-mini': { contextWindow: 128_000, @@ -337,7 +351,7 @@ const openAIModelOptions = { // https://platform.openai.com/docs/pricing supportsFIM: false, supportsTools: 'openai-style', supportsSystemMessage: 'system-role', // ?? - supportsReasoning: false, + reasoningCapabilities: false, }, } as const satisfies { [s: string]: ModelOptions } @@ -363,7 +377,7 @@ const xAIModelOptions = { supportsFIM: false, supportsSystemMessage: 'system-role', supportsTools: 'openai-style', - supportsReasoning: false, + reasoningCapabilities: false, }, } as const satisfies { [s: string]: ModelOptions } @@ -387,7 +401,7 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing supportsFIM: false, supportsSystemMessage: 'system-role', supportsTools: 'openai-style', // we are assuming OpenAI SDK when calling gemini - supportsReasoning: false, + reasoningCapabilities: false, }, 'gemini-2.0-flash-lite-preview-02-05': { contextWindow: 1_048_576, @@ -396,7 +410,7 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing supportsFIM: false, supportsSystemMessage: 'system-role', supportsTools: 'openai-style', - supportsReasoning: false, + reasoningCapabilities: false, }, 'gemini-1.5-flash': { contextWindow: 1_048_576, @@ -405,7 +419,7 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing supportsFIM: false, supportsSystemMessage: 'system-role', supportsTools: 'openai-style', - supportsReasoning: false, + reasoningCapabilities: false, }, 'gemini-1.5-pro': { contextWindow: 2_097_152, @@ -414,7 +428,7 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing supportsFIM: false, supportsSystemMessage: 'system-role', supportsTools: 'openai-style', - supportsReasoning: false, + reasoningCapabilities: false, }, 'gemini-1.5-flash-8b': { contextWindow: 1_048_576, @@ -423,7 +437,7 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing supportsFIM: false, supportsSystemMessage: 'system-role', supportsTools: 'openai-style', - supportsReasoning: false, + reasoningCapabilities: false, }, } as const satisfies { [s: string]: ModelOptions } @@ -469,7 +483,7 @@ const groqModelOptions = { // https://console.groq.com/docs/models, https://groq supportsFIM: false, supportsSystemMessage: 'system-role', supportsTools: 'openai-style', - supportsReasoning: false, + reasoningCapabilities: false, }, 'llama-3.1-8b-instant': { contextWindow: 128_000, @@ -478,7 +492,7 @@ const groqModelOptions = { // https://console.groq.com/docs/models, https://groq supportsFIM: false, supportsSystemMessage: 'system-role', supportsTools: 'openai-style', - supportsReasoning: false, + reasoningCapabilities: false, }, 'qwen-2.5-coder-32b': { contextWindow: 128_000, @@ -487,7 +501,7 @@ const groqModelOptions = { // https://console.groq.com/docs/models, https://groq supportsFIM: false, // unfortunately looks like no FIM support on groq supportsSystemMessage: 'system-role', supportsTools: 'openai-style', - supportsReasoning: false, + reasoningCapabilities: false, }, 'qwen-qwq-32b': { // https://huggingface.co/Qwen/QwQ-32B contextWindow: 128_000, @@ -496,11 +510,21 @@ const groqModelOptions = { // https://console.groq.com/docs/models, https://groq supportsFIM: false, supportsSystemMessage: 'system-role', supportsTools: 'openai-style', - supportsReasoning: { canIOReasoning: true, canToggleReasoning: false, openSourceThinkTags: ['', ''] }, // we're using reasoning_format:parsed so really don't need to know openSourceThinkTags + reasoningCapabilities: { supportsReasoning: true, canIOReasoning: true, canTurnOffReasoning: false, openSourceThinkTags: ['', ''] }, // we're using reasoning_format:parsed so really don't need to know openSourceThinkTags }, } as const satisfies { [s: string]: ModelOptions } const groqSettings: ProviderSettings = { - providerReasoningIOSettings: { input: { includeInPayload: { reasoning_format: 'parsed' } }, output: { nameOfFieldInDelta: 'reasoning' }, }, // Must be set to either parsed or hidden when using tool calling https://console.groq.com/docs/reasoning + providerReasoningIOSettings: { + input: { + includeInPayload: (reasoningInfo) => { + if (reasoningInfo?.type === 'budgetEnabled') { + return { reasoning_format: 'parsed' } + } + return null + } + }, + output: { nameOfFieldInDelta: 'reasoning' }, + }, // Must be set to either parsed or hidden when using tool calling https://console.groq.com/docs/reasoning modelOptions: groqModelOptions, modelOptionsFallback: (modelName) => { return null } } @@ -536,6 +560,21 @@ const openRouterModelOptions_assumingOpenAICompat = { maxOutputTokens: null, cost: { input: 0.8, output: 2.4 }, }, + 'anthropic/claude-3.7-sonnet:thinking': { + contextWindow: 200_000, + maxOutputTokens: null, + cost: { input: 3.00, output: 15.00 }, + supportsFIM: false, + supportsSystemMessage: 'system-role', + supportsTools: 'openai-style', + reasoningCapabilities: { // same as anthropic, see above + supportsReasoning: true, + canTurnOffReasoning: false, + canIOReasoning: true, + reasoningMaxOutputTokens: 64_000, + reasoningBudgetSlider: { type: 'slider', min: 1024, max: 32_000, default: 1024 }, // they recommend batching if max > 32_000 + }, + }, 'anthropic/claude-3.7-sonnet': { contextWindow: 200_000, maxOutputTokens: null, @@ -543,7 +582,7 @@ const openRouterModelOptions_assumingOpenAICompat = { supportsFIM: false, supportsSystemMessage: 'system-role', supportsTools: 'openai-style', - supportsReasoning: { canIOReasoning: true, canToggleReasoning: false }, // TODO!!! false for now + reasoningCapabilities: false, // stupidly, openrouter separates thinking from non-thinking }, 'anthropic/claude-3.5-sonnet': { contextWindow: 200_000, @@ -552,7 +591,7 @@ const openRouterModelOptions_assumingOpenAICompat = { supportsFIM: false, supportsSystemMessage: 'system-role', supportsTools: 'openai-style', - supportsReasoning: false, + reasoningCapabilities: false, }, 'mistralai/codestral-2501': { ...openSourceModelOptions_assumingOAICompat.codestral, @@ -560,7 +599,7 @@ const openRouterModelOptions_assumingOpenAICompat = { maxOutputTokens: null, cost: { input: 0.3, output: 0.9 }, supportsTools: 'openai-style', - supportsReasoning: false, + reasoningCapabilities: false, }, 'qwen/qwen-2.5-coder-32b-instruct': { ...openSourceModelOptions_assumingOAICompat['qwen2.5coder'], @@ -581,7 +620,18 @@ const openRouterModelOptions_assumingOpenAICompat = { const openRouterSettings: ProviderSettings = { // reasoning: OAICompat + response.choices[0].delta.reasoning : payload should have {include_reasoning: true} https://openrouter.ai/announcements/reasoning-tokens-for-thinking-models providerReasoningIOSettings: { - input: { includeInPayload: { include_reasoning: true } }, + input: { + includeInPayload: (reasoningInfo) => { + if (reasoningInfo?.type === 'budgetEnabled') { + return { + reasoning: { + max_tokens: reasoningInfo.reasoningBudget + } + } + } + return null + } + }, output: { nameOfFieldInDelta: 'reasoning' }, }, modelOptions: openRouterModelOptions_assumingOpenAICompat, @@ -632,12 +682,45 @@ export const getProviderCapabilities = (providerName: ProviderName) => { return { providerReasoningIOSettings } } -// state from optionsOfModelSelection -export const getModelSelectionState = (providerName: ProviderName, modelName: string, modelSelectionOptions: ModelSelectionOptions | undefined): { isReasoningEnabled: boolean, reasoningBudget: number | undefined } => { - const { canToggleReasoning, reasoningBudgetSlider } = getModelCapabilities(providerName, modelName).supportsReasoning || {} - const defaultEnabledVal = canToggleReasoning ? true : false +export type SendableReasoningInfo = { + type: 'budgetEnabled', + isReasoningEnabled: true, + reasoningBudget: number, +} | null + + + +export const getIsResoningEnabledState = ( + providerName: ProviderName, + modelName: string, + modelSelectionOptions: ModelSelectionOptions | undefined, +) => { + const { supportsReasoning } = getModelCapabilities(providerName, modelName).reasoningCapabilities || {} + if (!supportsReasoning) return false + + const defaultEnabledVal = true // if can't toggle reasoning, then this must be true. just true as default const isReasoningEnabled = modelSelectionOptions?.reasoningEnabled ?? defaultEnabledVal - const reasoningBudget = reasoningBudgetSlider?.type === 'slider' ? modelSelectionOptions?.reasoningBudget ?? reasoningBudgetSlider?.default : undefined - return { isReasoningEnabled, reasoningBudget } + return isReasoningEnabled +} + + +// used to force reasoning state (complex) into something simple we can just read from when sending a message +export const getSendableReasoningInfo = ( + providerName: ProviderName, + modelName: string, + modelSelectionOptions: ModelSelectionOptions | undefined, +): SendableReasoningInfo => { + + const { canIOReasoning, reasoningBudgetSlider } = getModelCapabilities(providerName, modelName).reasoningCapabilities || {} + if (!canIOReasoning) return null + const isReasoningEnabled = getIsResoningEnabledState(providerName, modelName, modelSelectionOptions) + if (!isReasoningEnabled) return null + + // check for reasoning budget + const reasoningBudget = reasoningBudgetSlider?.type === 'slider' ? modelSelectionOptions?.reasoningBudget ?? reasoningBudgetSlider?.default : undefined + if (reasoningBudget) { + return { type: 'budgetEnabled', isReasoningEnabled: isReasoningEnabled, reasoningBudget: reasoningBudget } + } + return null } 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 a34535bf..2b23c0cb 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 @@ -11,7 +11,7 @@ import { extractReasoningOnFinalMessage, extractReasoningOnTextWrapper } from '. import { LLMChatMessage, LLMFIMMessage, ModelListParams, OllamaModelResponse, OnError, OnFinalMessage, OnText } from '../../common/sendLLMMessageTypes.js'; import { defaultProviderSettings, displayInfoOfProviderName, ModelSelectionOptions, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js'; import { prepareFIMMessage, prepareMessages } from './preprocessLLMMessages.js'; -import { getModelSelectionState, getModelCapabilities, getProviderCapabilities } from '../../common/modelCapabilities.js'; +import { getSendableReasoningInfo, getModelCapabilities, getProviderCapabilities } from '../../common/modelCapabilities.js'; import { InternalToolInfo, ToolName, isAToolName } from '../../common/toolsServiceTypes.js'; @@ -150,32 +150,41 @@ const _sendOpenAICompatibleFIM = ({ messages: messages_, onFinalMessage, onError -const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, aiInstructions, tools: tools_ }: SendChatParams_Internal) => { +const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, providerName, aiInstructions, tools: tools_ }: SendChatParams_Internal) => { const { modelName, - supportsReasoning, supportsSystemMessage, supportsTools, - // maxOutputTokens, right now we are ignoring this + // maxOutputTokens, + reasoningCapabilities, } = getModelCapabilities(providerName, modelName_) - const { - canIOReasoning, - openSourceThinkTags, - } = supportsReasoning || {} - - const { providerReasoningIOSettings } = getProviderCapabilities(providerName) - const { messages } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsTools, supportsAnthropicReasoningSignature: false }) + // reasoning + const { canIOReasoning, openSourceThinkTags, } = reasoningCapabilities || {} + const reasoningInfo = getSendableReasoningInfo(providerName, modelName_, modelSelectionOptions) // user's modelName_ here + const includeInPayload = providerReasoningIOSettings?.input?.includeInPayload?.(reasoningInfo) || {} + + // tools const tools = (supportsTools && ((tools_?.length ?? 0) !== 0)) ? tools_?.map(tool => toOpenAICompatibleTool(tool)) : undefined - - const includeInPayload = canIOReasoning ? providerReasoningIOSettings?.input?.includeInPayload || {} : {} - const toolsObj = tools ? { tools: tools, tool_choice: 'auto', parallel_tool_calls: false, } as const : {} - const openai: OpenAI = newOpenAICompatibleSDK({ providerName, settingsOfProvider, includeInPayload }) - const options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = { model: modelName, messages: messages, stream: true, ...toolsObj, } + // max tokens + // const maxTokens = reasoningInfo?.isReasoningEnabled && reasoningCapabilities ? reasoningCapabilities.reasoningMaxOutputTokens : maxOutputTokens + + // instance + const { messages } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsTools, supportsAnthropicReasoningSignature: false }) + const openai: OpenAI = newOpenAICompatibleSDK({ providerName, settingsOfProvider, includeInPayload }) + const options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = { + model: modelName, + messages: messages, + stream: true, + // max_completion_tokens: maxTokens, + ...toolsObj, + } + + // open source models - manually parse think tokens const { needsManualParse: needsManualReasoningParse, nameOfFieldInDelta: nameOfReasoningFieldInDelta } = providerReasoningIOSettings?.output ?? {} const manuallyParseReasoning = needsManualReasoningParse && canIOReasoning && openSourceThinkTags if (manuallyParseReasoning) { @@ -300,31 +309,32 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM supportsSystemMessage, supportsTools, maxOutputTokens, - supportsReasoning, + reasoningCapabilities, } = getModelCapabilities(providerName, modelName_) - const { - isReasoningEnabled, - reasoningBudget, - } = getModelSelectionState(providerName, modelName_, modelSelectionOptions) // user's modelName_ here - - const { messages, separateSystemMessageStr } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsTools, supportsAnthropicReasoningSignature: true }) const thisConfig = settingsOfProvider.anthropic - const anthropic = new Anthropic({ apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true }); + const { providerReasoningIOSettings } = getProviderCapabilities(providerName) + + // reasoning + const reasoningInfo = getSendableReasoningInfo(providerName, modelName_, modelSelectionOptions) // user's modelName_ here + const includeInPayload = providerReasoningIOSettings?.input?.includeInPayload?.(reasoningInfo) || {} + + // tools const tools = ((tools_?.length ?? 0) !== 0) ? tools_?.map(tool => toAnthropicTool(tool)) : undefined - - const toolsObj: Partial = tools ? { tools: tools, tool_choice: { type: 'auto', disable_parallel_tool_use: true } // one tool at a time } : {} + // anthropic-specific - max tokens + const maxTokens = reasoningInfo?.isReasoningEnabled && reasoningCapabilities ? reasoningCapabilities.reasoningMaxOutputTokens : maxOutputTokens - const enableThinking = supportsReasoning && isReasoningEnabled && reasoningBudget - const maxTokens = enableThinking ? supportsReasoning.reasoningMaxOutputTokens : maxOutputTokens - const thinkingObj: Partial = enableThinking ? { - thinking: { type: 'enabled', budget_tokens: reasoningBudget } // thinking enabled - } : {} + // instance + const { messages, separateSystemMessageStr } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsTools, supportsAnthropicReasoningSignature: true }) + const anthropic = new Anthropic({ + apiKey: thisConfig.apiKey, + dangerouslyAllowBrowser: true + }); const stream = anthropic.messages.stream({ system: separateSystemMessageStr, @@ -332,7 +342,7 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM model: modelName, max_tokens: maxTokens ?? 4_096, // anthropic requires this ...toolsObj, - ...thinkingObj, + ...includeInPayload, }) // when receive text From 8dab3ac8546e3ad2dfd4c07b9b1907e06fa9075d Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 20 Mar 2025 02:52:44 -0700 Subject: [PATCH 130/173] tool error styles --- .../react/src/sidebar-tsx/SidebarChat.tsx | 51 ++++++++++++++++--- 1 file changed, 44 insertions(+), 7 deletions(-) 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 b6090aba..637de4cd 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 @@ -1252,6 +1252,14 @@ export const ToolChildrenWrapper = ({ children, className }: { children: React.R
    } +export const ErrorChildren = ({ children }: { children: React.ReactNode }) => { + return
    +
    + {children} +
    +
    +} + export const ListableToolItem = ({ name, onClick, isSmall, className, showDot }: { name: React.ReactNode, onClick?: () => void, isSmall?: boolean, className?: string, showDot?: boolean }) => { return
    } = { else { const { value, params } = toolMessage.result if (params) componentParams.desc2 = - componentParams.children = {value} + componentParams.children = + + {value} + + } return @@ -1361,7 +1373,11 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { } else { const { value, params } = toolMessage.result - componentParams.children = {value} + componentParams.children = + + {value} + + } return @@ -1400,7 +1416,11 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { } else { const { value, params } = toolMessage.result - componentParams.children = {value} + componentParams.children = + + {value} + + } return @@ -1439,7 +1459,11 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { } else { const { value, params } = toolMessage.result - componentParams.children = {value} + componentParams.children = + + {value} + + } return } @@ -1485,7 +1509,11 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { else if (toolMessage.result.type === 'error') { const { params, value } = toolMessage.result if (params) { componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } } - componentParams.children = {value} + componentParams.children = componentParams.children = + + {value} + + } return @@ -1531,7 +1559,11 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { else if (toolMessage.result.type === 'error') { const { params, value } = toolMessage.result if (params) { componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } } - componentParams.children = {value} + componentParams.children = componentParams.children = + + {value} + + } return @@ -1601,7 +1633,12 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { const { params, value } = toolMessage.result if (params) { componentParams.children = - {value} + {/* error */} + + {value} + + + {/* content */} Date: Thu, 20 Mar 2025 04:03:37 -0700 Subject: [PATCH 131/173] misc bug fixes with oai-compat and agent --- .../contrib/void/browser/chatThreadService.ts | 14 ++++----- .../contrib/void/browser/editCodeService.ts | 4 +-- .../react/src/sidebar-tsx/SidebarChat.tsx | 31 ++++++++++++------- .../void-command-bar-tsx/VoidCommandBar.tsx | 1 - .../contrib/void/browser/toolsService.ts | 2 +- .../contrib/void/common/prompt/prompts.ts | 8 ++--- .../llmMessage/sendLLMMessage.impl.ts | 2 +- 7 files changed, 35 insertions(+), 27 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 8772dc23..cb3ce39b 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -761,7 +761,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { const handleToolCall = async ( tool: ToolCallType, opts?: { preapproved: true, toolParams: ToolCallParams[ToolName] }, - ): Promise<{ awaitingUserApproval: boolean, canceled: boolean }> => { + ): Promise<{ awaitingUserApproval?: boolean, canceled?: boolean }> => { const toolName: ToolName = tool.name const toolParamsStr = tool.paramsStr const toolId = tool.id @@ -779,14 +779,14 @@ class ChatThreadService extends Disposable implements IChatThreadService { } catch (error) { const errorMessage = getErrorMessage(error) this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: toolParamsStr, id: toolId, content: errorMessage, result: { type: 'error', params: undefined, value: errorMessage }, }) - return { awaitingUserApproval: false, canceled: false } + return {} } // 2. if tool requires approval, break from the loop, awaiting approval const requiresApproval = !this._settingsService.state.globalSettings.autoApprove if (requiresApproval && toolNamesThatRequireApproval.has(toolName)) { this._addMessageToThread(threadId, { role: 'tool_request', name: toolName, paramsStr: toolParamsStr, params: toolParams, id: toolId }) - return { awaitingUserApproval: true, canceled: false } + return { awaitingUserApproval: true } } } else { @@ -805,10 +805,10 @@ class ChatThreadService extends Disposable implements IChatThreadService { toolResult = await Promise.race([result, resolveIfCancel]) // this await is needed, typescript is bad... } catch (error) { - if (canceled) return { awaitingUserApproval: false, canceled: true } + if (canceled) return { canceled: true } const errorMessage = getErrorMessage(error) this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: toolParamsStr, id: toolId, content: errorMessage, result: { type: 'error', params: toolParams, value: errorMessage }, }) - return { awaitingUserApproval: true, canceled: false } + return {} } // 4. stringify the result to give to the LLM @@ -817,12 +817,12 @@ class ChatThreadService extends Disposable implements IChatThreadService { } catch (error) { const errorMessage = this.errMsgs.errWhenStringifying(error) this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: toolParamsStr, id: toolId, content: errorMessage, result: { type: 'error', params: toolParams, value: errorMessage }, }) - return { awaitingUserApproval: false, canceled: false } + return {} } // 5. add to history and keep going this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: toolParamsStr, id: toolId, content: toolResultStr, result: { type: 'success', params: toolParams, value: toolResult }, }) - return { awaitingUserApproval: false, canceled: false } + return {} }; // above just defines helpers, below starts the actual function diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 7d3b20f9..41e0e1ea 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -1244,11 +1244,11 @@ class EditCodeService extends Disposable implements IEditCodeService { // ctrlkzone should never have any conflicts } else { - // keep conflict on whole file - to keep conflict, revert the change and use those contents as original, then un-revert the change + // keep conflict on whole file - to keep conflict, revert the change and use those contents as original, then un-revert the file const currentFileStr = originalFileStr this.acceptOrRejectAllDiffAreas({ uri, removeCtrlKs: true, behavior: 'reject', _addToHistory: false }) const oldFileStr = model.getValue(EndOfLinePreference.LF) // use this as original code - this._writeURIText(uri, currentFileStr, 'wholeFileRange', { shouldRealignDiffAreas: false }) // un-revert + this._writeURIText(uri, currentFileStr, 'wholeFileRange', { shouldRealignDiffAreas: true }) // un-revert originalCode = oldFileStr } 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 637de4cd..10a9a6dc 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 @@ -224,6 +224,7 @@ const ChatModeDropdown = ({ className }: { className: string }) => { const accessor = useAccessor() const voidSettingsService = accessor.get('IVoidSettingsService') + const settingsState = useSettingsState() const options: ChatMode[] = useMemo(() => ['normal', 'gather', 'agent'], []) @@ -234,7 +235,7 @@ const ChatModeDropdown = ({ className }: { className: string }) => { return nameOfChatMode[val]} getOptionDropdownName={(val) => nameOfChatMode[val]} @@ -674,7 +675,7 @@ export const SelectedFiles = ( type ToolHeaderParams = { icon?: React.ReactNode; - title: string; + title: React.ReactNode; desc1: React.ReactNode; desc2?: React.ReactNode; isError?: boolean; @@ -1109,24 +1110,30 @@ 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) => { + return + {item} + + +} const folderFileStr = (isFolder: boolean) => isFolder ? 'folder' : 'file' const toolNameToTitle = { - 'read_file': { done: 'Read file', proposed: 'Read file', running: 'Reading file...' }, - 'list_dir': { done: 'Inspected folder', proposed: 'Inspect folder', running: 'Inspecting folder...' }, - 'pathname_search': { done: 'Searched by file name', proposed: 'Search by file name', running: 'Searching by file name...' }, - 'text_search': { done: 'Searched', proposed: 'Search text', running: 'Searching...' }, + 'read_file': { done: 'Read file', proposed: 'Read file', running: loadingTitleWrapper('Reading file') }, + 'list_dir': { done: 'Inspected folder', proposed: 'Inspect folder', running: loadingTitleWrapper('Inspecting folder') }, + 'pathname_search': { done: 'Searched by file name', proposed: 'Search by file name', running: loadingTitleWrapper('Searching by file name') }, + 'text_search': { done: 'Searched', proposed: 'Search text', running: loadingTitleWrapper('Searching') }, 'create_uri': { done: (isFolder: boolean) => `Created ${folderFileStr(isFolder)}`, proposed: (isFolder: boolean) => `Create ${folderFileStr(isFolder)}`, - running: (isFolder: boolean) => `Creating ${folderFileStr(isFolder)}...` + running: (isFolder: boolean) => loadingTitleWrapper(`Creating ${folderFileStr(isFolder)}`) }, 'delete_uri': { done: (isFolder: boolean) => `Deleted ${folderFileStr(isFolder)}`, proposed: (isFolder: boolean) => `Delete ${folderFileStr(isFolder)}`, - running: (isFolder: boolean) => `Deleting ${folderFileStr(isFolder)}...` + running: (isFolder: boolean) => loadingTitleWrapper(`Deleting ${folderFileStr(isFolder)}`) }, - 'edit': { done: 'Edited file', proposed: 'Edit file', running: 'Editing file...' }, - 'terminal_command': { done: 'Ran terminal command', proposed: 'Run terminal command', running: 'Running terminal command...' } + 'edit': { done: `Edited file`, proposed: 'Edit file', running: loadingTitleWrapper('Editing file') }, + 'terminal_command': { done: `Ran terminal command`, proposed: 'Run terminal command', running: loadingTitleWrapper('Running terminal command') } } as const satisfies Record @@ -1778,7 +1785,9 @@ const ChatBubble = ({ chatMessage, isCommitted, messageIdx, isLast, chatIsRunnin } else if (role === 'tool') { const ToolResultWrapper = toolNameToComponent[chatMessage.name].resultWrapper as ResultWrapper - return + if (ToolResultWrapper) + return + return null } } diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx index be9fb5d2..58e1f47d 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx @@ -21,7 +21,6 @@ export type VoidCommandBarProps = { export const VoidCommandBarMain = ({ uri, editor }: VoidCommandBarProps) => { const isDark = useIsDark() - console.log('VoidCommandBarMain', uri?.fsPath) return
    diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index c43fe5af..629436a7 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -78,7 +78,7 @@ const directoryResultToString = (params: ToolCallParams['list_dir'], result: Too const entries = result.children; if (!result.hasPrevPage) { // is first page - output += `${params.rootURI}\n`; + output += `${params.rootURI.fsPath}\n`; } for (let i = 0; i < entries.length; i++) { diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index 51fb3703..ffedfe24 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -16,10 +16,10 @@ import { EndOfLinePreference } from '../../../../../editor/common/model.js'; export const tripleTick = ['```', '```'] export const editToolDesc_toolDescription = `\ -A high level description of the change you'd like to make in the file. This description will be handed to a dumber, faster model that will quickly apply the change.\ -Make sure to include all necessary information to make the change in this description, since it is the only context given to the fast-apply model that will actually write the change.\ -The best description you can give is a single code block of the form:\n${tripleTick[0]}\n// ... existing code ...\n{{change 1}}\n// ... existing code ...\n{{change2}}\n// ... existing code ...\n{{change 3}}\n...\n${tripleTick[1]}. \ -Wrap all code in triple backticks. Do NOT output the whole file here if possible, and try to write as LITTLE code as needed to describe the change.` +A high level description of the change you'd like to make in the file. \ +Your output should be of the form:\n${tripleTick[0]}\n// ... existing code ...\n{{change 1}}\n// ... existing code ...\n{{change2}}\n// ... existing code ...\n{{change 3}}\n...\n${tripleTick[1]}. \ +(wrap your output in triple backticks). Do NOT output the whole file here, and write as little as possible. If a change seems big, break it up into smaller edits. \ +Your description will be handed to a dumber, faster model that will quickly apply the change, so try to be brief, but also make sure to include enough information to accurately describe the change.` 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 2b23c0cb..27e99357 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 @@ -206,7 +206,7 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage if (!toolCallOfIndex[index]) toolCallOfIndex[index] = { name: '', paramsStr: '', id: '' } toolCallOfIndex[index].name += tool.function?.name ?? '' toolCallOfIndex[index].paramsStr += tool.function?.arguments ?? ''; - toolCallOfIndex[index].id = tool.id ?? '' + toolCallOfIndex[index].id += tool.id ?? '' } // message const newText = chunk.choices[0]?.delta?.content ?? '' From cc4115c8c42b30ee1bb50660703010db4db1cb89 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 20 Mar 2025 04:19:33 -0700 Subject: [PATCH 132/173] misc fixes --- .../contrib/void/browser/editCodeService.ts | 3 +-- .../react/src/markdown/ChatMarkdownRender.tsx | 13 ++++--------- .../workbench/contrib/void/common/prompt/prompts.ts | 8 ++++---- .../contrib/void/common/toolsServiceTypes.ts | 2 +- 4 files changed, 10 insertions(+), 16 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 41e0e1ea..9b7580ab 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -1285,7 +1285,6 @@ class EditCodeService extends Disposable implements IEditCodeService { } - console.log('DONE WITH _STARTSTREAMING', diffZone) return { diffZone, onFinishEdit } } @@ -1773,7 +1772,7 @@ class EditCodeService extends Disposable implements IEditCodeService { onFinalMessage: async (params) => { const { fullText } = params - console.log('DONE!', fullText) + console.log('DONE - editCode!', fullText) // 1. wait 500ms and fix lint errors - call lint error workflow // (update react state to say "Fixing errors") diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index 8a6d97ac..2507f796 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -301,18 +301,13 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, .. if (inPTag) return {contents} - - return

    - {contents} -

    + return

    {contents}

    } if (t.type === "html") { - return ( -

    - {t.raw} -

    - ) + const contents = t.raw + if (inPTag) return {contents} + return

    {contents}

    } if (t.type === "text" || t.type === "escape") { diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index ffedfe24..d5042dc9 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -16,10 +16,10 @@ import { EndOfLinePreference } from '../../../../../editor/common/model.js'; export const tripleTick = ['```', '```'] export const editToolDesc_toolDescription = `\ -A high level description of the change you'd like to make in the file. \ -Your output should be of the form:\n${tripleTick[0]}\n// ... existing code ...\n{{change 1}}\n// ... existing code ...\n{{change2}}\n// ... existing code ...\n{{change 3}}\n...\n${tripleTick[1]}. \ -(wrap your output in triple backticks). Do NOT output the whole file here, and write as little as possible. If a change seems big, break it up into smaller edits. \ -Your description will be handed to a dumber, faster model that will quickly apply the change, so try to be brief, but also make sure to include enough information to accurately describe the change.` +Your description should be of the form:\n${tripleTick[0]}\n// ... existing code ...\n{{change 1}}\n// ... existing code ...\n{{change2}}\n// ... existing code ...\n{{change 3}}\n...\n${tripleTick[1]}. \ +Do NOT re-write the whole file, and instead use comments like // ... existing code ... . Write as little as possible. \ +Your description will be handed to a dumber, faster model that will quickly apply the change, so try to be brief, but also make sure to include enough information to accurately describe the change. \ +Include the triple ticks in your output.` diff --git a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts index ce8b2b95..a4dbabbf 100644 --- a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts @@ -102,7 +102,7 @@ export const voidTools = { edit: { // APPLY TOOL name: 'edit', - description: `Edits the contents of a file at the given URI. Fails gracefully if the file does not exist.`, + description: `Edits the contents of a file, given the file's URI and a description. Fails gracefully if the file does not exist.`, params: { uri: { type: 'string', description: undefined }, changeDescription: { type: 'string', description: editToolDesc_toolDescription } // long description here From 5d825ad9707d6c2e7d22a0effe506fabc8b78996 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Thu, 20 Mar 2025 04:41:10 -0700 Subject: [PATCH 133/173] styles better --- .../void-command-bar-tsx/VoidCommandBar.tsx | 202 ++++++++++-------- 1 file changed, 115 insertions(+), 87 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx index 58e1f47d..5e3fd2fd 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx @@ -64,9 +64,6 @@ const VoidCommandBar = ({ uri, editor }: { uri: URI | null, editor: ICodeEditor } }, [sortedCommandBarURIs, uri]) - // just for style - const [isFocused, setIsFocused] = useState(false) - const getNextDiffIdx = (step: 1 | -1) => { // check undefined if (!uri) return null @@ -136,63 +133,88 @@ const VoidCommandBar = ({ uri, editor }: { uri: URI | null, editor: ICodeEditor // if there are *any* changes at all const navPanel = anyUriHasChanges &&
    setIsFocused(true)} - onBlur={() => setIsFocused(false)} + className="flex items-center gap-1" > -
    - + - + - + - -
    +
    -
    - {`File ${(currUriIdx ?? 0) + 1} of ${sortedCommandBarURIs.length}`} -
    -
    - {sortedDiffIds?.length ?? 0 === 0 ? - '(No changes)' - : `Diff ${(currDiffIdx ?? 0) + 1} of ${sortedDiffIds?.length ?? 0}`} -
    + {currUriIdx !== null && sortedCommandBarURIs.length ? +
    + File {currUriIdx + 1} of {sortedCommandBarURIs.length} +
    + : '(No changes)' + } + {currDiffIdx !== null && sortedDiffIds?.length &&
    + Diff {currDiffIdx + 1} of {sortedDiffIds?.length ?? 0} +
    }
    -
    +
    // accept/reject if current URI has changes @@ -207,42 +229,48 @@ const VoidCommandBar = ({ uri, editor }: { uri: URI | null, editor: ICodeEditor metricsService.capture('Reject All', {}) } - const acceptRejectButtons = currUriHasChanges &&
    - - -
    + + const acceptAllButton = - return
    + const rejectAllButton = + + + return
    + {currUriHasChanges && <> +
    + {acceptAllButton} + {rejectAllButton} +
    + } {navPanel} - {acceptRejectButtons}
    } From 794c4593e6e8dbb9d9725a9a6128b145761e76a6 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Thu, 20 Mar 2025 04:44:03 -0700 Subject: [PATCH 134/173] styles --- .../react/src/void-command-bar-tsx/VoidCommandBar.tsx | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx index 5e3fd2fd..6df78f20 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx @@ -204,12 +204,9 @@ const VoidCommandBar = ({ uri, editor }: { uri: URI | null, editor: ICodeEditor >→
    - {currUriIdx !== null && sortedCommandBarURIs.length ? -
    - File {currUriIdx + 1} of {sortedCommandBarURIs.length} -
    - : '(No changes)' - } + {currUriIdx !== null && sortedCommandBarURIs.length &&
    + File {currUriIdx + 1} of {sortedCommandBarURIs.length} +
    } {currDiffIdx !== null && sortedDiffIds?.length &&
    Diff {currDiffIdx + 1} of {sortedDiffIds?.length ?? 0}
    } From dafc509fa10a97fce856beb91459db4fd7d86d95 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 20 Mar 2025 05:55:15 -0700 Subject: [PATCH 135/173] always add tool_request --- .../contrib/void/browser/chatThreadService.ts | 10 +++++++--- .../contrib/void/browser/editCodeService.ts | 14 ++++++++++---- .../browser/react/src/sidebar-tsx/SidebarChat.tsx | 11 ++++++++--- .../src/void-command-bar-tsx/VoidCommandBar.tsx | 5 ++++- .../workbench/contrib/void/browser/toolsService.ts | 2 +- 5 files changed, 30 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index cb3ce39b..3f2aaad3 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -783,10 +783,14 @@ class ChatThreadService extends Disposable implements IChatThreadService { } // 2. if tool requires approval, break from the loop, awaiting approval - const requiresApproval = !this._settingsService.state.globalSettings.autoApprove - if (requiresApproval && toolNamesThatRequireApproval.has(toolName)) { + const requiresApproval = toolNamesThatRequireApproval.has(toolName) + if (requiresApproval) { + const autoApprove = this._settingsService.state.globalSettings.autoApprove + // add a tool_request because we use it for UI if a tool is loading (this should be improved in the future) this._addMessageToThread(threadId, { role: 'tool_request', name: toolName, paramsStr: toolParamsStr, params: toolParams, id: toolId }) - return { awaitingUserApproval: true } + if (!autoApprove) { + return { awaitingUserApproval: true } + } } } else { diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 9b7580ab..54883d08 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -1244,12 +1244,18 @@ class EditCodeService extends Disposable implements IEditCodeService { // ctrlkzone should never have any conflicts } else { + console.log('KEEPING CONFLICTS!!!!!!!!') // keep conflict on whole file - to keep conflict, revert the change and use those contents as original, then un-revert the file - const currentFileStr = originalFileStr + console.log('originalFile', originalFileStr) + console.log('diffareas A', JSON.stringify(this.diffAreasOfURI, null, 2)) this.acceptOrRejectAllDiffAreas({ uri, removeCtrlKs: true, behavior: 'reject', _addToHistory: false }) + console.log('diffareas B', JSON.stringify(this.diffAreasOfURI, null, 2)) const oldFileStr = model.getValue(EndOfLinePreference.LF) // use this as original code - this._writeURIText(uri, currentFileStr, 'wholeFileRange', { shouldRealignDiffAreas: true }) // un-revert + console.log('oldFileStr', { oldFileStr }) + this._writeURIText(uri, originalFileStr, 'wholeFileRange', { shouldRealignDiffAreas: true }) // un-revert originalCode = oldFileStr + console.log('originalCode', { originalCode }) + console.log('NEW STR', { newStr: model.getValue(EndOfLinePreference.LF) }) } } @@ -1772,7 +1778,7 @@ class EditCodeService extends Disposable implements IEditCodeService { onFinalMessage: async (params) => { const { fullText } = params - console.log('DONE - editCode!', fullText) + console.log('DONE - editCode!', { fullText }) // 1. wait 500ms and fix lint errors - call lint error workflow // (update react state to say "Fixing errors") @@ -1788,7 +1794,7 @@ class EditCodeService extends Disposable implements IEditCodeService { addedTrackingZoneOfBlockNum.sort((a, b) => a.metadata.originalBounds[0] - b.metadata.originalBounds[0]) const { model } = this._voidModelService.getModel(uri) - console.log('CURRENT\n', model?.getValue()) + console.log('CURRENT!!!', { current: model?.getValue() }) console.log('ADDED', addedTrackingZoneOfBlockNum) console.log('BLOX', blocks) 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 10a9a6dc..dc776649 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 @@ -1113,7 +1113,7 @@ const ReasoningWrapper = ({ isDoneReasoning, isStreaming, children }: { isDoneRe const loadingTitleWrapper = (item: React.ReactNode) => { return {item} - + } const folderFileStr = (isFolder: boolean) => isFolder ? 'folder' : 'file' @@ -1652,6 +1652,11 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { /> } + else { + componentParams.children = + {value} + + } } } @@ -1769,7 +1774,7 @@ const ChatBubble = ({ chatMessage, isCommitted, messageIdx, isLast, chatIsRunnin /> } else if (role === 'tool_request') { - const ToolRequestWrapper = toolNameToComponent[chatMessage.name].requestWrapper as RequestWrapper + const ToolRequestWrapper = toolNameToComponent[chatMessage.name]?.requestWrapper as RequestWrapper const toolRequestType = ( chatIsRunning === 'awaiting_user' ? 'awaiting_user' : chatIsRunning === 'tool' ? 'running' @@ -1784,7 +1789,7 @@ const ChatBubble = ({ chatMessage, isCommitted, messageIdx, isLast, chatIsRunnin return null } else if (role === 'tool') { - const ToolResultWrapper = toolNameToComponent[chatMessage.name].resultWrapper as ResultWrapper + const ToolResultWrapper = toolNameToComponent[chatMessage.name]?.resultWrapper as ResultWrapper if (ToolResultWrapper) return return null diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx index 6df78f20..718ea2ad 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx @@ -208,7 +208,10 @@ const VoidCommandBar = ({ uri, editor }: { uri: URI | null, editor: ICodeEditor File {currUriIdx + 1} of {sortedCommandBarURIs.length}
    } {currDiffIdx !== null && sortedDiffIds?.length &&
    - Diff {currDiffIdx + 1} of {sortedDiffIds?.length ?? 0} + {(sortedDiffIds?.length ?? 0) === 0 ? + <>Diff {currDiffIdx + 1} of {sortedDiffIds?.length ?? 0} + : <>(No changes)} +
    }
    diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index 629436a7..4d89a365 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -120,7 +120,7 @@ const validateStr = (argName: string, value: unknown) => { // We are NOT checking to make sure in workspace const validateURI = (uriStr: unknown) => { - if (typeof uriStr !== 'string') throw new Error('Error: provided uri must be a string.') + if (typeof uriStr !== 'string') throw new Error('Provided uri must be a string.') const uri = URI.file(uriStr) return uri From cffe053226e9d0775ad003be99ac96661e0266c3 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 20 Mar 2025 06:03:37 -0700 Subject: [PATCH 136/173] merge --- .../react/src/void-command-bar-tsx/VoidCommandBar.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx index 718ea2ad..6df78f20 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx @@ -208,10 +208,7 @@ const VoidCommandBar = ({ uri, editor }: { uri: URI | null, editor: ICodeEditor File {currUriIdx + 1} of {sortedCommandBarURIs.length}
    } {currDiffIdx !== null && sortedDiffIds?.length &&
    - {(sortedDiffIds?.length ?? 0) === 0 ? - <>Diff {currDiffIdx + 1} of {sortedDiffIds?.length ?? 0} - : <>(No changes)} - + Diff {currDiffIdx + 1} of {sortedDiffIds?.length ?? 0}
    }
    From 07e995fc2f80f79ccbe5a6337c5c9695f228104c Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 20 Mar 2025 07:24:57 -0700 Subject: [PATCH 137/173] strict:true --- .../src/void-command-bar-tsx/VoidCommandBar.tsx | 2 ++ .../contrib/void/browser/toolsService.ts | 16 +++++++++++----- .../contrib/void/common/toolsServiceTypes.ts | 13 ++----------- .../llmMessage/sendLLMMessage.impl.ts | 14 ++++++++------ 4 files changed, 23 insertions(+), 22 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx index 6df78f20..f8e85334 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx @@ -21,6 +21,8 @@ export type VoidCommandBarProps = { export const VoidCommandBarMain = ({ uri, editor }: VoidCommandBarProps) => { const isDark = useIsDark() + if (uri?.scheme !== 'file') return null // don't show in editors that we made, they must be files + return
    diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index 4d89a365..cdc27fe7 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -103,24 +103,30 @@ const directoryResultToString = (params: ToolCallParams['list_dir'], result: Too const validateJSON = (s: string): { [s: string]: unknown } => { try { const o = JSON.parse(s) + if (typeof o !== 'object') throw new Error() + + if ('result' in o) { // openrouter sometimes wraps the result with { 'result': ... } + return o.result + } + return o } catch (e) { - throw new Error(`Tool parameter was not a string of a valid JSON: "${s}".`) + throw new Error(`Invalid LLM output format: Tool parameter was not a string of a valid JSON: "${s}".`) } } const validateStr = (argName: string, value: unknown) => { - if (typeof value !== 'string') throw new Error(`Error: ${argName} must be a string.`) + if (typeof value !== 'string') throw new Error(`Invalid LLM output format: ${argName} must be a string.`) return value } // We are NOT checking to make sure in workspace const validateURI = (uriStr: unknown) => { - if (typeof uriStr !== 'string') throw new Error('Provided uri must be a string.') + if (typeof uriStr !== 'string') throw new Error('Invalid LLM output format: Provided uri must be a string.') const uri = URI.file(uriStr) return uri @@ -130,12 +136,12 @@ const validatePageNum = (pageNumberUnknown: unknown) => { if (!pageNumberUnknown) return 1 const parsedInt = Number.parseInt(pageNumberUnknown + '') if (!Number.isInteger(parsedInt)) throw new Error(`Page number was not an integer: "${pageNumberUnknown}".`) - if (parsedInt < 1) throw new Error(`Specified page number must be 1 or greater: "${pageNumberUnknown}".`) + if (parsedInt < 1) throw new Error(`Invalid LLM output format: Specified page number must be 1 or greater: "${pageNumberUnknown}".`) return parsedInt } const validateRecursiveParamStr = (paramsUnknown: unknown) => { - if (typeof paramsUnknown !== 'string') throw new Error('Error calling tool: provided params must be a string.') + if (typeof paramsUnknown !== 'string') throw new Error('Invalid LLM output format: Error calling tool: provided params must be a string.') const params = paramsUnknown const isRecursive = params.includes('r') return isRecursive diff --git a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts index a4dbabbf..0b6f7ef3 100644 --- a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts @@ -10,7 +10,6 @@ export type InternalToolInfo = { params: { [paramName: string]: { type: string, description: string | undefined } // name -> type }, - required: string[], // required paramNames } @@ -33,7 +32,7 @@ export type ResolveReason = { type: 'toofull' | 'timeout' | 'bgtask' } | { type: const paginationHelper = { desc: `Very large results may be paginated (indicated in the result). Pagination fails gracefully if out of bounds or invalid page number.`, - param: { pageNumber: { type: 'number', description: 'The page number (optional, default is 1).' }, } + param: { pageNumber: { type: 'number', description: 'The page number (default is the first page = 1).' }, } } as const export const voidTools = { @@ -46,7 +45,6 @@ export const voidTools = { uri: { type: 'string', description: undefined }, ...paginationHelper.param, }, - required: ['uri'], }, list_dir: { @@ -56,7 +54,6 @@ export const voidTools = { uri: { type: 'string', description: undefined }, ...paginationHelper.param, }, - required: ['uri'], }, pathname_search: { @@ -66,7 +63,6 @@ export const voidTools = { query: { type: 'string', description: undefined }, ...paginationHelper.param, }, - required: ['query'], }, text_search: { @@ -76,7 +72,6 @@ export const voidTools = { query: { type: 'string', description: undefined }, ...paginationHelper.param, }, - required: ['query'], }, // --- editing (create/delete) --- @@ -87,7 +82,6 @@ export const voidTools = { params: { uri: { type: 'string', description: undefined }, }, - required: ['uri'], }, delete_uri: { @@ -97,7 +91,6 @@ export const voidTools = { uri: { type: 'string', description: undefined }, params: { type: 'string', description: 'Return -r here to delete this URI and all descendants (if applicable). Default is the empty string.' } }, - required: ['uri', 'params'], }, edit: { // APPLY TOOL @@ -107,7 +100,6 @@ export const voidTools = { uri: { type: 'string', description: undefined }, changeDescription: { type: 'string', description: editToolDesc_toolDescription } // long description here }, - required: ['uri', 'changeDescription'], }, terminal_command: { @@ -116,9 +108,8 @@ export const voidTools = { params: { command: { type: 'string', description: 'The terminal command to execute.' }, waitForCompletion: { type: 'string', description: `Whether or not to await the command to complete and get the final result. Default is true. Make this value false when you want a command to run indefinitely without waiting for it.` }, - terminalId: { type: 'string', description: 'Optional (if provided, value must be an integer >= 1). This is the ID of the terminal instance to execute the command in. The primary purpose of this is to start a new terminal for background processes or tasks that run indefinitely (e.g. if you want to run a server locally). Fails gracefully if a terminal ID does not exist, by creating a new terminal instance. Defaults to the preferred terminal ID.' }, + terminalId: { type: 'string', description: 'Optional (value must be an integer >= 1, or empty which will go with the default). This is the ID of the terminal instance to execute the command in. The primary purpose of this is to start a new terminal for background processes or tasks that run indefinitely (e.g. if you want to run a server locally). Fails gracefully if a terminal ID does not exist, by creating a new terminal instance. Defaults to the preferred terminal ID.' }, }, - required: ['command'], }, 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 27e99357..9e82f58c 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 @@ -36,17 +36,19 @@ const invalidApiKeyMessage = (providerName: ProviderName) => `Invalid ${displayI // ------------ OPENAI-COMPATIBLE (HELPERS) ------------ const toOpenAICompatibleTool = (toolInfo: InternalToolInfo) => { - const { name, description, params, required } = toolInfo + const { name, description, params } = toolInfo return { type: 'function', function: { name: name, + strict: true, // strict mode - https://platform.openai.com/docs/guides/function-calling?api-mode=chat description: description, parameters: { type: 'object', properties: params, - required: required, - } + required: Object.keys(params), // in strict mode, all params are required and additionalProperties is false + additionalProperties: false, + }, } } satisfies OpenAI.Chat.Completions.ChatCompletionTool } @@ -283,15 +285,15 @@ const _openaiCompatibleList = async ({ onSuccess: onSuccess_, onError: onError_, // ------------ ANTHROPIC ------------ const toAnthropicTool = (toolInfo: InternalToolInfo) => { - const { name, description, params, required } = toolInfo + const { name, description, params } = toolInfo return { name: name, description: description, input_schema: { type: 'object', properties: params, - required: required, - } + required: Object.keys(params), + }, } satisfies Anthropic.Messages.Tool } From e894be803b66e3ca931e8962e25ec5c4db563eb6 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Thu, 20 Mar 2025 22:10:45 -0700 Subject: [PATCH 138/173] command bar draft --- .../void-command-bar-tsx/VoidCommandBar.tsx | 37 ++++++++---- .../void/browser/voidCommandBarService.ts | 58 ++++++++++++++----- 2 files changed, 69 insertions(+), 26 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx index f8e85334..9864110e 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx @@ -7,18 +7,12 @@ import { useAccessor, useCommandBarState, useIsDark } from '../util/services.js'; import '../styles.css' -import { useCallback, useEffect, useState } from 'react'; -import { URI } from '../../../../../../../base/common/uri.js'; -import { ICodeEditor } from '../../../../../../../editor/browser/editorBrowser.js'; +import { useCallback, useEffect, useState, useRef } from 'react'; import { ScrollType } from '../../../../../../../editor/common/editorCommon.js'; import { acceptAllBg, acceptBorder, buttonFontSize, buttonTextColor, rejectAllBg, rejectBorder } from '../../../../common/helpers/colors.js'; +import { VoidCommandBarProps } from '../../../voidCommandBarService.js'; -export type VoidCommandBarProps = { - uri: URI | null; - editor: ICodeEditor; -} - -export const VoidCommandBarMain = ({ uri, editor }: VoidCommandBarProps) => { +export const VoidCommandBarMain = ({ uri, editor, onChangeHeight }: VoidCommandBarProps) => { const isDark = useIsDark() if (uri?.scheme !== 'file') return null // don't show in editors that we made, they must be files @@ -26,7 +20,7 @@ export const VoidCommandBarMain = ({ uri, editor }: VoidCommandBarProps) => { return
    - +
    } @@ -38,9 +32,11 @@ const stepIdx = (currIdx: number | null, len: number, step: -1 | 1) => { return ((currIdx ?? 0) + step + len) % len // for some reason, small negatives are kept negative. just add len to offset } +const DummyContainer = ()=>{ +} -const VoidCommandBar = ({ uri, editor }: { uri: URI | null, editor: ICodeEditor }) => { +const VoidCommandBar = ({ uri, editor, onChangeHeight }: VoidCommandBarProps) => { const accessor = useAccessor() const editCodeService = accessor.get('IEditCodeService') const editorService = accessor.get('ICodeEditorService') @@ -50,6 +46,23 @@ const VoidCommandBar = ({ uri, editor }: { uri: URI | null, editor: ICodeEditor const voidModelService = accessor.get('IVoidModelService') const { state: commandBarState, sortedURIs: sortedCommandBarURIs } = useCommandBarState() + // Add a reference to the container for resize observer + const sizerRef = useRef(null) + + // Add the resize observer effect + useEffect(() => { + const inputContainer = sizerRef.current + if (!inputContainer) return; + // only observing 1 element + let resizeObserver: ResizeObserver | undefined + resizeObserver = new ResizeObserver((entries) => { + const height = entries[0].borderBoxSize[0].blockSize + onChangeHeight(height) + }) + resizeObserver.observe(inputContainer); + return () => { resizeObserver?.disconnect(); }; + }, [onChangeHeight]); + // changes if the user clicks left/right or if the user goes on a uri with changes const [currUriIdx, setUriIdx] = useState(null) @@ -263,7 +276,7 @@ const VoidCommandBar = ({ uri, editor }: { uri: URI | null, editor: ICodeEditor - return
    + return
    {currUriHasChanges && <>
    {acceptAllButton} diff --git a/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts b/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts index 4b3b8645..28392bd5 100644 --- a/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts +++ b/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts @@ -17,6 +17,7 @@ import { InstantiationType, registerSingleton } from '../../../../platform/insta import { IEditCodeService } from './editCodeServiceInterface.js'; import { ITextModel } from '../../../../editor/common/model.js'; import { IModelService } from '../../../../editor/common/services/model.js'; +import { generateUuid } from '../../../../base/common/uuid.js'; @@ -112,6 +113,8 @@ export class VoidCommandBarService extends Disposable implements IVoidCommandBar const onCodeEditorAdd = (editor: ICodeEditor) => { const id = editor.getId(); disposablesOfEditorId[id] = []; + + // mount the command bar const d1 = this._instantiationService.createInstance(AcceptRejectAllFloatingWidget, { editor }); disposablesOfEditorId[id].push(d1); const d2 = editor.onDidChangeModel((e) => { if (e?.newModelUrl?.scheme === 'file') updateActiveURI() }) @@ -346,6 +349,12 @@ registerSingleton(IVoidCommandBarService, VoidCommandBarService, InstantiationTy // registerWorkbenchContribution2(VoidCommandBarService.ID, VoidCommandBarService, WorkbenchPhase.BlockRestore); +export type VoidCommandBarProps = { + uri: URI | null; + editor: ICodeEditor; + onChangeHeight: (height: number) => void; +} + @@ -354,14 +363,15 @@ class AcceptRejectAllFloatingWidget extends Widget implements IOverlayWidget { private readonly editor: ICodeEditor; private readonly ID: string; + _height = 0 + constructor({ editor }: { editor: ICodeEditor, }, @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); - this.ID = editor.getId() + '-voidfloatingwidget'; + this.ID = generateUuid(); this.editor = editor; - // Create container div const { root } = dom.h('div@root'); @@ -370,30 +380,50 @@ class AcceptRejectAllFloatingWidget extends Widget implements IOverlayWidget { root.style.padding = '4px'; root.style.alignItems = 'center'; root.style.pointerEvents = 'none'; - // Mount command bar using mountVoidCommandBar + // this.editor.getDomNode()?.appendChild(root) + + const onChangeHeight = (height: number) => { + if (height === 0) return; + + this._height = height + // editor.layoutOverlayWidget(this) + // stupid hack because layoutOverlayWidget doesn't work + editor.removeOverlayWidget(this) + editor.addOverlayWidget(this) + } + + // alternative to mount VoidCommandBar without the stupid widget + // editor.getLayoutInfo() + // this._register( + // editor.onDidLayoutChange(e => { + // // e.height + // // e.width + // }) + // ) + + + + + this._domNode = root; + editor.addOverlayWidget(this); + this.instantiationService.invokeFunction(accessor => { - type Props = { uri: URI | null, editor: ICodeEditor } const uri = editor.getModel()?.uri || null - const res = mountVoidCommandBar(root, accessor, { uri, editor } satisfies Props) + + const res = mountVoidCommandBar(root, accessor, { uri, editor, onChangeHeight } satisfies VoidCommandBarProps) if (!res) return - const dispose = res.dispose - const rerender: (o: Props) => void = res.rerender - - this._register(toDisposable(() => dispose?.())) + this._register(toDisposable(() => res.dispose?.())) this._register(editor.onDidChangeModel((model) => { const uri = model.newModelUrl - rerender({ uri, editor }) + res.rerender({ uri, editor, onChangeHeight }) })) }); - this._domNode = root; - // Mount the widget - editor.addOverlayWidget(this); } @@ -407,7 +437,7 @@ class AcceptRejectAllFloatingWidget extends Widget implements IOverlayWidget { public getPosition() { return { - preference: OverlayWidgetPositionPreference.BOTTOM_RIGHT_CORNER, + preference: OverlayWidgetPositionPreference.BOTTOM_RIGHT_CORNER } } From 4dff3e216a7491035b83255ca73bf35ca1427871 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Fri, 21 Mar 2025 00:22:51 -0700 Subject: [PATCH 139/173] command bar decent --- .../void-command-bar-tsx/VoidCommandBar.tsx | 105 ++++++++++++------ .../void/browser/voidCommandBarService.ts | 37 ++---- 2 files changed, 79 insertions(+), 63 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx index 9864110e..407305cd 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx @@ -12,7 +12,7 @@ import { ScrollType } from '../../../../../../../editor/common/editorCommon.js'; import { acceptAllBg, acceptBorder, buttonFontSize, buttonTextColor, rejectAllBg, rejectBorder } from '../../../../common/helpers/colors.js'; import { VoidCommandBarProps } from '../../../voidCommandBarService.js'; -export const VoidCommandBarMain = ({ uri, editor, onChangeHeight }: VoidCommandBarProps) => { +export const VoidCommandBarMain = ({ uri, editor }: VoidCommandBarProps) => { const isDark = useIsDark() if (uri?.scheme !== 'file') return null // don't show in editors that we made, they must be files @@ -20,7 +20,7 @@ export const VoidCommandBarMain = ({ uri, editor, onChangeHeight }: VoidCommandB return
    - +
    } @@ -32,11 +32,9 @@ const stepIdx = (currIdx: number | null, len: number, step: -1 | 1) => { return ((currIdx ?? 0) + step + len) % len // for some reason, small negatives are kept negative. just add len to offset } -const DummyContainer = ()=>{ -} -const VoidCommandBar = ({ uri, editor, onChangeHeight }: VoidCommandBarProps) => { +const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { const accessor = useAccessor() const editCodeService = accessor.get('IEditCodeService') const editorService = accessor.get('ICodeEditorService') @@ -46,28 +44,11 @@ const VoidCommandBar = ({ uri, editor, onChangeHeight }: VoidCommandBarProps) => const voidModelService = accessor.get('IVoidModelService') const { state: commandBarState, sortedURIs: sortedCommandBarURIs } = useCommandBarState() - // Add a reference to the container for resize observer - const sizerRef = useRef(null) - - // Add the resize observer effect - useEffect(() => { - const inputContainer = sizerRef.current - if (!inputContainer) return; - // only observing 1 element - let resizeObserver: ResizeObserver | undefined - resizeObserver = new ResizeObserver((entries) => { - const height = entries[0].borderBoxSize[0].blockSize - onChangeHeight(height) - }) - resizeObserver.observe(inputContainer); - return () => { resizeObserver?.disconnect(); }; - }, [onChangeHeight]); // changes if the user clicks left/right or if the user goes on a uri with changes const [currUriIdx, setUriIdx] = useState(null) const [currUriHasChanges, setCurrUriHasChanges] = useState(false) - const anyUriHasChanges = sortedCommandBarURIs.length !== 0 useEffect(() => { const i = sortedCommandBarURIs.findIndex(e => e.fsPath === uri?.fsPath) if (i !== -1) { @@ -137,19 +118,26 @@ const VoidCommandBar = ({ uri, editor, onChangeHeight }: VoidCommandBarProps) => const currDiffIdx = uri ? commandBarState[uri.fsPath]?.diffIdx ?? null : null - const sortedDiffIds = uri ? commandBarState[uri.fsPath]?.sortedDiffIds ?? null : null + const sortedDiffIds = uri ? commandBarState[uri.fsPath]?.sortedDiffIds ?? [] : [] + const sortedDiffZoneIds = uri ? commandBarState[uri.fsPath]?.sortedDiffZoneIds ?? [] : [] + const nextDiffIdx = getNextDiffIdx(1) const prevDiffIdx = getNextDiffIdx(-1) const nextURIIdx = getNextUriIdx(1) const prevURIIdx = getNextUriIdx(-1) + const isAChangeInThisFile = sortedDiffIds.length !== 0 + const isADiffZoneInThisFile = sortedDiffZoneIds.length !== 0 + const isADiffZoneInAnyFile = sortedCommandBarURIs.length !== 0 - // if there are *any* changes at all - const navPanel = anyUriHasChanges &&
    + if (!isADiffZoneInAnyFile) { // no changes for the user to accept + return null + } + + + const buttonsHTML = <> + -
    + + const descriptionHTML = isADiffZoneInThisFile ? + <> {currUriIdx !== null && sortedCommandBarURIs.length &&
    - File {currUriIdx + 1} of {sortedCommandBarURIs.length} + {`File ${currUriIdx + 1} of ${sortedCommandBarURIs.length}`}
    } - {currDiffIdx !== null && sortedDiffIds?.length &&
    - Diff {currDiffIdx + 1} of {sortedDiffIds?.length ?? 0} -
    } -
    -
    + {/*
    + {!isAChangeInThisFile ? + `(No changes)` + : `Diff ${(currDiffIdx ?? 0) + 1} of ${sortedDiffIds.length}` + } +
    */} + + : <> + {`${sortedCommandBarURIs.length} file${sortedCommandBarURIs.length === 1 ? '' : 's'}`} + + + // accept/reject if current URI has changes @@ -276,13 +274,48 @@ const VoidCommandBar = ({ uri, editor, onChangeHeight }: VoidCommandBarProps) => - return
    + // const closeCommandBar = useCallback(() => { + // commandService.executeCommand('void.hideCommandBar'); + // }, [commandService]); + + // const hideButton = + + // const actionButtons = currUriHasChanges && ( + //
    + // {acceptAllButton} + // {rejectAllButton} + // {hideButton} + //
    + // ); + + + // dummy container due to annoyances with VS Code mounting the widget + return
    {currUriHasChanges && <>
    {acceptAllButton} {rejectAllButton}
    } - {navPanel} +
    +
    + {buttonsHTML} +
    +
    + {descriptionHTML} +
    +
    } diff --git a/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts b/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts index 28392bd5..2b01e37b 100644 --- a/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts +++ b/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts @@ -352,7 +352,6 @@ registerSingleton(IVoidCommandBarService, VoidCommandBarService, InstantiationTy export type VoidCommandBarProps = { uri: URI | null; editor: ICodeEditor; - onChangeHeight: (height: number) => void; } @@ -376,33 +375,17 @@ class AcceptRejectAllFloatingWidget extends Widget implements IOverlayWidget { const { root } = dom.h('div@root'); // Style the container + // root.style.backgroundColor = 'rgb(248 113 113)'; + root.style.height = '16rem'; // make a fixed size, and all contents go on the bottom right. this fixes annoying VS Code mounting issues + root.style.width = '16rem'; + root.style.flexDirection = 'column'; + root.style.justifyContent = 'flex-end'; + root.style.alignItems = 'flex-end'; root.style.zIndex = '2'; root.style.padding = '4px'; - root.style.alignItems = 'center'; root.style.pointerEvents = 'none'; - // Mount command bar using mountVoidCommandBar - // this.editor.getDomNode()?.appendChild(root) - - const onChangeHeight = (height: number) => { - if (height === 0) return; - - this._height = height - // editor.layoutOverlayWidget(this) - // stupid hack because layoutOverlayWidget doesn't work - editor.removeOverlayWidget(this) - editor.addOverlayWidget(this) - } - - // alternative to mount VoidCommandBar without the stupid widget - // editor.getLayoutInfo() - // this._register( - // editor.onDidLayoutChange(e => { - // // e.height - // // e.width - // }) - // ) - - + root.style.display = 'flex'; + root.style.overflow = 'hidden'; this._domNode = root; @@ -412,14 +395,14 @@ class AcceptRejectAllFloatingWidget extends Widget implements IOverlayWidget { const uri = editor.getModel()?.uri || null - const res = mountVoidCommandBar(root, accessor, { uri, editor, onChangeHeight } satisfies VoidCommandBarProps) + const res = mountVoidCommandBar(root, accessor, { uri, editor } satisfies VoidCommandBarProps) if (!res) return this._register(toDisposable(() => res.dispose?.())) this._register(editor.onDidChangeModel((model) => { const uri = model.newModelUrl - res.rerender({ uri, editor, onChangeHeight }) + res.rerender({ uri, editor }) })) }); From 85116817107dfe8efc56547b0d5bfffc879156b9 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 21 Mar 2025 02:47:31 -0700 Subject: [PATCH 140/173] fixed events and interrupts during agent mode --- .../contrib/void/browser/chatThreadService.ts | 128 ++++---- .../contrib/void/browser/editCodeService.ts | 172 +++++----- .../src/markdown/ApplyBlockHoverButtons.tsx | 4 +- .../src/quick-edit-tsx/QuickEditChat.tsx | 2 +- .../react/src/sidebar-tsx/SidebarChat.tsx | 10 +- .../void-command-bar-tsx/VoidCommandBar.tsx | 14 +- .../void/browser/terminalToolService.ts | 6 +- .../contrib/void/browser/toolsService.ts | 10 +- .../contrib/void/common/prompt/prompts.ts | 298 +++++++++++++----- .../contrib/void/common/toolsServiceTypes.ts | 120 +------ 10 files changed, 401 insertions(+), 363 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 3f2aaad3..2455bb44 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -11,13 +11,13 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo import { URI } from '../../../../base/common/uri.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { ILLMMessageService } from '../common/sendLLMMessageService.js'; -import { chat_userMessageContent, chat_systemMessage, chat_lastUserMessageWithFilesAdded, chat_selectionsString } from '../common/prompt/prompts.js'; +import { chat_userMessageContent, chat_systemMessage, chat_lastUserMessageWithFilesAdded, chat_selectionsString, voidTools } from '../common/prompt/prompts.js'; import { getErrorMessage, LLMChatMessage, ToolCallType } from '../common/sendLLMMessageTypes.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; import { generateUuid } from '../../../../base/common/uuid.js'; import { ChatMode, FeatureName, ModelSelection, ModelSelectionOptions } from '../common/voidSettingsTypes.js'; import { IVoidSettingsService } from '../common/voidSettingsService.js'; -import { ToolName, ToolCallParams, ToolResultType, InternalToolInfo, voidTools, toolNamesThatRequireApproval } from '../common/toolsServiceTypes.js'; +import { ToolName, ToolCallParams, ToolResultType, toolNamesThatRequireApproval, InternalToolInfo } from '../common/toolsServiceTypes.js'; import { IToolsService } from './toolsService.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js'; @@ -109,11 +109,11 @@ export type ThreadsState = { export type IsRunningType = undefined | 'message' | 'tool' | 'awaiting_user' export type ThreadStreamState = { [threadId: string]: undefined | { - // state related + // state related to streaming (not just when streaming) isRunning?: IsRunningType; // whether or not actually running the agent loop (can be running and not streaming, like if it's calling a tool and awaiting user response) error?: { message: string, fullError: Error | null, }; - // streaming related + // streaming related - when streaming message streamingToken?: string; messageSoFar?: string; reasoningSoFar?: string; @@ -426,7 +426,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { result: { type: 'success', params: { uri: URI.file('/Users/username/Project/main.js'), changeDescription: 'I think we should do this:\n```typescript\n//Add console.log statement\n for i in ...\n\t\tdo:\nabc\n```' }, - value: {} + value: Promise.resolve() }, } satisfies ToolMessage<'edit'>, { @@ -645,14 +645,12 @@ class ChatThreadService extends Disposable implements IChatThreadService { const callThisToolFirst: ToolRequestApproval = lastMessage - this._chatAgentLoop({ callThisToolFirst, prevSelns, currSelns, threadId, userMessageContent: instructions, ...this._currentModelSelectionProps() }) + this._runChatAgent({ callThisToolFirst, prevSelns, currSelns, threadId, userMessageContent: instructions, ...this._currentModelSelectionProps() }) } rejectTool(threadId: string) { const thread = this.state.allThreads[threadId] if (!thread) return // should never happen - this._cancelToolOfThreadId[threadId]?.() - const lastMessage = thread.messages[thread.messages.length - 1] if (lastMessage.role !== 'tool_request') return // should never happen const { name, params, paramsStr, id } = lastMessage @@ -665,21 +663,24 @@ class ChatThreadService extends Disposable implements IChatThreadService { const thread = this.state.allThreads[threadId] if (!thread) return // should never happen - const messageSoFar = this.streamState[threadId]?.messageSoFar ?? '' - const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? '' - - // abort the stream first so it doesn't change any state - const llmCancelToken = this.streamState[threadId]?.streamingToken - if (llmCancelToken !== undefined) { this._llmMessageService.abort(llmCancelToken) } - - // add the correct message to the state - const lastMessage = thread.messages[thread.messages.length - 1] - if (lastMessage.role === 'tool_request') { - // interrupt tool request + const isRunning = this.streamState[threadId]?.isRunning + // reject the tool for the user + if (isRunning === 'awaiting_user') { this.rejectTool(threadId) } - else { - // interrupt assistant message + // interrupt the tool + else if (isRunning === 'tool') { + this._currentlyRunningToolInterruptor[threadId]?.() + } + // interrupt assistant message + else if (isRunning === 'message') { + // abort the stream first so it doesn't change any state + const messageSoFar = this.streamState[threadId]?.messageSoFar ?? '' + const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? '' + + const llmCancelToken = this.streamState[threadId]?.streamingToken + if (llmCancelToken !== undefined) { this._llmMessageService.abort(llmCancelToken) } + this._addMessageToThread(threadId, { role: 'assistant', content: messageSoFar, reasoning: reasoningSoFar, anthropicReasoning: null }) } @@ -706,9 +707,9 @@ class ChatThreadService extends Disposable implements IChatThreadService { } - private readonly _cancelToolOfThreadId: { [threadId: string]: (() => void) | undefined } = {} + private readonly _currentlyRunningToolInterruptor: { [threadId: string]: (() => void) | undefined } = {} - private async _chatAgentLoop({ + private async _runChatAgent({ threadId, prevSelns, currSelns, @@ -761,7 +762,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { const handleToolCall = async ( tool: ToolCallType, opts?: { preapproved: true, toolParams: ToolCallParams[ToolName] }, - ): Promise<{ awaitingUserApproval?: boolean, canceled?: boolean }> => { + ): Promise<{ awaitingUserApproval?: boolean, interrupted?: boolean }> => { const toolName: ToolName = tool.name const toolParamsStr = tool.paramsStr const toolId = tool.id @@ -799,17 +800,22 @@ class ChatThreadService extends Disposable implements IChatThreadService { // 3. call the tool this._setStreamState(threadId, { isRunning: 'tool' }, 'merge') - let canceled = false + let interrupted = false try { - const { result, cancel } = await this._toolsService.callTool[toolName](toolParams as any) // ts is bad... - this._cancelToolOfThreadId[threadId] = cancel - let cancelRes: () => void = () => { } - const resolveIfCancel = new Promise((res, rej) => { cancelRes = rej }) - this._cancelToolOfThreadId[threadId] = () => { cancel?.(); canceled = true; delete this._cancelToolOfThreadId[threadId]; cancelRes() } - toolResult = await Promise.race([result, resolveIfCancel]) // this await is needed, typescript is bad... + const { result, interruptTool } = await this._toolsService.callTool[toolName](toolParams as any) + this._currentlyRunningToolInterruptor[threadId] = () => { + interrupted = true; + interruptTool?.(); + delete this._currentlyRunningToolInterruptor[threadId]; + } + toolResult = await result // ts is bad... await is needed } catch (error) { - if (canceled) return { canceled: true } + if (interrupted) { + // ideally this should have same implementation as abort - addMessage should get called in stopRunning + this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: toolParamsStr, id: toolId, content: this.errMsgs.rejected, result: { type: 'rejected', params: toolParams }, }) + return { interrupted: true } + } const errorMessage = getErrorMessage(error) this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: toolParamsStr, id: toolId, content: errorMessage, result: { type: 'error', params: toolParams, value: errorMessage }, }) return {} @@ -843,25 +849,22 @@ class ChatThreadService extends Disposable implements IChatThreadService { // before enter loop, call tool if (callThisToolFirst) { - this._setStreamState(threadId, { isRunning: 'tool' }, 'merge') - const { canceled } = await handleToolCall(callThisToolFirst, { preapproved: true, toolParams: callThisToolFirst.params }) - if (canceled) return + const { interrupted } = await handleToolCall(callThisToolFirst, { preapproved: true, toolParams: callThisToolFirst.params }) + if (interrupted) return } // tool use loop while (shouldSendAnotherMessage) { - this._setStreamState(threadId, { isRunning: 'message' }, 'merge') - // false by default each iteration shouldSendAnotherMessage = false isRunningWhenEnd = undefined - nMessagesSent += 1 - let resMessageIsDonePromise: () => void // resolves when user approves this tool use (or if tool doesn't require approval) - const messageIsDonePromise = new Promise((res, rej) => { resMessageIsDonePromise = res }) + let resMessageIsDonePromise: (toolCalls?: ToolCallType[] | undefined) => void // resolves when user approves this tool use (or if tool doesn't require approval) + const messageIsDonePromise = new Promise((res, rej) => { resMessageIsDonePromise = res }) // send llm message + this._setStreamState(threadId, { isRunning: 'message' }, 'merge') const messages = await getLatestMessages() const llmCancelToken = this._llmMessageService.sendLLMMessage({ messagesType: 'chatMessages', @@ -872,23 +875,11 @@ class ChatThreadService extends Disposable implements IChatThreadService { logging: { loggingName: `Chat - ${chatMode}`, loggingExtras: { threadId, nMessagesSent, chatMode } }, onText: ({ fullText, fullReasoning }) => { this._setStreamState(threadId, { messageSoFar: fullText, reasoningSoFar: fullReasoning }, 'merge') }, onFinalMessage: async ({ fullText, toolCalls, fullReasoning, anthropicReasoning }) => { - this._addMessageToThread(threadId, { role: 'assistant', content: fullText, reasoning: fullReasoning, anthropicReasoning }) // added to history and no longer streaming this, so clear messages so far and streamingToken (but do not stop isRunning) this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, }, 'merge') - - // call tool if there is one - const tool: ToolCallType | undefined = toolCalls?.[0] - if (tool) { - const { awaitingUserApproval } = await handleToolCall(tool) // things happen correctly if canceled is true here, because canceled calls onAbort - if (awaitingUserApproval) { - isRunningWhenEnd = 'awaiting_user' - } else { - shouldSendAnotherMessage = true - } - - } - resMessageIsDonePromise() + // resolve with tool calls + resMessageIsDonePromise(toolCalls) }, onError: (error) => { const messageSoFar = this.streamState[threadId]?.messageSoFar ?? '' @@ -913,11 +904,28 @@ class ChatThreadService extends Disposable implements IChatThreadService { }, 'set') break } - this._setStreamState(threadId, { streamingToken: llmCancelToken }, 'merge') // new stream token for the new message - - await messageIsDonePromise + const toolCalls = await messageIsDonePromise // wait for message to complete if (aborted) { return } + this._setStreamState(threadId, { streamingToken: undefined }, 'merge') // streaming message is done + + // call tool if there is one + const tool: ToolCallType | undefined = toolCalls?.[0] + if (tool) { + const { awaitingUserApproval, interrupted } = await handleToolCall(tool) + + // stop if interrupted. we don't have to do this for llmMessage because we have a stream token for it and onAbort gets called, but we don't have the equivalent for tools. + // just detect tool interruption which is the same as chat interruption right now + if (interrupted) { return } + + if (awaitingUserApproval) { + isRunningWhenEnd = 'awaiting_user' + } + else { + shouldSendAnotherMessage = true + } + } + } // end while @@ -940,9 +948,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { // if the current thread is already streaming, stop it (this simply resolves the promise to free up space) const llmCancelToken = this.streamState[threadId]?.streamingToken - if (llmCancelToken) { - if (llmCancelToken !== undefined) this._llmMessageService.abort(llmCancelToken) - } + if (llmCancelToken !== undefined) this._llmMessageService.abort(llmCancelToken) // selections in all past chats, then in current chat (can have many duplicates here) const prevSelns: StagingSelectionItem[] = _chatSelections?.prevSelns ?? this._getAllSelections(threadId) @@ -955,7 +961,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { const userHistoryElt: ChatMessage = { role: 'user', content: userMessageContent, displayContent: instructions, selections: currSelns, state: defaultMessageState } this._addMessageToThread(threadId, userHistoryElt) - this._chatAgentLoop({ prevSelns, currSelns, threadId, userMessageContent, ...this._currentModelSelectionProps(), }) + this._runChatAgent({ prevSelns, currSelns, threadId, userMessageContent, ...this._currentModelSelectionProps(), }) } dismissStreamError(threadId: string): void { diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 54883d08..dba6870a 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -722,7 +722,7 @@ class EditCodeService extends Disposable implements IEditCodeService { - private _addToHistory(uri: URI, opts?: { onUndo?: () => void }) { + private _addToHistory(uri: URI, opts?: { onWillUndo?: () => void }) { const getCurrentSnapshot = (): HistorySnapshot => { @@ -807,7 +807,7 @@ class EditCodeService extends Disposable implements IEditCodeService { resource: uri, label: 'Void Changes', code: 'undoredo.editCode', - undo: () => { restoreDiffAreas(beforeSnapshot); opts?.onUndo?.() }, + undo: () => { opts?.onWillUndo?.(); restoreDiffAreas(beforeSnapshot); }, redo: () => { if (afterSnapshot) restoreDiffAreas(afterSnapshot) } } this._undoRedoService.pushElement(elt) @@ -1163,7 +1163,7 @@ class EditCodeService extends Disposable implements IEditCodeService { - // the applyDonePromise this returns can throw an error (reject) + // the applyDonePromise this returns can reject, and should be caught with .catch public async startApplying(opts: StartApplyingOpts): Promise<[URI, Promise] | null> { let res: [DiffZone, Promise] | undefined = undefined @@ -1212,14 +1212,14 @@ class EditCodeService extends Disposable implements IEditCodeService { uri, startBehavior, streamRequestIdRef, - onUndo, linkedCtrlKZone, + onWillUndo, }: { uri: URI, startBehavior: 'accept-conflicts' | 'reject-conflicts' | 'keep-conflicts', streamRequestIdRef: { current: string | null }, linkedCtrlKZone: CtrlKZone | null, - onUndo: () => void, + onWillUndo: () => void, }) { const { model } = this._voidModelService.getModel(uri) if (!model) return @@ -1235,8 +1235,9 @@ class EditCodeService extends Disposable implements IEditCodeService { const originalFileStr = model.getValue(EndOfLinePreference.LF) let originalCode = model.getValueInRange(range, EndOfLinePreference.LF) + // add to history as a checkpoint, before we start modifying - const { onFinishEdit } = this._addToHistory(uri, { onUndo }) + const { onFinishEdit } = this._addToHistory(uri, { onWillUndo }) // clear diffZones so no conflict if (startBehavior === 'keep-conflicts') { @@ -1328,21 +1329,12 @@ class EditCodeService extends Disposable implements IEditCodeService { throw new Error(`Void: diff.type not recognized on: ${from}`) } - console.log('q1', this.diffAreaOfId) await this._voidModelService.initializeModel(uri) - console.log('q2', this.diffAreaOfId) const { model } = this._voidModelService.getModel(uri) if (!model) return - console.log('q3', this.diffAreaOfId) - - // promise that resolves when the apply is done - let resApplyPromise: () => void - let rejApplyPromise: (e: any) => void - const applyPromise = new Promise((res_, rej_) => { resApplyPromise = res_; rejApplyPromise = rej_ }) let streamRequestIdRef: { current: string | null } = { current: null } // can use this as a proxy to set the diffArea's stream state requestId - // build messages const quickEditFIMTags = defaultQuickEditFimTags // TODO can eventually let users customize modelFimTags const originalFileCode = model.getValue(EndOfLinePreference.LF) @@ -1357,13 +1349,9 @@ class EditCodeService extends Disposable implements IEditCodeService { ] } else if (from === 'QuickEdit') { - console.log('aaa') if (!ctrlKZoneIfQuickEdit) return - console.log('bbb', ctrlKZoneIfQuickEdit) const { _mountInfo } = ctrlKZoneIfQuickEdit - console.log('ccc', _mountInfo) const instructions = _mountInfo?.textAreaRef.current?.value ?? '' - console.log('ddd', instructions) const startLine = startRange === 'fullFile' ? 1 : startRange[0] const endLine = startRange === 'fullFile' ? model.getLineCount() : startRange[1] @@ -1378,17 +1366,22 @@ class EditCodeService extends Disposable implements IEditCodeService { else { throw new Error(`featureName ${from} is invalid`) } + // start diffzone const res = this._startStreamingDiffZone({ uri, streamRequestIdRef, startBehavior: opts.startBehavior, - onUndo: () => { if (diffZone._streamState.isStreaming) rejApplyPromise(new Error('Edit was interrupted by pressing undo.')) }, linkedCtrlKZone: ctrlKZoneIfQuickEdit, + onWillUndo: () => { + if (streamRequestIdRef.current) { + this._llmMessageService.abort(streamRequestIdRef.current) + } + }, + }) if (!res) return - const { diffZone, onFinishEdit } = res - + const { diffZone, onFinishEdit, } = res // helpers @@ -1408,6 +1401,14 @@ class EditCodeService extends Disposable implements IEditCodeService { onFinishEdit() } + // throws + const onError = (e: { message: string; fullError: Error | null; }) => { + this._notifyError(e) + onDone() + this._undoHistory(uri) + throw e.fullError + } + const extractText = (fullText: string, recentlyAddedTextLen: number) => { if (from === 'QuickEdit') { return extractCodeFromFIM({ text: fullText, recentlyAddedTextLen, midTag: quickEditFIMTags.midTag }) @@ -1418,7 +1419,6 @@ class EditCodeService extends Disposable implements IEditCodeService { throw new Error('Void 1') } - // refresh now in case onText takes a while to get 1st message this._refreshStylesAndDiffsInURI(uri) @@ -1428,7 +1428,8 @@ class EditCodeService extends Disposable implements IEditCodeService { const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName] const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[modelSelection.providerName]?.[modelSelection.modelName] : undefined - const writeover = async () => { + // allowed to throw errors - this is called inside a promise that handles everything + const runWriteover = async () => { let shouldSendAnotherMessage = true while (shouldSendAnotherMessage) { shouldSendAnotherMessage = false @@ -1440,6 +1441,8 @@ class EditCodeService extends Disposable implements IEditCodeService { let fullTextSoFar = '' // so far (INCLUDING ignored suffix) let prevIgnoredSuffix = '' let aborted = false + let weAreAborting = false + streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({ messagesType: 'chatMessages', @@ -1476,31 +1479,27 @@ class EditCodeService extends Disposable implements IEditCodeService { resMessageDonePromise() }, onError: (e) => { - this._notifyError(e) - onDone() - this._undoHistory(uri) - resMessageDonePromise() + onError(e) }, onAbort: () => { + if (weAreAborting) return // stop the loop to free up the promise, but don't modify state (already handled by whatever stopped it) - resMessageDonePromise() aborted = true + resMessageDonePromise() }, }) // should never happen, just for safety if (streamRequestIdRef.current === null) { return } await messageDonePromise - if (aborted) { return } + if (aborted) { + throw new Error(`Edit was interrupted by the user.`) + } } // end while } // end writeover - writeover().then(() => { - resApplyPromise() - // this._noLongerNeedModelReference(uri) - }).catch((e) => rejApplyPromise(e)) - - return [diffZone, applyPromise] + const applyDonePromise = runWriteover() + return [diffZone, applyDonePromise] } @@ -1523,10 +1522,6 @@ class EditCodeService extends Disposable implements IEditCodeService { const { model } = this._voidModelService.getModel(uri) if (!model) return - // promise that resolves when the apply is done - let resApplyDonePromise: () => void - let rejApplyDonePromise: (e: any) => void - const applyDonePromise = new Promise((res_, rej_) => { resApplyDonePromise = res_; rejApplyDonePromise = rej_ }) let streamRequestIdRef: { current: string | null } = { current: null } // can use this as a proxy to set the diffArea's stream state requestId @@ -1544,7 +1539,11 @@ class EditCodeService extends Disposable implements IEditCodeService { streamRequestIdRef, startBehavior: opts.startBehavior, linkedCtrlKZone: null, - onUndo: () => { if (diffZone._streamState.isStreaming) rejApplyDonePromise(new Error('Edit was interrupted by user pressing undo.')) }, + onWillUndo: () => { + if (streamRequestIdRef.current) { + this._llmMessageService.abort(streamRequestIdRef.current) // triggers onAbort() + } + }, }) if (!res) return const { diffZone, onFinishEdit } = res @@ -1573,7 +1572,7 @@ class EditCodeService extends Disposable implements IEditCodeService { } - const errMsgOfInvalidStr = (str: string & ReturnType, blockOrig: string, blockNum: number, blocks: ExtractedSearchReplaceBlock[]) => { + const errContentOfInvalidStr = (str: string & ReturnType, blockOrig: string, blockNum: number, blocks: ExtractedSearchReplaceBlock[]) => { 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)}` @@ -1604,6 +1603,13 @@ class EditCodeService extends Disposable implements IEditCodeService { onFinishEdit() } + const onError = (e: { message: string; fullError: Error | null; }) => { + this._notifyError(e) + onDone() + this._undoHistory(uri) + throw e.fullError // throw error h + } + // refresh now in case onText takes a while to get 1st message this._refreshStylesAndDiffsInURI(uri) @@ -1614,12 +1620,14 @@ class EditCodeService extends Disposable implements IEditCodeService { const addedTrackingZoneOfBlockNum: TrackingZone[] = [] diffZone._streamState.line = 1 - const featureName: FeatureName = 'Apply' const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName] const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[modelSelection.providerName]?.[modelSelection.modelName] : undefined - const retryLoop = async () => { + const N_RETRIES = 5 + + // allowed to throw errors - this is called inside a promise that handles everything + const runSearchReplace = async () => { // this generates >>>>>>> ORIGINAL <<<<<<< REPLACE blocks and and simultaneously applies it let shouldSendAnotherMessage = true let nMessagesSent = 0 @@ -1629,10 +1637,12 @@ class EditCodeService extends Disposable implements IEditCodeService { while (shouldSendAnotherMessage) { shouldSendAnotherMessage = false nMessagesSent += 1 - if (nMessagesSent >= 5) { - this._notifyError({ message: 'Tried to Fast Apply 5 times but failed. Please try again with a smarter model or disable Fast Apply.', fullError: null }) - onDone() - this._undoHistory(uri) + if (nMessagesSent >= N_RETRIES) { + const e = { + message: `Tried to Fast Apply ${N_RETRIES} times but failed. This may be related to model intelligence, or it may an edit that's too complex. Please retry or disable Fast Apply.`, + fullError: null + } + onError(e) break } @@ -1693,7 +1703,7 @@ class EditCodeService extends Disposable implements IEditCodeService { console.log('fullText', { fullText }) console.log('error:', originalBounds) console.log('block.orig:', block.orig) - const content = errMsgOfInvalidStr(originalBounds, block.orig, blockNum, blocks) + const content = errContentOfInvalidStr(originalBounds, block.orig, blockNum, blocks) messages.push( { role: 'assistant', content: fullText, anthropicReasoning: null }, // latest output { role: 'user', content: content } // user explanation of what's wrong @@ -1769,8 +1779,6 @@ class EditCodeService extends Disposable implements IEditCodeService { // diffZone._streamState.line = currentEndLine diffZone._streamState.line = latestStreamLocationMutable.line - - } // end for this._refreshStylesAndDiffsInURI(uri) @@ -1822,41 +1830,45 @@ class EditCodeService extends Disposable implements IEditCodeService { resMessageDonePromise() }, onError: (e) => { - this._notifyError(e) - onDone() - this._undoHistory(uri) - resMessageDonePromise() + onError(e) }, onAbort: () => { if (weAreAborting) return // stop the loop to free up the promise, but don't modify state (already handled by whatever stopped it) - resMessageDonePromise() aborted = true + resMessageDonePromise() }, }) // should never happen, just for safety if (streamRequestIdRef.current === null) { break } - console.log('awaiting...') await messageDonePromise - console.log('done awaiting, aborted=', aborted) - if (aborted) { return } - + if (aborted) { + throw new Error(`Edit was interrupted by the user.`) + } } // end while } // end retryLoop - retryLoop().then(() => { - console.log('resolving Apply Done') - resApplyDonePromise() - // this._noLongerNeedModelReference(uri) - }).catch((e) => rejApplyDonePromise(e)) - + const applyDonePromise = runSearchReplace() return [diffZone, applyDonePromise] } + _undoHistory(uri: URI) { + this._undoRedoService.undo(uri) + } + + + + isCtrlKZoneStreaming({ diffareaid }: { diffareaid: number }) { + const ctrlKZone = this.diffAreaOfId[diffareaid] + if (!ctrlKZone) return false + if (ctrlKZone.type !== 'CtrlKZone') return false + return !!ctrlKZone._linkedStreamingDiffZone + } + private _stopIfStreaming(diffZone: DiffZone) { const uri = diffZone._URI @@ -1870,31 +1882,6 @@ class EditCodeService extends Disposable implements IEditCodeService { this._onDidChangeStreamingInDiffZone.fire({ uri, diffareaid: diffZone.diffareaid }) } - _undoHistory(uri: URI) { - this._undoRedoService.undo(uri) - } - - - - - - _interruptSingleDiffZoneStreaming({ diffareaid }: { diffareaid: number }) { - const diffZone = this.diffAreaOfId[diffareaid] - if (diffZone?.type !== 'DiffZone') return - if (!diffZone._streamState.isStreaming) return - - this._stopIfStreaming(diffZone) - this._undoHistory(diffZone._URI) - } - - - isCtrlKZoneStreaming({ diffareaid }: { diffareaid: number }) { - const ctrlKZone = this.diffAreaOfId[diffareaid] - if (!ctrlKZone) return false - if (ctrlKZone.type !== 'CtrlKZone') return false - return !!ctrlKZone._linkedStreamingDiffZone - } - // diffareaid of the ctrlKZone (even though the stream state is dictated by the linked diffZone) interruptCtrlKStreaming({ diffareaid }: { diffareaid: number }) { @@ -1906,7 +1893,8 @@ class EditCodeService extends Disposable implements IEditCodeService { if (!linkedStreamingDiffZone) return if (linkedStreamingDiffZone.type !== 'DiffZone') return - this._interruptSingleDiffZoneStreaming({ diffareaid: linkedStreamingDiffZone.diffareaid }) + this._stopIfStreaming(linkedStreamingDiffZone) + this._undoHistory(linkedStreamingDiffZone._URI) } 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 bff5576a..0927ab5f 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 @@ -159,15 +159,13 @@ export const useApplyButtonHTML = ({ codeStr, applyBoxId, uri }: { codeStr: stri const onClickSubmit = useCallback(async () => { if (isDisabled) return if (getStreamState() === 'streaming') return - const [newApplyingUri, _] = await editCodeService.startApplying({ + const [newApplyingUri, applyDonePromise] = await editCodeService.startApplying({ from: 'ClickApply', applyStr: codeStr, uri: uri, startBehavior: 'keep-conflicts', }) ?? [] - if (!newApplyingUri) console.log('NOT new applying') - applyingURIOfApplyBoxIdRef.current[applyBoxId] = newApplyingUri ?? undefined rerender(c => c + 1) diff --git a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx index 205ef3a9..d6ed8605 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx @@ -63,7 +63,7 @@ export const QuickEditChat = ({ if (isStreamingRef.current) return textAreaFnsRef.current?.disable() - editCodeService.startApplying({ + const res = editCodeService.startApplying({ from: 'QuickEdit', diffareaid, startBehavior: 'keep-conflicts', 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 dc776649..99d6bf62 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 @@ -1040,7 +1040,7 @@ const ProseWrapper = ({ children }: { children: React.ReactNode }) => { {children}
    } -const AssistantMessageComponent = ({ chatMessage, isCommitted, messageIdx, isLast }: { chatMessage: ChatMessage & { role: 'assistant' }, messageIdx: number, isCommitted: boolean, isLast: boolean, }) => { +const AssistantMessageComponent = ({ chatMessage, isCommitted, messageIdx, isLast, chatIsRunning }: { chatMessage: ChatMessage & { role: 'assistant' }, messageIdx: number, isCommitted: boolean, isLast: boolean, chatIsRunning: IsRunningType }) => { const accessor = useAccessor() const chatThreadsService = accessor.get('IChatThreadService') @@ -1058,7 +1058,7 @@ const AssistantMessageComponent = ({ chatMessage, isCommitted, messageIdx, isLas } const isEmpty = !chatMessage.content && !chatMessage.reasoning - const isLastAndLoading = !isCommitted && isLast + const isLastAndLoading = !isCommitted && isLast && (chatIsRunning === 'message' || chatIsRunning === 'awaiting_user') if (isEmpty && !isLastAndLoading) return null return <> @@ -1770,6 +1770,7 @@ const ChatBubble = ({ chatMessage, isCommitted, messageIdx, isLast, chatIsRunnin chatMessage={chatMessage} messageIdx={messageIdx} isCommitted={isCommitted} + chatIsRunning={chatIsRunning} isLast={isLast} /> } @@ -1893,7 +1894,7 @@ export const SidebarChat = () => { const previousMessagesHTML = useMemo(() => { const threadId = currentThread.id return previousMessages.map((message, i) => { - const isLast = i === numMessages - 1 + const isLast = i === numMessages - 1 && (isRunning === 'tool' || isRunning === 'awaiting_user') return { isLast={isLast} threadId={threadId} /> - } - ) + }) }, [previousMessages, isRunning, currentThread, numMessages]) const threadId = currentThread.id diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx index 407305cd..b052ae66 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx @@ -48,16 +48,9 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { // changes if the user clicks left/right or if the user goes on a uri with changes const [currUriIdx, setUriIdx] = useState(null) - const [currUriHasChanges, setCurrUriHasChanges] = useState(false) useEffect(() => { const i = sortedCommandBarURIs.findIndex(e => e.fsPath === uri?.fsPath) - if (i !== -1) { - setUriIdx(i) - setCurrUriHasChanges(true) - } - else { - setCurrUriHasChanges(false) - } + if (i !== -1) { setUriIdx(i) } }, [sortedCommandBarURIs, uri]) const getNextDiffIdx = (step: 1 | -1) => { @@ -131,6 +124,9 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { const isADiffZoneInThisFile = sortedDiffZoneIds.length !== 0 const isADiffZoneInAnyFile = sortedCommandBarURIs.length !== 0 + const streamState = uri ? commandBarService.getStreamState(uri) : null + const showAcceptRejectAll = streamState === 'idle-has-changes' + if (!isADiffZoneInAnyFile) { // no changes for the user to accept return null @@ -303,7 +299,7 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { // dummy container due to annoyances with VS Code mounting the widget return
    - {currUriHasChanges && <> + {showAcceptRejectAll && <>
    {acceptAllButton} {rejectAllButton} diff --git a/src/vs/workbench/contrib/void/browser/terminalToolService.ts b/src/vs/workbench/contrib/void/browser/terminalToolService.ts index e8d1edcd..c9cbd178 100644 --- a/src/vs/workbench/contrib/void/browser/terminalToolService.ts +++ b/src/vs/workbench/contrib/void/browser/terminalToolService.ts @@ -9,7 +9,7 @@ import { registerSingleton, InstantiationType } from '../../../../platform/insta import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { TerminalLocation } from '../../../../platform/terminal/common/terminal.js'; import { ITerminalService, ITerminalInstance } from '../../../../workbench/contrib/terminal/browser/terminal.js'; -import { ResolveReason } from '../common/toolsServiceTypes.js'; +import { TerminalResolveReason } from '../common/toolsServiceTypes.js'; import { MAX_TERMINAL_CHARS_PAGE, TERMINAL_BG_WAIT_TIME, TERMINAL_TIMEOUT_TIME } from './toolsService.js'; @@ -18,7 +18,7 @@ export interface ITerminalToolService { readonly _serviceBrand: undefined; listTerminalIds(): string[]; - runCommand(command: string, proposedTerminalId: string, waitForCompletion: boolean): Promise<{ terminalId: string, didCreateTerminal: boolean, result: string, resolveReason: ResolveReason }>; + runCommand(command: string, proposedTerminalId: string, waitForCompletion: boolean): Promise<{ terminalId: string, didCreateTerminal: boolean, result: string, resolveReason: TerminalResolveReason }>; openTerminal(terminalId: string): Promise terminalExists(terminalId: string): boolean } @@ -162,7 +162,7 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ await this.terminalService.focusActiveInstance() let result: string = '' - let resolveReason: ResolveReason | undefined = undefined + let resolveReason: TerminalResolveReason | undefined = undefined const disposables: IDisposable[] = [] diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index cdc27fe7..271322d5 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -20,7 +20,7 @@ import { basename } from '../../../../base/common/path.js' type ValidateParams = { [T in ToolName]: (p: string) => Promise } -type CallTool = { [T in ToolName]: (p: ToolCallParams[T]) => Promise<{ result: ToolResultType[T], cancel?: () => void }> } +type CallTool = { [T in ToolName]: (p: ToolCallParams[T]) => Promise<{ result: ToolResultType[T], interruptTool?: () => void }> } type ToolResultToString = { [T in ToolName]: (p: ToolCallParams[T], result: ToolResultType[T]) => string } @@ -357,9 +357,10 @@ export class ToolsService implements IToolsService { if (!res) throw new Error(`The Apply model did not start running on ${basename(uri.fsPath)}. Please try again.`) const [diffZoneURI, applyDonePromise] = res - const cancel = () => editCodeService.interruptURIStreaming({ uri: diffZoneURI }) - - return { result: applyDonePromise, cancel } + const interruptTool = () => { // must reject the applyPromiseDone promise + editCodeService.interruptURIStreaming({ uri: diffZoneURI }) + } + return { result: applyDonePromise, interruptTool } }, terminal_command: async ({ command, proposedTerminalId, waitForCompletion }) => { const { terminalId, didCreateTerminal, result, resolveReason } = await this.terminalToolService.runCommand(command, proposedTerminalId, waitForCompletion) @@ -393,6 +394,7 @@ export class ToolsService implements IToolsService { return `URI ${params.uri.fsPath} successfully deleted.` }, edit: (params, result) => { + console.log('STR OF RESULT', params) return `Change successfully made to ${params.uri.fsPath}.` }, terminal_command: (params, result) => { diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index d5042dc9..d6c05f13 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -10,17 +10,137 @@ import { CodeSelection, FileSelection, StagingSelectionItem } from '../chatThrea import { ChatMode } from '../voidSettingsTypes.js'; import { IVoidModelService } from '../voidModelService.js'; import { EndOfLinePreference } from '../../../../../editor/common/model.js'; +import { InternalToolInfo } from '../toolsServiceTypes.js'; // this is just for ease of readability export const tripleTick = ['```', '```'] -export const editToolDesc_toolDescription = `\ -Your description should be of the form:\n${tripleTick[0]}\n// ... existing code ...\n{{change 1}}\n// ... existing code ...\n{{change2}}\n// ... existing code ...\n{{change 3}}\n...\n${tripleTick[1]}. \ -Do NOT re-write the whole file, and instead use comments like // ... existing code ... . Write as little as possible. \ -Your description will be handed to a dumber, faster model that will quickly apply the change, so try to be brief, but also make sure to include enough information to accurately describe the change. \ -Include the triple ticks in your output.` +const changesExampleContent = `\ +// ... existing code ... +// {{change 1}} +// // ... existing code ... +// {{change 2}} +// // ... existing code ... +// {{change 3}} +// ... existing code ...` +const editToolDescription = `\ +${tripleTick[0]} +${changesExampleContent} +${tripleTick[1]}` + +const fileNameEdit = `${tripleTick[0]}typescript +/Users/username/Dekstop/my_project/app.ts +${changesExampleContent} +${tripleTick[1]}` + + + + + +// ======================================================== tools ======================================================== + +const paginationHelper = { + desc: `Very large results may be paginated (indicated in the result). Pagination fails gracefully if out of bounds or invalid page number.`, + param: { pageNumber: { type: 'number', description: 'The page number (default is the first page = 1).' }, } +} as const + +export const voidTools = { + // --- context-gathering (read/search/list) --- + + read_file: { + name: 'read_file', + description: `Returns file contents of a given URI. ${paginationHelper.desc}`, + params: { + uri: { type: 'string', description: undefined }, + ...paginationHelper.param, + }, + }, + + list_dir: { + name: 'list_dir', + description: `Returns all file names and folder names in a given URI. ${paginationHelper.desc}`, + params: { + uri: { type: 'string', description: undefined }, + ...paginationHelper.param, + }, + }, + + pathname_search: { + name: 'pathname_search', + description: `Returns all pathnames that match a given grep query. You should use this when looking for a file with a specific name or path. This does NOT search file content. ${paginationHelper.desc}`, + params: { + query: { type: 'string', description: undefined }, + ...paginationHelper.param, + }, + }, + + text_search: { + name: 'text_search', + description: `Returns pathnames of files with an exact match of the query. The query can be any regex. This does NOT search pathname. As a follow-up, you may want to use read_file to view the full file contents of the results. ${paginationHelper.desc}`, + params: { + query: { type: 'string', description: undefined }, + ...paginationHelper.param, + }, + }, + + // --- editing (create/delete) --- + + create_uri: { + name: 'create_uri', + description: `Create a file or folder at the given path. To create a folder, ensure the path ends with a trailing slash. Fails gracefully if the file already exists. Missing ancestors in the path will be recursively created automatically.`, + params: { + uri: { type: 'string', description: undefined }, + }, + }, + + delete_uri: { + name: 'delete_uri', + description: `Delete a file or folder at the given path. Fails gracefully if the file or folder does not exist.`, + params: { + uri: { type: 'string', description: undefined }, + params: { type: 'string', description: 'Return -r here to delete this URI and all descendants (if applicable). Default is the empty string.' } + }, + }, + + edit: { // APPLY TOOL + name: 'edit', + description: `Edits the contents of a file, given the file's URI and a description. Fails gracefully if the file does not exist.`, + params: { + uri: { type: 'string', description: undefined }, + changeDescription: { + type: 'string', description: `\ +- Your changeDescription should be a brief code description of the change you want to make, with comments like "// ... existing code ..." to condense your writing. +- NEVER re-write the whole file, and ALWAYS use comments like "// ... existing code ...". Bias towards writing as little as possible. +- Your description will be handed to a dumber, faster model that will quickly apply the change, so it should be clear and concise. +- You must output your description in triple backticks. +Here's an example of a good description:\n${editToolDescription}.` + } + }, + }, + + terminal_command: { + name: 'terminal_command', + description: `Executes a terminal command.`, + params: { + command: { type: 'string', description: 'The terminal command to execute.' }, + waitForCompletion: { type: 'string', description: `Whether or not to await the command to complete and get the final result. Default is true. Make this value false when you want a command to run indefinitely without waiting for it.` }, + terminalId: { type: 'string', description: 'Optional (value must be an integer >= 1, or empty which will go with the default). This is the ID of the terminal instance to execute the command in. The primary purpose of this is to start a new terminal for background processes or tasks that run indefinitely (e.g. if you want to run a server locally). Fails gracefully if a terminal ID does not exist, by creating a new terminal instance. Defaults to the preferred terminal ID.' }, + }, + }, + + + // go_to_definition + // go_to_usages + +} satisfies { [name: string]: InternalToolInfo } + + + + + +// ======================================================== chat (normal, gather, agent) ======================================================== @@ -32,17 +152,21 @@ ${mode === 'agent' ? `to help the user develop, run, deploy, and make changes to : ''} You will be given instructions to follow from the user, \`INSTRUCTIONS\`. You may also be given a list of files that the user has specifically selected, \`SELECTIONS\`. Please assist the user with their query. The user's query is never invalid. - +${/* system info */''} The user's system information is as follows: - ${os} - Open workspace(s): ${workspaces.join(', ') || 'NO WORKSPACE OPEN'} ${(mode === 'agent') && runningTerminalIds.length !== 0 ? `\ - Existing terminal IDs: ${runningTerminalIds.join(', ')} `: '\n'} -${mode === 'agent' || mode === 'gather' /* tool use */ ? `\ +${/* tool use */ mode === 'agent' || mode === 'gather' ? `\ You will be given tools you can call. +${mode === 'agent' ? `\ - Only use tools if they help you accomplish the user's goal. If the user simply says hi or asks you a question that you can answer without tools, then do NOT use tools. -- ALWAYS use tools to take actions. For example, if you would like to edit a file, you MUST use a tool. +- ALWAYS use tools to take actions. For example, if you would like to edit a file, you MUST use a tool.` + : mode === 'gather' ? `\ +- Your primary use of tools should be to gather information to help the user understand the codebase and answer their query.` + : ''} - If you think you should use tools, you do not need to ask for permission. Feel free to call tools whenever you'd like. You can use them to understand the codebase, ${mode === 'agent' ? 'run terminal commands, edit files, ' : 'gather relevant files and information, '}etc. - NEVER refer to a tool by name when speaking with the user (NEVER say something like "I'm going to use \`tool_name\`"). Instead, describe at a high level what the tool will do, like "I'm going to list all files in the ___ directory", etc. Also do not refer to "pages" of results, just say you're getting more results. - Some tools only work if the user has a workspace open.${mode === 'agent' ? ` @@ -52,20 +176,23 @@ You will be given tools you can call. You're allowed to ask for more context. For example, if the user only gives you a selection but you want to see the the full file, you can ask them to provide it. \ `} -${mode === 'agent' /* code blocks */ ? `\ +${/* code blocks */ mode === 'agent' ? `\ Behavior: -- Prioritize editing files and running commands over simply making suggestions. +- Always use tools (edit, terminal, etc) to take actions and implement changes. Don't just describe them. - Prioritize taking as many steps as you need to complete your request over stopping early.\ `: `\ If you think it's appropriate to suggest an edit to a file, then you must describe your suggestion in CODE BLOCK(S) (wrapped in triple backticks). -- The first line of the code block must be the FULL PATH of the file you want to change. If the path does not already exist, it will be created. -- The remaining contents of the code block will be given to a dumber, faster model that will quickly apply the change. -- Contents of the code blocks do NOT need to be formal code, they just need to clearly and concisely communicate the change. -- Do NOT re-write the entire file in the code block(s). Instead, write comments like "// ... existing code" to indicate how to change the existing code.`} - +- The first line of the code block must be the FULL PATH of the file you want to change. +- The remaining contents should be a brief code description of the change you want to make, with comments like "// ... existing code ..." to condense your writing. +- NEVER re-write the whole file, and ALWAYS use comments like "// ... existing code ...". Bias towards writing as little as possible. +- Your description will be handed to a dumber, faster model that will quickly apply the change, so it should be clear and concise. +Here's an example of a good code block:\n${fileNameEdit}.\ +`} +${/* misc */''} Misc: - Do not make things up. - Do not be lazy. +- NEVER re-write the entire file. - Always wrap any code you produce in triple backticks, and specify a language if possible. For example, ${tripleTick[0]}typescript\n...\n${tripleTick[1]}.\ ` // agent mode doesn't know about 1st line paths yet @@ -181,6 +308,7 @@ Directions: +// ======================================================== apply (writeover) ======================================================== export const rewriteCode_userMessage = ({ originalCode, applyStr, language }: { originalCode: string, applyStr: string, language: string }) => { @@ -202,69 +330,7 @@ Please finish writing the new file by applying the change to the original file. - - - -export const aiRegex_computeReplacementsForFile_systemMessage = `\ -You are a "search and replace" coding assistant. - -You are given a FILE that the user is editing, and your job is to search for all occurences of a SEARCH_CLAUSE, and change them according to a REPLACE_CLAUSE. - -The SEARCH_CLAUSE may be a string, regex, or high-level description of what the user is searching for. - -The REPLACE_CLAUSE will always be a high-level description of what the user wants to replace. - -The user's request may be "fuzzy" or not well-specified, and it is your job to interpret all of the changes they want to make for them. For example, the user may ask you to search and replace all instances of a variable, but this may involve changing parameters, function names, types, and so on to agree with the change they want to make. Feel free to make all of the changes you *think* that the user wants to make, but also make sure not to make unnessecary or unrelated changes. - -## Instructions - -1. If you do not want to make any changes, you should respond with the word "no". - -2. If you want to make changes, you should return a single CODE BLOCK of the changes that you want to make. -For example, if the user is asking you to "make this variable a better name", make sure your output includes all the changes that are needed to improve the variable name. -- Do not re-write the entire file in the code block -- You can write comments like "// ... existing code" to indicate existing code -- Make sure you give enough context in the code block to apply the changes to the correct location in the code` - - -// export const aiRegex_computeReplacementsForFile_userMessage = async ({ searchClause, replaceClause, fileURI, voidFileService }: { searchClause: string, replaceClause: string, fileURI: URI, voidFileService: IVoidFileService }) => { - -// // we may want to do this in batches -// const fileSelection: FileSelection = { type: 'File', fileURI, selectionStr: null, range: null, state: { isOpened: false } } - -// const file = await stringifyFileSelections([fileSelection], voidFileService) - -// return `\ -// ## FILE -// ${file} - -// ## SEARCH_CLAUSE -// Here is what the user is searching for: -// ${searchClause} - -// ## REPLACE_CLAUSE -// Here is what the user wants to replace it with: -// ${replaceClause} - -// ## INSTRUCTIONS -// Please return the changes you want to make to the file in a codeblock, or return "no" if you do not want to make changes.` -// } - - - - -// // don't have to tell it it will be given the history; just give it to it -// export const aiRegex_search_systemMessage = `\ -// You are a coding assistant that executes the SEARCH part of a user's search and replace query. - -// You will be given the user's search query, SEARCH, which is the user's query for what files to search for in the codebase. You may also be given the user's REPLACE query for additional context. - -// Output -// - Regex query -// - Files to Include (optional) -// - Files to Exclude? (optional) - -// ` +// ======================================================== apply (fast apply - search/replace) ======================================================== @@ -387,6 +453,8 @@ export const voidPrefixAndSuffix = ({ fullFileStr, startLine, endLine }: { fullF } +// ======================================================== quick edit (ctrl+K) ======================================================== + export type QuickEditFimTagsType = { preTag: string, sufTag: string, @@ -444,10 +512,82 @@ ${tripleTick[1]}).` + + + /* +// ======================================================== ai search/replace ======================================================== -OLD CHAT EXAMPLES: +export const aiRegex_computeReplacementsForFile_systemMessage = `\ +You are a "search and replace" coding assistant. + +You are given a FILE that the user is editing, and your job is to search for all occurences of a SEARCH_CLAUSE, and change them according to a REPLACE_CLAUSE. + +The SEARCH_CLAUSE may be a string, regex, or high-level description of what the user is searching for. + +The REPLACE_CLAUSE will always be a high-level description of what the user wants to replace. + +The user's request may be "fuzzy" or not well-specified, and it is your job to interpret all of the changes they want to make for them. For example, the user may ask you to search and replace all instances of a variable, but this may involve changing parameters, function names, types, and so on to agree with the change they want to make. Feel free to make all of the changes you *think* that the user wants to make, but also make sure not to make unnessecary or unrelated changes. + +## Instructions + +1. If you do not want to make any changes, you should respond with the word "no". + +2. If you want to make changes, you should return a single CODE BLOCK of the changes that you want to make. +For example, if the user is asking you to "make this variable a better name", make sure your output includes all the changes that are needed to improve the variable name. +- Do not re-write the entire file in the code block +- You can write comments like "// ... existing code" to indicate existing code +- Make sure you give enough context in the code block to apply the changes to the correct location in the code` + + + + +// export const aiRegex_computeReplacementsForFile_userMessage = async ({ searchClause, replaceClause, fileURI, voidFileService }: { searchClause: string, replaceClause: string, fileURI: URI, voidFileService: IVoidFileService }) => { + +// // we may want to do this in batches +// const fileSelection: FileSelection = { type: 'File', fileURI, selectionStr: null, range: null, state: { isOpened: false } } + +// const file = await stringifyFileSelections([fileSelection], voidFileService) + +// return `\ +// ## FILE +// ${file} + +// ## SEARCH_CLAUSE +// Here is what the user is searching for: +// ${searchClause} + +// ## REPLACE_CLAUSE +// Here is what the user wants to replace it with: +// ${replaceClause} + +// ## INSTRUCTIONS +// Please return the changes you want to make to the file in a codeblock, or return "no" if you do not want to make changes.` +// } + + + + +// // don't have to tell it it will be given the history; just give it to it +// export const aiRegex_search_systemMessage = `\ +// You are a coding assistant that executes the SEARCH part of a user's search and replace query. + +// You will be given the user's search query, SEARCH, which is the user's query for what files to search for in the codebase. You may also be given the user's REPLACE query for additional context. + +// Output +// - Regex query +// - Files to Include (optional) +// - Files to Exclude? (optional) + +// ` + + + + + + +// ======================================================== old examples ======================================================== Do not tell the user anything about the examples below. Do not assume the user is talking about any of the examples below. diff --git a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts index 0b6f7ef3..adefa286 100644 --- a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts @@ -1,5 +1,16 @@ import { URI } from '../../../../base/common/uri.js' -import { editToolDesc_toolDescription } from './prompt/prompts.js'; +import { voidTools } from './prompt/prompts.js'; + + +export type ToolDirectoryItem = { + uri: URI; + name: string; + isDirectory: boolean; + isSymbolicLink: boolean; +} + + +export type TerminalResolveReason = { type: 'toofull' | 'timeout' | 'bgtask' } | { type: 'done', exitCode: number } @@ -15,109 +26,6 @@ export type InternalToolInfo = { - -export type ToolDirectoryItem = { - uri: URI; - name: string; - isDirectory: boolean; - isSymbolicLink: boolean; -} - - -export type ResolveReason = { type: 'toofull' | 'timeout' | 'bgtask' } | { type: 'done', exitCode: number } - - - - - -const paginationHelper = { - desc: `Very large results may be paginated (indicated in the result). Pagination fails gracefully if out of bounds or invalid page number.`, - param: { pageNumber: { type: 'number', description: 'The page number (default is the first page = 1).' }, } -} as const - -export const voidTools = { - // --- context-gathering (read/search/list) --- - - read_file: { - name: 'read_file', - description: `Returns file contents of a given URI. ${paginationHelper.desc}`, - params: { - uri: { type: 'string', description: undefined }, - ...paginationHelper.param, - }, - }, - - list_dir: { - name: 'list_dir', - description: `Returns all file names and folder names in a given URI. ${paginationHelper.desc}`, - params: { - uri: { type: 'string', description: undefined }, - ...paginationHelper.param, - }, - }, - - pathname_search: { - name: 'pathname_search', - description: `Returns all pathnames that match a given grep query. You should use this when looking for a file with a specific name or path. This does NOT search file content. ${paginationHelper.desc}`, - params: { - query: { type: 'string', description: undefined }, - ...paginationHelper.param, - }, - }, - - text_search: { - name: 'text_search', - description: `Returns pathnames of files with an exact match of the query. The query can be any regex. This does NOT search pathname. As a follow-up, you may want to use read_file to view the full file contents of the results. ${paginationHelper.desc}`, - params: { - query: { type: 'string', description: undefined }, - ...paginationHelper.param, - }, - }, - - // --- editing (create/delete) --- - - create_uri: { - name: 'create_uri', - description: `Create a file or folder at the given path. To create a folder, ensure the path ends with a trailing slash. Fails gracefully if the file already exists. Missing ancestors in the path will be recursively created automatically.`, - params: { - uri: { type: 'string', description: undefined }, - }, - }, - - delete_uri: { - name: 'delete_uri', - description: `Delete a file or folder at the given path. Fails gracefully if the file or folder does not exist.`, - params: { - uri: { type: 'string', description: undefined }, - params: { type: 'string', description: 'Return -r here to delete this URI and all descendants (if applicable). Default is the empty string.' } - }, - }, - - edit: { // APPLY TOOL - name: 'edit', - description: `Edits the contents of a file, given the file's URI and a description. Fails gracefully if the file does not exist.`, - params: { - uri: { type: 'string', description: undefined }, - changeDescription: { type: 'string', description: editToolDesc_toolDescription } // long description here - }, - }, - - terminal_command: { - name: 'terminal_command', - description: `Executes a terminal command.`, - params: { - command: { type: 'string', description: 'The terminal command to execute.' }, - waitForCompletion: { type: 'string', description: `Whether or not to await the command to complete and get the final result. Default is true. Make this value false when you want a command to run indefinitely without waiting for it.` }, - terminalId: { type: 'string', description: 'Optional (value must be an integer >= 1, or empty which will go with the default). This is the ID of the terminal instance to execute the command in. The primary purpose of this is to start a new terminal for background processes or tasks that run indefinitely (e.g. if you want to run a server locally). Fails gracefully if a terminal ID does not exist, by creating a new terminal instance. Defaults to the preferred terminal ID.' }, - }, - }, - - - // go_to_definition - // go_to_usages - -} satisfies { [name: string]: InternalToolInfo } - export type ToolName = keyof typeof voidTools export const toolNames = Object.keys(voidTools) as ToolName[] @@ -151,9 +59,9 @@ export type ToolResultType = { 'pathname_search': { uris: URI[], hasNextPage: boolean }, 'text_search': { uris: URI[], hasNextPage: boolean }, // --- - 'edit': {}, + 'edit': Promise, 'create_uri': {}, 'delete_uri': {}, - 'terminal_command': { terminalId: string, didCreateTerminal: boolean, result: string; resolveReason: ResolveReason; }, + 'terminal_command': { terminalId: string, didCreateTerminal: boolean, result: string; resolveReason: TerminalResolveReason; }, } From 90a282a1f7f1e0220315c402995bdb2f7bc2cf63 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 21 Mar 2025 02:53:02 -0700 Subject: [PATCH 141/173] minor prompt fix --- .../contrib/void/browser/chatThreadService.ts | 1 + .../contrib/void/browser/editCodeService.ts | 14 +++++++------- .../contrib/void/common/prompt/prompts.ts | 9 ++------- 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 2455bb44..861e1847 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -753,6 +753,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { { role: 'user', content: userMessageFullContent }, ...messages_.slice(lastUserMsgIdx + 1, Infinity), ] + // console.log('MESSAGES!!!', messages) return messages } diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index dba6870a..a89ffa50 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -1245,18 +1245,18 @@ class EditCodeService extends Disposable implements IEditCodeService { // ctrlkzone should never have any conflicts } else { - console.log('KEEPING CONFLICTS!!!!!!!!') + // console.log('KEEPING CONFLICTS!!!!!!!!') // keep conflict on whole file - to keep conflict, revert the change and use those contents as original, then un-revert the file - console.log('originalFile', originalFileStr) - console.log('diffareas A', JSON.stringify(this.diffAreasOfURI, null, 2)) + // console.log('originalFile', originalFileStr) + // console.log('diffareas A', JSON.stringify(this.diffAreasOfURI, null, 2)) this.acceptOrRejectAllDiffAreas({ uri, removeCtrlKs: true, behavior: 'reject', _addToHistory: false }) - console.log('diffareas B', JSON.stringify(this.diffAreasOfURI, null, 2)) + // console.log('diffareas B', JSON.stringify(this.diffAreasOfURI, null, 2)) const oldFileStr = model.getValue(EndOfLinePreference.LF) // use this as original code - console.log('oldFileStr', { oldFileStr }) + // console.log('oldFileStr', { oldFileStr }) this._writeURIText(uri, originalFileStr, 'wholeFileRange', { shouldRealignDiffAreas: true }) // un-revert originalCode = oldFileStr - console.log('originalCode', { originalCode }) - console.log('NEW STR', { newStr: model.getValue(EndOfLinePreference.LF) }) + // console.log('originalCode', { originalCode }) + // console.log('NEW STR', { newStr: model.getValue(EndOfLinePreference.LF) }) } } diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index d6c05f13..350eaa3b 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -282,13 +282,8 @@ export const chat_selectionsString = async ( const filesStr = await stringifyFileSelections(fileSelections, voidModelService) const selnsStr = stringifyCodeSelections(codeSelections) - - if (filesStr || selnsStr) return `\ -ALL FILE CONTENTS -${filesStr} -${selnsStr}` - - return null + const fileContents = [filesStr, selnsStr].filter(Boolean).join('\n') + return fileContents || null } export const chat_lastUserMessageWithFilesAdded = (userMessage: string, selectionsString: string | null) => { From eed71d95823630d64f5d5c7c77900ad0774ace33 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 21 Mar 2025 04:18:25 -0700 Subject: [PATCH 142/173] minor changes --- src/vs/workbench/contrib/void/browser/editCodeService.ts | 2 +- .../void/electron-main/llmMessage/preprocessLLMMessages.ts | 6 ------ .../contrib/void/electron-main/llmMessage/sendLLMMessage.ts | 4 ++-- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index a89ffa50..fc9f1496 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -739,7 +739,7 @@ class EditCodeService extends Disposable implements IEditCodeService { ) as DiffAreaSnapshot } - const entireFileCode = model ? model.getValue(EndOfLinePreference.LF) : '' // TODO!!! make sure this isn't '' usually + const entireFileCode = model ? model.getValue(EndOfLinePreference.LF) : '' // this._noLongerNeedModelReference(uri) return { diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts index 3ba8f383..ab9991c1 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts @@ -89,12 +89,6 @@ const prepareMessages_systemMessage = ({ const newMessages: (InternalLLMChatMessage | { role: 'developer', content: string })[] = messages.filter(msg => msg.role !== 'system') - // if (!supportsTools) { - // if (!systemMessageStr) systemMessageStr = '' - // systemMessageStr += '' // TODO!!! add tool use system message here - // } - - // if it has a system message (if doesn't, we obviously don't care about whether it supports system message or not...) if (systemMessageStr) { // if supports system message diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts index ad94bb72..87938d62 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts @@ -95,9 +95,9 @@ export const sendLLMMessage = ({ abortRef_.current = onAbort if (messagesType === 'chatMessages') - captureLLMEvent(`${loggingName} - Sending Message`, { messageLength: messages_[messages_.length - 1]?.content.length }) + captureLLMEvent(`${loggingName} - Sending Message`, { messageLength: messages_?.[messages_.length - 1]?.content.length }) else if (messagesType === 'FIMMessage') - captureLLMEvent(`${loggingName} - Sending FIM`, {}) // TODO!!! add more metrics + captureLLMEvent(`${loggingName} - Sending FIM`, { prefixLen: messages_?.prefix?.length, suffixLen: messages_?.suffix?.length }) // TODO!!! add more metrics for FIM try { From 6a6cb56d87a1184712b00ec782cb750868bcd64a Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 21 Mar 2025 05:50:31 -0700 Subject: [PATCH 143/173] add visual feedback for tool that's being loaded (eg edit tool) --- .../contrib/void/browser/chatThreadService.ts | 8 +- .../contrib/void/browser/editCodeService.ts | 22 ++++- .../react/src/sidebar-tsx/SidebarChat.tsx | 92 +++++++++++-------- .../common/helpers/extractCodeFromResult.ts | 12 +-- .../void/common/sendLLMMessageTypes.ts | 2 +- .../llmMessage/sendLLMMessage.impl.ts | 30 ++++-- 6 files changed, 109 insertions(+), 57 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 861e1847..6eed5b16 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -117,6 +117,8 @@ export type ThreadStreamState = { streamingToken?: string; messageSoFar?: string; reasoningSoFar?: string; + toolNameSoFar?: string; + toolParamsSoFar?: string; } } @@ -874,11 +876,13 @@ class ChatThreadService extends Disposable implements IChatThreadService { modelSelection, modelSelectionOptions, logging: { loggingName: `Chat - ${chatMode}`, loggingExtras: { threadId, nMessagesSent, chatMode } }, - onText: ({ fullText, fullReasoning }) => { this._setStreamState(threadId, { messageSoFar: fullText, reasoningSoFar: fullReasoning }, 'merge') }, + onText: ({ fullText, fullReasoning, fullToolName, fullToolParams }) => { + this._setStreamState(threadId, { messageSoFar: fullText, reasoningSoFar: fullReasoning, toolNameSoFar: fullToolName, toolParamsSoFar: fullToolParams }, 'merge') + }, onFinalMessage: async ({ fullText, toolCalls, fullReasoning, anthropicReasoning }) => { this._addMessageToThread(threadId, { role: 'assistant', content: fullText, reasoning: fullReasoning, anthropicReasoning }) // added to history and no longer streaming this, so clear messages so far and streamingToken (but do not stop isRunning) - this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, }, 'merge') + this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, toolNameSoFar: undefined, toolParamsSoFar: undefined }, 'merge') // resolve with tool calls resMessageIsDonePromise(toolCalls) }, diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index fc9f1496..abb0bf24 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -1695,14 +1695,17 @@ class EditCodeService extends Disposable implements IEditCodeService { // if this is the first time we're seeing this block, add it as a diffarea so we can start streaming in it if (!(blockNum in addedTrackingZoneOfBlockNum)) { + + const originalBounds = findTextInCode(block.orig, originalFileCode) // if error if (typeof originalBounds === 'string') { - console.log('Error finding text in code:') + console.log('--------------Error finding text in code:') console.log('originalFileCode', { originalFileCode }) console.log('fullText', { fullText }) console.log('error:', originalBounds) console.log('block.orig:', block.orig) + console.log('---------') const content = errContentOfInvalidStr(originalBounds, block.orig, blockNum, blocks) messages.push( { role: 'assistant', content: fullText, anthropicReasoning: null }, // latest output @@ -1710,10 +1713,14 @@ class EditCodeService extends Disposable implements IEditCodeService { ) // 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 @@ -1729,7 +1736,14 @@ class EditCodeService extends Disposable implements IEditCodeService { return } + console.log('---------adding-------') + console.log('CURRENT TEXT!!!', { current: model?.getValue() }) + console.log('block', deepClone(block)) + console.log('origBounds', originalBounds) + + const [startLine, endLine] = convertOriginalRangeToFinalRange(originalBounds) + console.log('start end', startLine, endLine) // otherwise if no error, add the position as a diffarea const adding: Omit, 'diffareaid'> = { @@ -1802,9 +1816,9 @@ class EditCodeService extends Disposable implements IEditCodeService { addedTrackingZoneOfBlockNum.sort((a, b) => a.metadata.originalBounds[0] - b.metadata.originalBounds[0]) const { model } = this._voidModelService.getModel(uri) - console.log('CURRENT!!!', { current: model?.getValue() }) - console.log('ADDED', addedTrackingZoneOfBlockNum) - console.log('BLOX', blocks) + 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 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 99d6bf62..f887dea6 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 @@ -27,7 +27,7 @@ import { WarningBox } from '../void-settings-tsx/WarningBox.js'; import { getModelCapabilities, getIsResoningEnabledState } from '../../../../common/modelCapabilities.js'; import { AlertTriangle, Ban, ChevronRight, Dot, Pencil, X } from 'lucide-react'; import { ChatMessage, StagingSelectionItem, ToolMessage, ToolRequestApproval } from '../../../../common/chatThreadServiceTypes.js'; -import { ToolCallParams, ToolName, ToolNameWithApproval } from '../../../../common/toolsServiceTypes.js'; +import { ToolCallParams, ToolName, toolNames, ToolNameWithApproval } from '../../../../common/toolsServiceTypes.js'; import { JumpToFileButton, useApplyButtonHTML } from '../markdown/ApplyBlockHoverButtons.js'; import { IsRunningType } from '../../../chatThreadService.js'; @@ -1040,7 +1040,7 @@ const ProseWrapper = ({ children }: { children: React.ReactNode }) => { {children}
    } -const AssistantMessageComponent = ({ chatMessage, isCommitted, messageIdx, isLast, chatIsRunning }: { chatMessage: ChatMessage & { role: 'assistant' }, messageIdx: number, isCommitted: boolean, isLast: boolean, chatIsRunning: IsRunningType }) => { +const AssistantMessageComponent = ({ chatMessage, isCommitted, messageIdx, isLast, chatIsRunning, isToolBeingWritten }: { chatMessage: ChatMessage & { role: 'assistant' }, messageIdx: number, isCommitted: boolean, isLast: boolean, chatIsRunning: IsRunningType, isToolBeingWritten: boolean }) => { const accessor = useAccessor() const chatThreadsService = accessor.get('IChatThreadService') @@ -1058,7 +1058,8 @@ const AssistantMessageComponent = ({ chatMessage, isCommitted, messageIdx, isLas } const isEmpty = !chatMessage.content && !chatMessage.reasoning - const isLastAndLoading = !isCommitted && isLast && (chatIsRunning === 'message' || chatIsRunning === 'awaiting_user') + const isLoading = !isCommitted && !isToolBeingWritten && (chatIsRunning === 'message' || chatIsRunning === 'awaiting_user') + const isLastAndLoading = isLast && isLoading if (isEmpty && !isLastAndLoading) return null return <> @@ -1083,7 +1084,7 @@ const AssistantMessageComponent = ({ chatMessage, isCommitted, messageIdx, isLas isLinkDetectionEnabled={true} /> {/* loading indicator */} - {!isCommitted && } + {isLoading && } @@ -1117,19 +1118,19 @@ const loadingTitleWrapper = (item: React.ReactNode) => { } const folderFileStr = (isFolder: boolean) => isFolder ? 'folder' : 'file' -const toolNameToTitle = { +const titleOfToolName = { 'read_file': { done: 'Read file', proposed: 'Read file', running: loadingTitleWrapper('Reading file') }, 'list_dir': { done: 'Inspected folder', proposed: 'Inspect folder', running: loadingTitleWrapper('Inspecting folder') }, 'pathname_search': { done: 'Searched by file name', proposed: 'Search by file name', running: loadingTitleWrapper('Searching by file name') }, 'text_search': { done: 'Searched', proposed: 'Search text', running: loadingTitleWrapper('Searching') }, 'create_uri': { done: (isFolder: boolean) => `Created ${folderFileStr(isFolder)}`, - proposed: (isFolder: boolean) => `Create ${folderFileStr(isFolder)}`, + proposed: (isFolder: boolean | null) => isFolder === null ? 'Create URI' : `Create ${folderFileStr(isFolder)}`, running: (isFolder: boolean) => loadingTitleWrapper(`Creating ${folderFileStr(isFolder)}`) }, 'delete_uri': { done: (isFolder: boolean) => `Deleted ${folderFileStr(isFolder)}`, - proposed: (isFolder: boolean) => `Delete ${folderFileStr(isFolder)}`, + proposed: (isFolder: boolean | null) => isFolder === null ? 'Delete URI' : `Delete ${folderFileStr(isFolder)}`, running: (isFolder: boolean) => loadingTitleWrapper(`Deleting ${folderFileStr(isFolder)}`) }, 'edit': { done: `Edited file`, proposed: 'Edit file', running: loadingTitleWrapper('Editing file') }, @@ -1259,7 +1260,7 @@ export const ToolChildrenWrapper = ({ children, className }: { children: React.R
    } -export const ErrorChildren = ({ children }: { children: React.ReactNode }) => { +export const CodeChildren = ({ children }: { children: React.ReactNode }) => { return
    {children} @@ -1315,7 +1316,7 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') - const title = toolMessage.result.type === 'success' ? toolNameToTitle[toolMessage.name].done : toolNameToTitle[toolMessage.name].proposed + const title = toolMessage.result.type === 'success' ? titleOfToolName[toolMessage.name].done : titleOfToolName[toolMessage.name].proposed const { uri } = toolMessage.result.params ?? {} const desc1 = uri ? getBasename(uri.fsPath) : ''; const icon = null @@ -1334,9 +1335,9 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { const { value, params } = toolMessage.result if (params) componentParams.desc2 = componentParams.children = - + {value} - + } @@ -1349,7 +1350,7 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { const accessor = useAccessor() const commandService = accessor.get('ICommandService') const explorerService = accessor.get('IExplorerService') - const title = toolMessage.result.type === 'success' ? toolNameToTitle[toolMessage.name].done : toolNameToTitle[toolMessage.name].proposed + const title = toolMessage.result.type === 'success' ? titleOfToolName[toolMessage.name].done : titleOfToolName[toolMessage.name].proposed const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params) const icon = null @@ -1381,9 +1382,9 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { else { const { value, params } = toolMessage.result componentParams.children = - + {value} - + } @@ -1396,7 +1397,7 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { const accessor = useAccessor() const commandService = accessor.get('ICommandService') const isError = toolMessage.result.type === 'error' - const title = toolMessage.result.type === 'success' ? toolNameToTitle[toolMessage.name].done : toolNameToTitle[toolMessage.name].proposed + const title = toolMessage.result.type === 'success' ? titleOfToolName[toolMessage.name].done : titleOfToolName[toolMessage.name].proposed const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params) const icon = null @@ -1424,9 +1425,9 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { else { const { value, params } = toolMessage.result componentParams.children = - + {value} - + } @@ -1439,7 +1440,7 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { const accessor = useAccessor() const commandService = accessor.get('ICommandService') const isError = toolMessage.result.type === 'error' - const title = toolMessage.result.type === 'success' ? toolNameToTitle[toolMessage.name].done : toolNameToTitle[toolMessage.name].proposed + const title = toolMessage.result.type === 'success' ? titleOfToolName[toolMessage.name].done : titleOfToolName[toolMessage.name].proposed const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params) const icon = null @@ -1467,9 +1468,9 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { else { const { value, params } = toolMessage.result componentParams.children = - + {value} - + } return @@ -1485,7 +1486,7 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { const explorerService = accessor.get('IExplorerService') const isError = false const isFolder = toolRequest.params.isFolder - const title = toolRequestState === 'awaiting_user' ? toolNameToTitle[toolRequest.name].proposed(isFolder) : toolNameToTitle[toolRequest.name].running(isFolder) + const title = toolRequestState === 'awaiting_user' ? titleOfToolName[toolRequest.name].proposed(isFolder) : titleOfToolName[toolRequest.name].running(isFolder) const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params) const icon = null @@ -1499,7 +1500,7 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { const isError = toolMessage.result.type === 'error' const isRejected = toolMessage.result.type === 'rejected' const isFolder = toolMessage.result.params?.isFolder ?? false - const title = toolMessage.result.type === 'success' ? toolNameToTitle[toolMessage.name].done(isFolder) : toolNameToTitle[toolMessage.name].proposed(isFolder) + const title = toolMessage.result.type === 'success' ? titleOfToolName[toolMessage.name].done(isFolder) : titleOfToolName[toolMessage.name].proposed(isFolder) const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params) const icon = null @@ -1517,9 +1518,9 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { const { params, value } = toolMessage.result if (params) { componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } } componentParams.children = componentParams.children = - + {value} - + } @@ -1532,7 +1533,7 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { const commandService = accessor.get('ICommandService') const isError = false const isFolder = toolRequest.params.isFolder - const title = toolRequestState === 'awaiting_user' ? toolNameToTitle[toolRequest.name].proposed(isFolder) : toolNameToTitle[toolRequest.name].running(isFolder) + const title = toolRequestState === 'awaiting_user' ? titleOfToolName[toolRequest.name].proposed(isFolder) : titleOfToolName[toolRequest.name].running(isFolder) const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params) const icon = null @@ -1549,7 +1550,7 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { const isFolder = toolMessage.result.params?.isFolder ?? false const isError = toolMessage.result.type === 'error' const isRejected = toolMessage.result.type === 'rejected' - const title = toolMessage.result.type === 'success' ? toolNameToTitle[toolMessage.name].done(isFolder) : toolNameToTitle[toolMessage.name].proposed(isFolder) + const title = toolMessage.result.type === 'success' ? titleOfToolName[toolMessage.name].done(isFolder) : titleOfToolName[toolMessage.name].proposed(isFolder) const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params) const icon = null @@ -1567,9 +1568,9 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { const { params, value } = toolMessage.result if (params) { componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } } componentParams.children = componentParams.children = - + {value} - + } @@ -1580,7 +1581,7 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { requestWrapper: ({ toolRequest, messageIdx, toolRequestState, threadId }) => { const accessor = useAccessor() const isError = false - const title = toolRequestState === 'awaiting_user' ? toolNameToTitle[toolRequest.name].proposed : toolNameToTitle[toolRequest.name].running + const title = toolRequestState === 'awaiting_user' ? titleOfToolName[toolRequest.name].proposed : titleOfToolName[toolRequest.name].running const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params) const icon = null @@ -1602,7 +1603,7 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { const accessor = useAccessor() const isError = toolMessage.result.type === 'error' const isRejected = toolMessage.result.type === 'rejected' - const title = toolMessage.result.type === 'success' ? toolNameToTitle[toolMessage.name].done : toolNameToTitle[toolMessage.name].proposed + const title = toolMessage.result.type === 'success' ? titleOfToolName[toolMessage.name].done : titleOfToolName[toolMessage.name].proposed const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params) const icon = null @@ -1641,9 +1642,9 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { if (params) { componentParams.children = {/* error */} - + {value} - + {/* content */} } = { } else { - componentParams.children = + componentParams.children = {value} - + } } } @@ -1669,7 +1670,7 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { const commandService = accessor.get('ICommandService') const terminalToolsService = accessor.get('ITerminalToolService') const isError = false - const title = toolRequestState === 'awaiting_user' ? toolNameToTitle[toolRequest.name].proposed : toolNameToTitle[toolRequest.name].running + const title = toolRequestState === 'awaiting_user' ? titleOfToolName[toolRequest.name].proposed : titleOfToolName[toolRequest.name].running const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params) const icon = null @@ -1688,7 +1689,7 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { const commandService = accessor.get('ICommandService') const terminalToolsService = accessor.get('ITerminalToolService') const isError = toolMessage.result.type === 'error' - const title = toolMessage.result.type === 'success' ? toolNameToTitle[toolMessage.name].done : toolNameToTitle[toolMessage.name].proposed + const title = toolMessage.result.type === 'success' ? titleOfToolName[toolMessage.name].done : titleOfToolName[toolMessage.name].proposed const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params) const icon = null @@ -1753,9 +1754,10 @@ type ChatBubbleProps = { isLast: boolean, // includes the streaming message (if streaming, isLast is false except for the streaming message) chatIsRunning: IsRunningType, threadId: string, + isToolBeingWritten: boolean, } -const ChatBubble = ({ chatMessage, isCommitted, messageIdx, isLast, chatIsRunning, threadId }: ChatBubbleProps) => { +const ChatBubble = ({ chatMessage, isCommitted, messageIdx, isLast, chatIsRunning, threadId, isToolBeingWritten }: ChatBubbleProps) => { const role = chatMessage.role if (role === 'user') { @@ -1772,6 +1774,7 @@ const ChatBubble = ({ chatMessage, isCommitted, messageIdx, isLast, chatIsRunnin isCommitted={isCommitted} chatIsRunning={chatIsRunning} isLast={isLast} + isToolBeingWritten={isToolBeingWritten} /> } else if (role === 'tool_request') { @@ -1838,6 +1841,10 @@ export const SidebarChat = () => { const messageSoFar = currThreadStreamState?.messageSoFar const reasoningSoFar = currThreadStreamState?.reasoningSoFar + const toolNameSoFar = currThreadStreamState?.toolNameSoFar + const toolParamsSoFar = currThreadStreamState?.toolParamsSoFar + const toolIsLoading = !!toolNameSoFar && toolNameSoFar === 'edit' // show loading for slow tools (right now just edit) + // ----- SIDEBAR CHAT state (local) ----- // state of current message @@ -1902,6 +1909,7 @@ export const SidebarChat = () => { chatIsRunning={isRunning} isLast={isLast} threadId={threadId} + isToolBeingWritten={toolIsLoading} /> }) }, [previousMessages, isRunning, currentThread, numMessages]) @@ -1921,9 +1929,17 @@ export const SidebarChat = () => { chatIsRunning={isRunning} isLast={true} threadId={threadId} + isToolBeingWritten={toolIsLoading} /> : null - const allMessagesHTML = [...previousMessagesHTML, currStreamingMessageHTML] + + const proposed = toolNameSoFar && toolNames.includes(toolNameSoFar as ToolName) ? titleOfToolName[toolNameSoFar as ToolName]?.proposed : toolNameSoFar + const toolTitle = typeof proposed === 'function' ? proposed(null) : proposed + const currStreamingToolHTML = toolIsLoading ? + } /> + : null + + const allMessagesHTML = [...previousMessagesHTML, currStreamingMessageHTML, currStreamingToolHTML] const threadSelector =
    { + const newOnText: OnText = ({ fullText: fullText_, ...p }) => { // until found the first think tag, keep adding to fullText if (!foundTag1) { const endsWithTag1 = endsWithAnyPrefixOf(fullText_, thinkTags[0]) @@ -282,7 +282,7 @@ export const extractReasoningOnTextWrapper = (onText: OnText, thinkTags: [string fullTextSoFar += fullText_.substring(0, tag1Index) // Update latestAddIdx to after the first tag latestAddIdx = tag1Index + thinkTags[0].length - onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar }) + onText({ ...p, fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar }) return } @@ -290,7 +290,7 @@ export const extractReasoningOnTextWrapper = (onText: OnText, thinkTags: [string // add the text to fullText fullTextSoFar = fullText_ latestAddIdx = fullText_.length - onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar }) + onText({ ...p, fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar }) return } @@ -314,7 +314,7 @@ export const extractReasoningOnTextWrapper = (onText: OnText, thinkTags: [string fullReasoningSoFar += fullText_.substring(latestAddIdx, tag2Index) // Update latestAddIdx to after the second tag latestAddIdx = tag2Index + thinkTags[1].length - onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar }) + onText({ ...p, fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar }) return } @@ -327,7 +327,7 @@ export const extractReasoningOnTextWrapper = (onText: OnText, thinkTags: [string latestAddIdx = fullText_.length } - onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar }) + onText({ ...p, fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar }) return } @@ -340,7 +340,7 @@ export const extractReasoningOnTextWrapper = (onText: OnText, thinkTags: [string latestAddIdx = fullText_.length } - onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar }) + onText({ ...p, fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar }) } return newOnText diff --git a/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts b/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts index f5660924..82df3d26 100644 --- a/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts +++ b/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts @@ -54,7 +54,7 @@ export type ToolCallType = { export type AnthropicReasoning = ({ type: 'thinking'; thinking: any; signature: string; } | { type: 'redacted_thinking', data: any }) -export type OnText = (p: { fullText: string; fullReasoning: string }) => void +export type OnText = (p: { fullText: string; fullReasoning: string; fullToolName: string; fullToolParams: string; }) => void export type OnFinalMessage = (p: { fullText: string; fullReasoning: string; toolCalls?: ToolCallType[]; anthropicReasoning: AnthropicReasoning[] | null }) => void // id is tool_use_id export type OnError = (p: { message: string; fullError: Error | null }) => void export type OnAbort = () => void 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 9e82f58c..1d61af41 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 @@ -195,6 +195,10 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage let fullReasoningSoFar = '' let fullTextSoFar = '' + + let fullToolName = '' + let fullToolParams = '' + const toolCallOfIndex: ToolCallOfIndex = {} openai.chat.completions .create(options) @@ -209,6 +213,9 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage toolCallOfIndex[index].name += tool.function?.name ?? '' toolCallOfIndex[index].paramsStr += tool.function?.arguments ?? ''; toolCallOfIndex[index].id += tool.id ?? '' + + fullToolName += tool.function?.name ?? '' + fullToolParams += tool.function?.arguments ?? '' } // message const newText = chunk.choices[0]?.delta?.content ?? '' @@ -222,7 +229,7 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage fullReasoningSoFar += newReasoning } - onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar }) + onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, fullToolName, fullToolParams }) } // on final const toolCalls = toolCallsFrom_OpenAICompat(toolCallOfIndex) @@ -351,6 +358,9 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM let fullText = '' let fullReasoning = '' + let fullToolName = '' + let fullToolParams = '' + // there are no events for tool_use, it comes in at the end stream.on('streamEvent', e => { // start block @@ -358,18 +368,22 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM if (e.content_block.type === 'text') { if (fullText) fullText += '\n\n' // starting a 2nd text block fullText += e.content_block.text - onText({ fullText, fullReasoning }) + onText({ fullText, fullReasoning, fullToolName, fullToolParams }) } else if (e.content_block.type === 'thinking') { if (fullReasoning) fullReasoning += '\n\n' // starting a 2nd reasoning block fullReasoning += e.content_block.thinking - onText({ fullText, fullReasoning }) + onText({ fullText, fullReasoning, fullToolName, fullToolParams }) } else if (e.content_block.type === 'redacted_thinking') { console.log('delta', e.content_block.type) if (fullReasoning) fullReasoning += '\n\n' // starting a 2nd reasoning block fullReasoning += '[redacted_thinking]' - onText({ fullText, fullReasoning }) + onText({ fullText, fullReasoning, fullToolName, fullToolParams }) + } + else if (e.content_block.type === 'tool_use') { + fullToolName += e.content_block.name ?? '' // anthropic gives us the tool name in the start block + onText({ fullText, fullReasoning, fullToolName, fullToolParams }) } } @@ -377,11 +391,15 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM else if (e.type === 'content_block_delta') { if (e.delta.type === 'text_delta') { fullText += e.delta.text - onText({ fullText, fullReasoning }) + onText({ fullText, fullReasoning, fullToolName, fullToolParams }) } else if (e.delta.type === 'thinking_delta') { fullReasoning += e.delta.thinking - onText({ fullText, fullReasoning }) + onText({ fullText, fullReasoning, fullToolName, fullToolParams }) + } + else if (e.delta.type === 'input_json_delta') { // tool use + fullToolParams += e.delta.partial_json ?? '' // anthropic gives us the partial delta (string) here - https://docs.anthropic.com/en/api/messages-streaming + onText({ fullText, fullReasoning, fullToolName, fullToolParams }) } } }) From 4946d3c2a09e0b104809d3bb14c5b92b25a17e38 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 21 Mar 2025 05:56:18 -0700 Subject: [PATCH 144/173] copy --- .../void/browser/react/src/void-settings-tsx/Settings.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 5e406e82..9b5bfff8 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 @@ -535,7 +535,7 @@ export const FeaturesTab = () => { {/* Tools Section */}

    Tools

    -
    {`Tools are functions that LLMs can call. By default, tools that can modify files require approval in Void.`}
    +
    {`Tools are functions that LLMs can call. Tools that can modify files require approval.`}
    {/* Auto Accept Switch */} From 96867326ae97b2f4eb83c9d0f8da98c200f253f2 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 21 Mar 2025 05:59:49 -0700 Subject: [PATCH 145/173] prompt --- src/vs/workbench/contrib/void/common/prompt/prompts.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index 350eaa3b..bd9dbb1b 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -146,7 +146,7 @@ Here's an example of a good description:\n${editToolDescription}.` export const chat_systemMessage = (workspaces: string[], runningTerminalIds: string[], mode: ChatMode) => `\ You are an expert coding ${mode === 'agent' ? 'agent' : 'assistant'} that runs in the Void code editor. Your job is \ -${mode === 'agent' ? `to help the user develop, run, deploy, and make changes to their codebase. You should ALWAYS bring user's task to completion to the fullest extent possible, making all necessary changes. Do not be lazy.` +${mode === 'agent' ? `to help the user develop, run, deploy, and make changes to their codebase. You should ALWAYS bring user's task to completion to the fullest extent possible, calling tools to make all necessary changes. Do not be lazy.` : mode === 'gather' ? `to search and understand their codebase by reading files and content and providing references to help with their query.` : mode === 'normal' ? `to assist the user with their coding tasks.` : ''} From b3522ad73c12b82ed4a80ca25bda456fb369c220 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Fri, 21 Mar 2025 06:42:03 -0700 Subject: [PATCH 146/173] better styling for commandbar --- .../react/src/sidebar-tsx/SidebarChat.tsx | 26 ++- .../void-command-bar-tsx/VoidCommandBar.tsx | 218 +++++++++--------- 2 files changed, 130 insertions(+), 114 deletions(-) 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 f887dea6..b7fa8c45 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 @@ -479,9 +479,9 @@ export const getBasename = (pathStr: string) => { } export const SelectedFiles = ( - { type, selections, setSelections, showProspectiveSelections }: - | { type: 'past', selections: StagingSelectionItem[]; setSelections?: undefined, showProspectiveSelections?: undefined } - | { type: 'staging', selections: StagingSelectionItem[]; setSelections: ((newSelections: StagingSelectionItem[]) => void), showProspectiveSelections?: boolean } + { type, selections, setSelections, showProspectiveSelections, messageIdx, }: + | { type: 'past', selections: StagingSelectionItem[]; setSelections?: undefined, showProspectiveSelections?: undefined, messageIdx: number, } + | { type: 'staging', selections: StagingSelectionItem[]; setSelections: ((newSelections: StagingSelectionItem[]) => void), showProspectiveSelections?: boolean, messageIdx?: number } ) => { const accessor = useAccessor() @@ -593,7 +593,11 @@ export const SelectedFiles = ( if (isThisSelectionAddedAsCurrentFile) { // make it so the file is added permanently, not just as the current file const newSelection: StagingSelectionItem = { ...selection, state: { ...selection.state, wasAddedAsCurrentFile: false } } - setSelections([...selections.slice(0, i), newSelection, ...selections.slice(i + 1)]) + setSelections([ + ...selections.slice(0, i), + newSelection, + ...selections.slice(i + 1) + ]) } } else { // show text @@ -620,8 +624,8 @@ export const SelectedFiles = ( + (isThisSelectionAFile ? '' : ` (${selection.range.startLineNumber}-${selection.range.endLineNumber})`) } - {isThisSelectionAddedAsCurrentFile && currentURI?.toString() === selection.fileURI.toString() && - + {isThisSelectionAddedAsCurrentFile && messageIdx === undefined && currentURI?.toString() === selection.fileURI.toString() && + {`(Current File)`} } @@ -792,7 +796,13 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isCommitted }: { chatMe const canInitialize = mode === 'edit' && textAreaRefState const shouldInitialize = _justEnabledEdit.current || _mustInitialize.current if (canInitialize && shouldInitialize) { - setStagingSelections(chatMessage.selections || []) + setStagingSelections((chatMessage.selections || []) + .map(s => { // quick hack so we dont have to do anything more + const sNew = s + sNew.state.wasAddedAsCurrentFile = false // wipe all "current file" info when the user first edits a message + return sNew + }) + ) if (textAreaFnsRef.current) textAreaFnsRef.current.setValue(chatMessage.displayContent || '') @@ -823,7 +833,7 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isCommitted }: { chatMe let chatbubbleContents: React.ReactNode if (mode === 'display') { chatbubbleContents = <> - + {chatMessage.displayContent} } diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx index b052ae66..b8600474 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx @@ -133,95 +133,90 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { } - const buttonsHTML = <> - + const upDownDisabled = prevDiffIdx === null || nextDiffIdx === null + const leftRightDisabled = prevURIIdx === null || currUriIdx === null - + const upButton = - + const downButton = - - - - - const descriptionHTML = isADiffZoneInThisFile ? - <> - {currUriIdx !== null && sortedCommandBarURIs.length &&
    - {`File ${currUriIdx + 1} of ${sortedCommandBarURIs.length}`} -
    } - {/*
    - {!isAChangeInThisFile ? - `(No changes)` - : `Diff ${(currDiffIdx ?? 0) + 1} of ${sortedDiffIds.length}` - } -
    */} - - : <> - {`${sortedCommandBarURIs.length} file${sortedCommandBarURIs.length === 1 ? '' : 's'}`} - + const leftButton = + + const rightButton = + const filesDescription = (isADiffZoneInThisFile ? + currUriIdx !== null && sortedCommandBarURIs.length !== 0 && + `File ${currUriIdx + 1} of ${sortedCommandBarURIs.length}` + : `${sortedCommandBarURIs.length} file${sortedCommandBarURIs.length === 1 ? '' : 's'} changed` + ); + const changesDescription = (isADiffZoneInThisFile ? + isAChangeInThisFile ? + `Diff ${(currDiffIdx ?? 0) + 1} of ${sortedDiffIds.length}` + : `No changes` + : '' + ); // accept/reject if current URI has changes const onAcceptAll = () => { @@ -237,7 +232,7 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { const acceptAllButton = const rejectAllButton = @@ -288,30 +283,41 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { // >x // - // const actionButtons = currUriHasChanges && ( - //
    - // {acceptAllButton} - // {rejectAllButton} - // {hideButton} - //
    - // ); + const gridLayout =
    + {/* First row */} + {filesDescription && +
    + {leftButton} +
    {/* Divider */} + {rightButton} +
    {/* Divider */} +
    {filesDescription}
    +
    + } + {/* Second row */} + {changesDescription && +
    + {upButton} +
    {/* Divider */} + {downButton} +
    {/* Divider */} +
    {changesDescription}
    +
    + } +
    - // dummy container due to annoyances with VS Code mounting the widget - return
    - {showAcceptRejectAll && <> -
    + return
    + {showAcceptRejectAll && +
    {acceptAllButton} {rejectAllButton}
    - } -
    -
    - {buttonsHTML} -
    -
    - {descriptionHTML} -
    + } +
    + {gridLayout} + {/* {oldLayout} */}
    +
    } From c889acadc0aaabc0b1846f81ad7d3258b1c8028f Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 21 Mar 2025 06:44:04 -0700 Subject: [PATCH 147/173] interruption handling --- .../workbench/contrib/void/browser/editCodeService.ts | 11 ++--------- .../react/src/markdown/ApplyBlockHoverButtons.tsx | 2 ++ .../react/src/quick-edit-tsx/QuickEditChat.tsx | 10 +++++++--- .../browser/react/src/sidebar-tsx/SidebarChat.tsx | 2 +- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index abb0bf24..8a589aa0 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -1245,18 +1245,11 @@ class EditCodeService extends Disposable implements IEditCodeService { // ctrlkzone should never have any conflicts } else { - // console.log('KEEPING CONFLICTS!!!!!!!!') // keep conflict on whole file - to keep conflict, revert the change and use those contents as original, then un-revert the file - // console.log('originalFile', originalFileStr) - // console.log('diffareas A', JSON.stringify(this.diffAreasOfURI, null, 2)) this.acceptOrRejectAllDiffAreas({ uri, removeCtrlKs: true, behavior: 'reject', _addToHistory: false }) - // console.log('diffareas B', JSON.stringify(this.diffAreasOfURI, null, 2)) const oldFileStr = model.getValue(EndOfLinePreference.LF) // use this as original code - // console.log('oldFileStr', { oldFileStr }) this._writeURIText(uri, originalFileStr, 'wholeFileRange', { shouldRealignDiffAreas: true }) // un-revert originalCode = oldFileStr - // console.log('originalCode', { originalCode }) - // console.log('NEW STR', { newStr: model.getValue(EndOfLinePreference.LF) }) } } @@ -1498,7 +1491,7 @@ class EditCodeService extends Disposable implements IEditCodeService { } // end while } // end writeover - const applyDonePromise = runWriteover() + const applyDonePromise = new Promise((res, rej) => { runWriteover().then(res).catch(rej) }) return [diffZone, applyDonePromise] } @@ -1865,7 +1858,7 @@ class EditCodeService extends Disposable implements IEditCodeService { } // end retryLoop - const applyDonePromise = runSearchReplace() + const applyDonePromise = new Promise((res, rej) => { runSearchReplace().then(res).catch(rej) }) return [diffZone, applyDonePromise] } 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 0927ab5f..b37f4c69 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 @@ -165,6 +165,8 @@ export const useApplyButtonHTML = ({ codeStr, applyBoxId, uri }: { codeStr: stri uri: uri, startBehavior: 'keep-conflicts', }) ?? [] + // catch any errors by interrupting the stream + applyDonePromise?.catch(e => { if (newApplyingUri) editCodeService.interruptURIStreaming({ uri: newApplyingUri }) }) applyingURIOfApplyBoxIdRef.current[applyBoxId] = newApplyingUri ?? undefined diff --git a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx index d6ed8605..42e9b81b 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx @@ -58,16 +58,20 @@ export const QuickEditChat = ({ className="@@codicon @@codicon-loading @@codicon-modifier-spin @@codicon-no-default-spin text-void-fg-3" /> - const onSubmit = useCallback(() => { + const onSubmit = useCallback(async () => { if (isDisabled) return if (isStreamingRef.current) return textAreaFnsRef.current?.disable() - const res = editCodeService.startApplying({ + const [newApplyingUri, applyDonePromise] = await editCodeService.startApplying({ from: 'QuickEdit', diffareaid, startBehavior: 'keep-conflicts', - }) + }) ?? [] + // catch any errors by interrupting the stream + applyDonePromise?.catch(e => { if (newApplyingUri) editCodeService.interruptCtrlKStreaming({ diffareaid }) }) + + }, [isStreamingRef, isDisabled, editCodeService, diffareaid]) const onInterrupt = useCallback(() => { 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 b7fa8c45..c00cb058 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 @@ -1946,7 +1946,7 @@ export const SidebarChat = () => { const proposed = toolNameSoFar && toolNames.includes(toolNameSoFar as ToolName) ? titleOfToolName[toolNameSoFar as ToolName]?.proposed : toolNameSoFar const toolTitle = typeof proposed === 'function' ? proposed(null) : proposed const currStreamingToolHTML = toolIsLoading ? - } /> + writing} /> : null const allMessagesHTML = [...previousMessagesHTML, currStreamingMessageHTML, currStreamingToolHTML] From 2eafda91328863ced68f27012d2d2d463427fb78 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Fri, 21 Mar 2025 06:44:15 -0700 Subject: [PATCH 148/173] fix opacity --- .../browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx index b8600474..2eebdfbf 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx @@ -286,7 +286,7 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { const gridLayout =
    {/* First row */} {filesDescription && -
    +
    {leftButton}
    {/* Divider */} {rightButton} @@ -297,7 +297,7 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { {/* Second row */} {changesDescription && -
    +
    {upButton}
    {/* Divider */} {downButton} From cf9eca05642acd0c7862ee10b0b1b7d7cab2eded Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 21 Mar 2025 14:47:17 -0700 Subject: [PATCH 149/173] copy --- .../void/browser/react/src/void-settings-tsx/Settings.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 9b5bfff8..375e5086 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 @@ -535,7 +535,7 @@ export const FeaturesTab = () => { {/* Tools Section */}

    Tools

    -
    {`Tools are functions that LLMs can call. Tools that can modify files require approval.`}
    +
    {`Tools are functions that LLMs can call. Some tools require user approval.`}
    {/* Auto Accept Switch */} From 22a16974f92279273235d6143fc68eefeaf175f4 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 21 Mar 2025 15:18:29 -0700 Subject: [PATCH 150/173] keep versioning, revert mistral (will review separately) --- package-lock.json | 8 +- package.json | 9 +- product.json | 77 ++++++------- scripts/update-version.js | 27 ----- src/vs/base/common/product.ts | 2 +- .../contrib/void/common/modelCapabilities.ts | 80 +++++-------- .../contrib/void/common/voidSettingsTypes.ts | 22 +--- .../llmMessage/sendLLMMessage.impl.ts | 105 ++++++------------ .../parts/dialogs/dialogHandler.ts | 6 +- 9 files changed, 115 insertions(+), 221 deletions(-) delete mode 100644 scripts/update-version.js diff --git a/package-lock.json b/package-lock.json index 8c614833..4a403917 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@google/generative-ai": "^0.22.0", "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", - "@mistralai/mistralai": "^1.5.1", + "@mistralai/mistralai": "^1.5.0", "@parcel/watcher": "2.5.1", "@types/semver": "^7.5.8", "@vscode/deviceid": "^0.1.1", @@ -2540,9 +2540,9 @@ "integrity": "sha512-n1VPsljTSkthsAFYdiWfC+DKzK2WwcRp83Y1YAqdX552BstvsDjft9YXppjUzp11BPsapDoO1LDgrDB0XVsfNQ==" }, "node_modules/@mistralai/mistralai": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-1.5.1.tgz", - "integrity": "sha512-Ie0EH4dAO11MEXR5N2kS2cgr+ycTWvqN/yP9bKrtmUEqjdcF4i7DLxtrFMUw5l2dOPhrkX93G4SziFiATPWu2w==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-1.5.0.tgz", + "integrity": "sha512-AIn8pwAwA/fDvEUvmkt+40zH1ZmfaG3Q7oUWl17GUEC1tU7ZPwYz8Cv9P59lyS1SisHdDSu81oknO7f1ywkz8Q==", "dependencies": { "zod-to-json-schema": "^3.24.1" }, diff --git a/package.json b/package.json index 4e53923b..de2e014d 100644 --- a/package.json +++ b/package.json @@ -68,18 +68,15 @@ "extensions-ci": "node ./node_modules/gulp/bin/gulp.js extensions-ci", "extensions-ci-pr": "node ./node_modules/gulp/bin/gulp.js extensions-ci-pr", "perf": "node scripts/code-perf.js", - "update-build-ts-version": "npm install typescript@next && tsc -p ./build/tsconfig.build.json", - "void-version-patch": "node scripts/update-version.js patch && git add product.json && git commit -m \"Bump: voidVersion\" && git tag v$(node -e \"console.log(require('./product.json').voidVersion)\") && git push && git push --tags", - "void-version-minor": "node scripts/update-version.js minor && git add product.json && git commit -m \"Bump: voidVersion minor\" && git tag v$(node -e \"console.log(require('./product.json').voidVersion)\") && git push && git push --tags", - "void-version-major": "node scripts/update-version.js major && git add product.json && git commit -m \"Bump: voidVersion major\" && git tag v$(node -e \"console.log(require('./product.json').voidVersion)\") && git push && git push --tags" - }, + "update-build-ts-version": "npm install typescript@next && tsc -p ./build/tsconfig.build.json" + }, "dependencies": { "@anthropic-ai/sdk": "^0.39.0", "@floating-ui/react": "^0.27.5", "@google/generative-ai": "^0.22.0", "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", - "@mistralai/mistralai": "^1.5.1", + "@mistralai/mistralai": "^1.5.0", "@parcel/watcher": "2.5.1", "@types/semver": "^7.5.8", "@vscode/deviceid": "^0.1.1", diff --git a/product.json b/product.json index b629f2b7..b7b9c269 100644 --- a/product.json +++ b/product.json @@ -1,41 +1,38 @@ { - "nameShort": "Void", - "nameLong": "Void", - "voidVersion": "1.0.3", - "commit": "18d2d9f5ee4cb290bd392035680b9c6ea60eeb6b", - "date": "2025-03-17", - "applicationName": "void", - "dataFolderName": ".void-editor", - "win32MutexName": "voideditor", - "licenseName": "MIT", - "licenseUrl": "https://github.com/voideditor/void/blob/main/LICENSE.txt", - "serverLicenseUrl": "https://github.com/voideditor/void/blob/main/LICENSE.txt", - "serverGreeting": [], - "serverLicense": [], - "serverLicensePrompt": "", - "serverApplicationName": "void-server", - "serverDataFolderName": ".void-server", - "tunnelApplicationName": "void-tunnel", - "win32DirName": "Void", - "win32NameVersion": "Void", - "win32RegValueName": "VoidEditor", - "win32x64AppId": "{{9D394D01-1728-45A7-B997-A6C82C5452C3}", - "win32arm64AppId": "{{0668DD58-2BDE-4101-8CDA-40252DF8875D}", - "win32x64UserAppId": "{{8BED5DC1-6C55-46E6-9FE6-18F7E6F7C7F1}", - "win32arm64UserAppId": "{{F6C87466-BC82-4A8F-B0FF-18CA366BA4D8}", - "win32AppUserModelId": "Void.Editor", - "win32ShellNameShort": "V&oid", - "win32TunnelServiceMutex": "void-tunnelservice", - "win32TunnelMutex": "void-tunnel", - "darwinBundleIdentifier": "com.voideditor.code", - "linuxIconName": "void-editor", - "licenseFileName": "LICENSE.txt", - "reportIssueUrl": "https://github.com/voideditor/void/issues/new", - "nodejsRepository": "https://nodejs.org", - "urlProtocol": "void-editor", - "extensionsGallery": { - "serviceUrl": "https://open-vsx.org/vscode/gallery", - "itemUrl": "https://open-vsx.org/vscode/item" - }, - "builtInExtensions": [] -} \ No newline at end of file + "nameShort": "Void", + "nameLong": "Void", + "applicationName": "void", + "dataFolderName": ".void-editor", + "win32MutexName": "voideditor", + "licenseName": "MIT", + "licenseUrl": "https://github.com/voideditor/void/blob/main/LICENSE.txt", + "serverLicenseUrl": "https://github.com/voideditor/void/blob/main/LICENSE.txt", + "serverGreeting": [], + "serverLicense": [], + "serverLicensePrompt": "", + "serverApplicationName": "void-server", + "serverDataFolderName": ".void-server", + "tunnelApplicationName": "void-tunnel", + "win32DirName": "Void", + "win32NameVersion": "Void", + "win32RegValueName": "VoidEditor", + "win32x64AppId": "{{9D394D01-1728-45A7-B997-A6C82C5452C3}", + "win32arm64AppId": "{{0668DD58-2BDE-4101-8CDA-40252DF8875D}", + "win32x64UserAppId": "{{8BED5DC1-6C55-46E6-9FE6-18F7E6F7C7F1}", + "win32arm64UserAppId": "{{F6C87466-BC82-4A8F-B0FF-18CA366BA4D8}", + "win32AppUserModelId": "Void.Editor", + "win32ShellNameShort": "V&oid", + "win32TunnelServiceMutex": "void-tunnelservice", + "win32TunnelMutex": "void-tunnel", + "darwinBundleIdentifier": "com.voideditor.code", + "linuxIconName": "void-editor", + "licenseFileName": "LICENSE.txt", + "reportIssueUrl": "https://github.com/voideditor/void/issues/new", + "nodejsRepository": "https://nodejs.org", + "urlProtocol": "void", + "extensionsGallery": { + "serviceUrl": "https://marketplace.visualstudio.com/_apis/public/gallery", + "itemUrl": "https://marketplace.visualstudio.com/items" + }, + "builtInExtensions": [] +} diff --git a/scripts/update-version.js b/scripts/update-version.js deleted file mode 100644 index 5a528a47..00000000 --- a/scripts/update-version.js +++ /dev/null @@ -1,27 +0,0 @@ -const fs = require('fs'); -const { execSync } = require('child_process'); -const semver = require('semver'); - -function updateProductJson(type = 'patch') { - // Read product.json - const productJsonPath = './product.json'; - const product = JSON.parse(fs.readFileSync(productJsonPath, 'utf8')); - - // Update the version - product.voidVersion = semver.inc(product.voidVersion, type); - - // Update the commit hash - product.commit = execSync('git rev-parse HEAD').toString().trim(); - - // Update the date - product.date = new Date().toISOString().split('T')[0]; - - // Write the modifications - fs.writeFileSync(productJsonPath, JSON.stringify(product, null, 2)); - - return product.voidVersion; -} - -// Execute the update -const newVersion = updateProductJson(process.argv[2] || 'patch'); -console.log(`Updated version: ${newVersion}`); diff --git a/src/vs/base/common/product.ts b/src/vs/base/common/product.ts index 7d3c2c40..c0a400d2 100644 --- a/src/vs/base/common/product.ts +++ b/src/vs/base/common/product.ts @@ -56,7 +56,7 @@ export type ExtensionVirtualWorkspaceSupport = { export interface IProductConfiguration { readonly version: string; - readonly voidVersion: string; + readonly voidVersion?: string; // Void added this readonly date?: string; readonly quality?: string; readonly commit?: string; diff --git a/src/vs/workbench/contrib/void/common/modelCapabilities.ts b/src/vs/workbench/contrib/void/common/modelCapabilities.ts index 25587fc9..95de9604 100644 --- a/src/vs/workbench/contrib/void/common/modelCapabilities.ts +++ b/src/vs/workbench/contrib/void/common/modelCapabilities.ts @@ -51,14 +51,13 @@ export const defaultModelsOfProvider = { 'llama-3.1-8b-instant', // 'qwen-2.5-coder-32b', // preview mode (experimental) ], - mistral: [ // https://docs.mistral.ai/getting-started/models/models_overview/ - 'codestral-latest', - 'open-codestral-mamba', - 'mistral-small-latest', - 'mistral-large-latest', - 'ministral-3b-latest', - 'ministral-8b-latest', - ], + // not supporting mistral right now- it's last on Void usage, and a huge pain to set up since it's nonstandard (it supports codestral FIM but it's on v1/fim/completions, etc) + // mistral: [ // https://docs.mistral.ai/getting-started/models/models_overview/ + // 'codestral-latest', + // 'mistral-large-latest', + // 'ministral-3b-latest', + // 'ministral-8b-latest', + // ], openAICompatible: [], // fallback } as const satisfies Record @@ -122,32 +121,6 @@ const modelOptionsDefaults: ModelOptions = { supportsReasoning: false, } -const mistralModelOptions = { - 'codestral-latest': { - contextWindow: 32_000, - maxOutputTokens: 4_096, - cost: { input: 0.00, output: 0.00 }, - supportsFIM: true, - supportsSystemMessage: 'system-role', - supportsTools: 'openai-style', - supportsReasoning: false, - }, - 'mistral-large-latest': { - contextWindow: 32_000, - maxOutputTokens: 4_096, - cost: { input: 0.00, output: 0.00 }, - supportsFIM: false, - supportsSystemMessage: 'system-role', - supportsTools: 'openai-style', - supportsReasoning: false, - } -} as const satisfies { [s: string]: ModelOptions } - -const mistralSettings: ProviderSettings = { - ...mistralModelOptions, - modelOptions: {}, - modelOptionsFallback: (modelName) => extensiveModelFallback(modelName), -} const openSourceModelOptions_assumingOAICompat = { 'deepseekR1': { @@ -162,6 +135,12 @@ const openSourceModelOptions_assumingOAICompat = { supportsTools: false, supportsReasoning: false, }, + 'codestral': { + supportsFIM: true, + supportsSystemMessage: 'system-role', + supportsTools: 'openai-style', + supportsReasoning: false, + }, // llama 'llama3': { supportsFIM: false, @@ -213,9 +192,11 @@ const openSourceModelOptions_assumingOAICompat = { supportsTools: false, supportsReasoning: false, }, - ...mistralModelOptions, } as const satisfies { [s: string]: Partial } + + + const extensiveModelFallback: ProviderSettings['modelOptionsFallback'] = (modelName) => { const toFallback = (opts: Omit): ModelOptions & { modelName: string } => { return { @@ -233,12 +214,16 @@ const extensiveModelFallback: ProviderSettings['modelOptionsFallback'] = (modelN if (modelName.includes('deepseek')) return toFallback({ ...openSourceModelOptions_assumingOAICompat.deepseekCoderV2, contextWindow: 32_000, maxOutputTokens: 4_096, }) if (modelName.includes('llama3')) return toFallback({ ...openSourceModelOptions_assumingOAICompat.llama3, contextWindow: 32_000, maxOutputTokens: 4_096, }) if (modelName.includes('qwen') && modelName.includes('2.5') && modelName.includes('coder')) return toFallback({ ...openSourceModelOptions_assumingOAICompat['qwen2.5coder'], contextWindow: 32_000, maxOutputTokens: 4_096, }) - if (modelName.includes('mistral')) return toFallback({ ...openSourceModelOptions_assumingOAICompat['mistral-large-latest'] }) - if (modelName.includes('codestral')) return toFallback({ ...openSourceModelOptions_assumingOAICompat['codestral-latest'] }) + if (modelName.includes('codestral')) return toFallback({ ...openSourceModelOptions_assumingOAICompat.codestral, contextWindow: 32_000, maxOutputTokens: 4_096, }) if (/\bo1\b/.test(modelName) || /\bo3\b/.test(modelName)) return toFallback(openAIModelOptions['o1']) return toFallback(modelOptionsDefaults) } + + + + + // ---------------- ANTHROPIC ---------------- const anthropicModelOptions = { 'claude-3-7-sonnet-20250219': { // https://docs.anthropic.com/en/docs/about-claude/models/all-models#model-comparison-table @@ -369,13 +354,6 @@ const openAISettings: ProviderSettings = { } } - - - - - - - // ---------------- XAI ---------------- const xAIModelOptions = { 'grok-2': { @@ -577,20 +555,13 @@ const openRouterModelOptions_assumingOpenAICompat = { supportsReasoning: false, }, 'mistralai/codestral-2501': { - ...openSourceModelOptions_assumingOAICompat['codestral-latest'], + ...openSourceModelOptions_assumingOAICompat.codestral, contextWindow: 256_000, maxOutputTokens: null, cost: { input: 0.3, output: 0.9 }, supportsTools: 'openai-style', supportsReasoning: false, }, - 'mistralai/mistral-large-latest': { - ...openSourceModelOptions_assumingOAICompat['mistral-large-latest'], - contextWindow: 256_000, - maxOutputTokens: null, - cost: { input: 0.3, output: 0.9 }, - }, - 'qwen/qwen-2.5-coder-32b-instruct': { ...openSourceModelOptions_assumingOAICompat['qwen2.5coder'], contextWindow: 33_000, @@ -619,6 +590,8 @@ const openRouterSettings: ProviderSettings = { } + + // ---------------- model settings of everything above ---------------- const modelSettingsOfProvider: { [providerName in ProviderName]: ProviderSettings } = { @@ -626,6 +599,7 @@ const modelSettingsOfProvider: { [providerName in ProviderName]: ProviderSetting anthropic: anthropicSettings, xAI: xAISettings, gemini: geminiSettings, + // open source models deepseek: deepseekSettings, groq: groqSettings, @@ -635,7 +609,7 @@ const modelSettingsOfProvider: { [providerName in ProviderName]: ProviderSetting vLLM: vLLMSettings, ollama: ollamaSettings, openAICompatible: openaiCompatible, - mistral: mistralSettings, + // googleVertex: {}, // microsoftAzure: {}, } as const diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index 79d2204b..5636790a 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -43,9 +43,6 @@ export const defaultProviderSettings = { xAI: { apiKey: '' }, - mistral: { - apiKey: '' - }, } as const @@ -147,11 +144,6 @@ export const displayInfoOfProviderName = (providerName: ProviderName): DisplayIn title: 'Grok (xAI)', } } - else if (providerName === 'mistral') { - return { - title: 'Mistral.ai API', - } - } throw new Error(`descOfProviderName: Unknown provider name: "${providerName}"`) @@ -178,8 +170,7 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName providerName === 'groq' ? 'gsk_key...' : providerName === 'openAICompatible' ? 'sk-key...' : providerName === 'xAI' ? 'xai-key...' : - providerName === 'mistral' ? 'key...' : - '', + '', subTextMd: providerName === 'anthropic' ? 'Get your [API Key here](https://console.anthropic.com/settings/keys).' : providerName === 'openAI' ? 'Get your [API Key here](https://platform.openai.com/api-keys).' : @@ -188,9 +179,8 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName providerName === 'gemini' ? 'Get your [API Key here](https://aistudio.google.com/apikey).' : providerName === 'groq' ? 'Get your [API Key here](https://console.groq.com/keys).' : providerName === 'xAI' ? 'Get your [API Key here](https://console.x.ai).' : - providerName === 'mistral' ? 'Get your [API Key here](https://console.mistral.ai/api-keys).' : - providerName === 'openAICompatible' ? undefined : - '', + providerName === 'openAICompatible' ? undefined : + '', isPasswordField: true, } } @@ -298,12 +288,6 @@ export const defaultSettingsOfProvider: SettingsOfProvider = { ...modelInfoOfDefaultModelNames(defaultModelsOfProvider.openAICompatible), _didFillInProviderSettings: undefined, }, - mistral: { // aggregator - ...defaultCustomSettings, - ...defaultProviderSettings.mistral, - ...modelInfoOfDefaultModelNames(defaultModelsOfProvider.mistral), - _didFillInProviderSettings: undefined, - }, ollama: { // aggregator ...defaultCustomSettings, ...defaultProviderSettings.ollama, 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 087b61b5..abb0bc17 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 @@ -6,13 +6,8 @@ import Anthropic from '@anthropic-ai/sdk'; import { Ollama } from 'ollama'; import OpenAI, { ClientOptions } from 'openai'; + import { Model as OpenAIModel } from 'openai/resources/models.js'; - -// Mistral FIM -import { MistralCore } from "@mistralai/mistralai/core.js"; -import { fimComplete } from "@mistralai/mistralai/funcs/fimComplete.js"; -// - import { extractReasoningOnFinalMessage, extractReasoningOnTextWrapper } from '../../common/helpers/extractCodeFromResult.js'; import { LLMChatMessage, LLMFIMMessage, ModelListParams, OllamaModelResponse, OnError, OnFinalMessage, OnText } from '../../common/sendLLMMessageTypes.js'; import { defaultProviderSettings, displayInfoOfProviderName, ModelSelectionOptions, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js'; @@ -117,16 +112,12 @@ const newOpenAICompatibleSDK = ({ settingsOfProvider, providerName, includeInPay const thisConfig = settingsOfProvider[providerName] return new OpenAI({ baseURL: 'https://api.x.ai/v1', apiKey: thisConfig.apiKey, ...commonPayloadOpts }) } - else if (providerName === 'mistral') { - const thisConfig = settingsOfProvider[providerName] - return new OpenAI({ baseURL: 'https://api.mistral.ai/v1', apiKey: thisConfig.apiKey, ...commonPayloadOpts }) - } else throw new Error(`Void providerName was invalid: ${providerName}.`) } -const _sendOpenAICompatibleFIM = ({ messages: messages_, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, aiInstructions, modelSelectionOptions, }: SendFIMParams_Internal) => { +const _sendOpenAICompatibleFIM = ({ messages: messages_, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, aiInstructions, }: SendFIMParams_Internal) => { const { modelName, supportsFIM } = getModelCapabilities(providerName, modelName_) if (!supportsFIM) { if (modelName === modelName_) @@ -148,7 +139,6 @@ const _sendOpenAICompatibleFIM = ({ messages: messages_, onFinalMessage, onError max_tokens: messages.maxTokens, }) .then(async response => { - const fullText = response.choices[0]?.text onFinalMessage({ fullText, fullReasoning: '', anthropicReasoning: null }); }) @@ -158,7 +148,10 @@ const _sendOpenAICompatibleFIM = ({ messages: messages_, onFinalMessage, onError }) } -const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, aiInstructions, modelSelectionOptions, tools: tools_ }: SendChatParams_Internal) => { + + + +const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, aiInstructions, tools: tools_ }: SendChatParams_Internal) => { const { modelName, supportsReasoning, @@ -471,56 +464,6 @@ const sendOllamaFIM = ({ messages: messages_, onFinalMessage, onError, settingsO }) } -//////// MISTRAL //////// -const _sendMistralChat = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, aiInstructions, modelSelectionOptions }: SendChatParams_Internal) => { - _sendOpenAICompatibleChat({ - messages: messages_, - onText, - onFinalMessage, - onError, - settingsOfProvider, - modelName: modelName_, - _setAborter, - providerName, - aiInstructions, - modelSelectionOptions - }); -} - -const _sendMistralFIM = ({ messages: messages_, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, aiInstructions, modelSelectionOptions }: SendFIMParams_Internal) => { - const { modelName, supportsFIM } = getModelCapabilities(providerName, modelName_) - if (!supportsFIM) { - if (modelName === modelName_) - onError({ message: `Model ${modelName} does not support FIM.`, fullError: null }) - else - onError({ message: `Model ${modelName_} (${modelName}) does not support FIM.`, fullError: null }) - return - } - - prepareFIMMessage({ messages: messages_, aiInstructions }) - - const mistral = new MistralCore({ apiKey: settingsOfProvider.mistral.apiKey }) - - fimComplete( - mistral, { - model: modelName, - prompt: messages_.prefix, - suffix: messages_.suffix, - stream: false, - topP: 1, - stop: messages_.stopTokens - }, - ) - .then(async response => { - let content = response?.ok ? response.value.choices?.[0]?.message?.content : ''; - const fullText = typeof content === 'string' ? content : - Array.isArray(content) ? content.map(chunk => chunk.type === 'text' ? chunk.text : '').join('') : ''; - onFinalMessage({ fullText, fullReasoning: '', anthropicReasoning: null }); - }) - .catch(error => { - onError({ message: error + '', fullError: error }); - }) -} type CallFnOfProvider = { @@ -552,6 +495,11 @@ export const sendLLMMessageToProviderImplementation = { sendFIM: null, list: null, }, + // mistral: { + // sendChat: , // TODO + // sendFIM: , // TODO // https://docs.mistral.ai/api/#tag/fim + // list: null, + // }, ollama: { sendChat: (params) => _sendOpenAICompatibleChat(params), sendFIM: sendOllamaFIM, @@ -582,9 +530,30 @@ export const sendLLMMessageToProviderImplementation = { sendFIM: null, list: null, }, - mistral: { - sendChat: (params) => _sendMistralChat(params), - sendFIM: (params) => _sendMistralFIM(params), - list: null, - }, } satisfies CallFnOfProvider + + + + +/* +FIM info (this may be useful in the future with vLLM, but in most cases the only way to use FIM is if the provider explicitly supports it): + +qwen2.5-coder https://ollama.com/library/qwen2.5-coder/blobs/e94a8ecb9327 +<|fim_prefix|>{{ .Prompt }}<|fim_suffix|>{{ .Suffix }}<|fim_middle|> + +codestral https://ollama.com/library/codestral/blobs/51707752a87c +[SUFFIX]{{ .Suffix }}[PREFIX] {{ .Prompt }} + +deepseek-coder-v2 https://ollama.com/library/deepseek-coder-v2/blobs/22091531faf0 +<|fim▁begin|>{{ .Prompt }}<|fim▁hole|>{{ .Suffix }}<|fim▁end|> + +starcoder2 https://ollama.com/library/starcoder2/blobs/3b190e68fefe + + +{{ .Prompt }}{{ .Suffix }} +<|end_of_text|> + +codegemma https://ollama.com/library/codegemma:2b/blobs/48d9a8140749 +<|fim_prefix|>{{ .Prompt }}<|fim_suffix|>{{ .Suffix }}<|fim_middle|> + +*/ diff --git a/src/vs/workbench/electron-sandbox/parts/dialogs/dialogHandler.ts b/src/vs/workbench/electron-sandbox/parts/dialogs/dialogHandler.ts index 4372d416..e65fade3 100644 --- a/src/vs/workbench/electron-sandbox/parts/dialogs/dialogHandler.ts +++ b/src/vs/workbench/electron-sandbox/parts/dialogs/dialogHandler.ts @@ -71,7 +71,7 @@ export class NativeDialogHandler extends AbstractDialogHandler { async about(): Promise { let version = this.productService.version; - let voidVersion = this.productService.voidVersion || 'Unknown'; + let voidVersion = this.productService.voidVersion; if (this.productService.target) { version = `${version} (${this.productService.target} setup)`; } else if (this.productService.darwinUniversalAssetId) { @@ -82,8 +82,8 @@ export class NativeDialogHandler extends AbstractDialogHandler { const detailString = (useAgo: boolean): string => { return localize({ key: 'aboutDetail', comment: ['Electron, Chromium, Node.js and V8 are product names that need no translation'] }, - "Void : {0}\nVSCode Version: {1}\nCommit: {2}\nDate: {3}\nElectron: {4}\nElectronBuildId: {5}\nChromium: {6}\nNode.js: {7}\nV8: {8}\nOS: {9}", - voidVersion, + "Void Version: {0}\nVSCode Version: {1}\nCommit: {2}\nDate: {3}\nElectron: {4}\nElectronBuildId: {5}\nChromium: {6}\nNode.js: {7}\nV8: {8}\nOS: {9}", + voidVersion || 'Unknown', version, this.productService.commit || 'Unknown', this.productService.date ? `${this.productService.date}${useAgo ? ' (' + fromNow(new Date(this.productService.date), true) + ')' : ''}` : 'Unknown', From 4ad82cff07432a6ebac81d029e8474f6e343a8e1 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 21 Mar 2025 15:19:07 -0700 Subject: [PATCH 151/173] format --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index de2e014d..d786053c 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "extensions-ci-pr": "node ./node_modules/gulp/bin/gulp.js extensions-ci-pr", "perf": "node scripts/code-perf.js", "update-build-ts-version": "npm install typescript@next && tsc -p ./build/tsconfig.build.json" - }, + }, "dependencies": { "@anthropic-ai/sdk": "^0.39.0", "@floating-ui/react": "^0.27.5", From 4301eb2bb1f52f55151530a6bd9c100abf4fa085 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 21 Mar 2025 15:21:23 -0700 Subject: [PATCH 152/173] version --- product.json | 1 + 1 file changed, 1 insertion(+) diff --git a/product.json b/product.json index b7b9c269..cea8f170 100644 --- a/product.json +++ b/product.json @@ -1,6 +1,7 @@ { "nameShort": "Void", "nameLong": "Void", + "voidVersion": "1.0.3", "applicationName": "void", "dataFolderName": ".void-editor", "win32MutexName": "voideditor", From e1d16b3de4a7ffafe89d3873555f6d6e71e44bbf Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 21 Mar 2025 15:23:40 -0700 Subject: [PATCH 153/173] + --- .../workbench/electron-sandbox/parts/dialogs/dialogHandler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/electron-sandbox/parts/dialogs/dialogHandler.ts b/src/vs/workbench/electron-sandbox/parts/dialogs/dialogHandler.ts index e65fade3..429862fc 100644 --- a/src/vs/workbench/electron-sandbox/parts/dialogs/dialogHandler.ts +++ b/src/vs/workbench/electron-sandbox/parts/dialogs/dialogHandler.ts @@ -71,7 +71,7 @@ export class NativeDialogHandler extends AbstractDialogHandler { async about(): Promise { let version = this.productService.version; - let voidVersion = this.productService.voidVersion; + let voidVersion = this.productService.voidVersion; // Void added this if (this.productService.target) { version = `${version} (${this.productService.target} setup)`; } else if (this.productService.darwinUniversalAssetId) { From 29f8fa6e1dbfe6eb9d7ecfc048ab22e9dc2d3cbf Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Fri, 21 Mar 2025 19:09:49 -0700 Subject: [PATCH 154/173] improve selections --- .../contrib/void/browser/chatThreadService.ts | 46 ++++++++++++------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 6eed5b16..e1152c3b 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -122,15 +122,21 @@ export type ThreadStreamState = { } } - -const newThreadObject = () => { +const defaultThreadAttributes = () => { const now = new Date().toISOString() return { - id: generateUuid(), createdAt: now, lastModified: now, messages: [], state: defaultThreadState, + } +} + +const newThreadObject = () => { + + return { + id: generateUuid(), + ...defaultThreadAttributes() } satisfies ChatThreads[string] } @@ -240,13 +246,13 @@ class ChatThreadService extends Disposable implements IChatThreadService { // add the current file to the thread being edited - const model = this._codeEditorService.getActiveCodeEditor()?.getModel() ?? null - if (!model) { return; } + const newModel = this._codeEditorService.getActiveCodeEditor()?.getModel() ?? null + if (!newModel) { return; } - const newSelection: StagingSelectionItem = { + const newStagingSelection: StagingSelectionItem = { type: 'File', - fileURI: model.uri, - language: model.getLanguageId(), + fileURI: newModel.uri, + language: newModel.getLanguageId(), selectionStr: null, range: null, state: { isOpened: false, wasAddedAsCurrentFile: true } @@ -259,15 +265,18 @@ class ChatThreadService extends Disposable implements IChatThreadService { const oldStagingSelections = this.getCurrentThreadState().stagingSelections || []; - // if the file already exists, do nothing - const alreadyHasFile = oldStagingSelections.some(s => s.type === 'File' && s.fileURI.toString() === newSelection.fileURI.toString()) - if (alreadyHasFile) { return; } + // remove all old selectons that are marked as `wasAddedAsCurrentFile` + const newStagingSelections: StagingSelectionItem[] = oldStagingSelections.filter(s => !s.state?.wasAddedAsCurrentFile); - // add the file - const filteredStagingSelections = oldStagingSelections.filter(s => !s.state?.wasAddedAsCurrentFile); // remove all old selectons that were added during a file change - const newSelections = [...filteredStagingSelections, newSelection]; + // add the new file if it doesn't exist + const fileIsAdded = oldStagingSelections.some(s => s.type === 'File' && s.fileURI.toString() === newStagingSelection.fileURI.toString()) + if (!fileIsAdded) { + newStagingSelections.push(newStagingSelection) + } + + // update thread state with new selections + this.setCurrentThreadState({ stagingSelections: newStagingSelections }); - this.setCurrentThreadState({ stagingSelections: newSelections }); } else { // user is editing a message @@ -275,14 +284,14 @@ class ChatThreadService extends Disposable implements IChatThreadService { // do nothing. I don't think it feels good to auto-add the current file when you're editing a message. // const oldStagingSelections = this.getCurrentMessageState(focusedMessageIdx).stagingSelections || []; + // const newStagingSelections = [...filteredStagingSelections, newSelection]; + // this.setCurrentMessageState(focusedMessageIdx, { stagingSelections: newSelections }); // // if the file already exists, do nothing // const alreadyHasFile = oldStagingSelections.some(s => s.type === 'File' && s.fileURI.toString() === newSelection.fileURI.toString()) // if (alreadyHasFile) { return; } // const filteredStagingSelections = oldStagingSelections.filter(s => !s.state?.wasAddedDuringFileChange); // remove all old selectons that were added during a file change - // const newSelections = [...filteredStagingSelections, newSelection]; - // this.setCurrentMessageState(focusedMessageIdx, { stagingSelections: newSelections }); } @@ -1244,6 +1253,9 @@ class ChatThreadService extends Disposable implements IChatThreadService { const { allThreads: currentThreads } = this.state for (const threadId in currentThreads) { if (currentThreads[threadId]!.messages.length === 0) { + // clear the thread + currentThreads[threadId]! = { id: currentThreads[threadId]!.id, ...defaultThreadAttributes() } + // switch to the thread this.switchToThread(threadId) return } From 159d52a71623033ca505e34e910086b853671455 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Fri, 21 Mar 2025 19:21:13 -0700 Subject: [PATCH 155/173] styles --- .../void/browser/react/src/sidebar-tsx/SidebarChat.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 c00cb058..f7104242 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 @@ -303,9 +303,9 @@ export const VoidChatArea: React.FC = ({ className={` gap-x-1 flex flex-col p-2 relative input text-left shrink-0 - transition-all duration-200 rounded-md - bg-vscode-input-bg + bg-void-bg-1 + transition-all duration-200 border border-void-border-3 focus-within:border-void-border-1 hover:border-void-border-1 max-h-[80vh] overflow-y-auto ${className} @@ -929,7 +929,7 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isCommitted }: { chatMe className={` text-left rounded-lg max-w-full ${mode === 'edit' ? '' - : mode === 'display' ? 'p-2 flex flex-col gap-1 bg-void-bg-1 text-void-fg-1 overflow-x-auto cursor-pointer' : '' + : mode === 'display' ? 'p-2 flex flex-col bg-void-bg-1 text-void-fg-1 overflow-x-auto cursor-pointer' : '' } `} onClick={() => { if (mode === 'display') { onOpenEdit() } }} From f3451d5077a14167a8689f70eb30ec82ae1bc2de Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 21 Mar 2025 19:23:04 -0700 Subject: [PATCH 156/173] command bar state + accept/reject all --- .../contrib/void/browser/editCodeService.ts | 19 ++- .../react/src/sidebar-tsx/SidebarChat.tsx | 74 +++++++++- .../void-command-bar-tsx/VoidCommandBar.tsx | 130 +++++++++--------- .../contrib/void/browser/toolsService.ts | 5 + .../void/browser/voidCommandBarService.ts | 34 +++-- .../contrib/void/common/prompt/prompts.ts | 4 +- 6 files changed, 184 insertions(+), 82 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 8a589aa0..e768fe6d 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -1226,8 +1226,6 @@ class EditCodeService extends Disposable implements IEditCodeService { // treat like full file, unless linkedCtrlKZone was provided in which case use its diff's range - - const startLine = linkedCtrlKZone ? linkedCtrlKZone.startLine : 1 const endLine = linkedCtrlKZone ? linkedCtrlKZone.endLine : model.getLineCount() const range = { startLineNumber: startLine, startColumn: 1, endLineNumber: endLine, endColumn: Number.MAX_SAFE_INTEGER } @@ -1291,7 +1289,16 @@ class EditCodeService extends Disposable implements IEditCodeService { - + private _uriIsStreaming(uri: URI) { + const diffAreas = this.diffAreasOfURI[uri.fsPath] + if (!diffAreas) return false + for (const diffareaid of diffAreas) { + const diffArea = this.diffAreaOfId[diffareaid] + if (diffArea?.type !== 'DiffZone') continue + if (diffArea._streamState.isStreaming) return true + } + return false + } private async _initializeWriteoverStream(opts: StartApplyingOpts): Promise<[DiffZone, Promise] | undefined> { @@ -1358,7 +1365,8 @@ class EditCodeService extends Disposable implements IEditCodeService { } else { throw new Error(`featureName ${from} is invalid`) } - + // if URI is already streaming, return (should never happen, caller is responsible for checking) + if (this._uriIsStreaming(uri)) return // start diffzone const res = this._startStreamingDiffZone({ @@ -1526,6 +1534,9 @@ class EditCodeService extends Disposable implements IEditCodeService { { role: 'user', content: userMessageContent }, ] + // if URI is already streaming, return (should never happen, caller is responsible for checking) + if (this._uriIsStreaming(uri)) return + // start diffzone const res = this._startStreamingDiffZone({ uri, 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 f7104242..01fc042c 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 @@ -10,7 +10,7 @@ import React, { ButtonHTMLAttributes, FormEvent, FormHTMLAttributes, Fragment, KeyboardEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { useAccessor, useSidebarState, useChatThreadsState, useChatThreadsStreamState, useSettingsState, useActiveURI } from '../util/services.js'; +import { useAccessor, useSidebarState, useChatThreadsState, useChatThreadsStreamState, useSettingsState, useActiveURI, useCommandBarState } from '../util/services.js'; import { ChatMarkdownRender, ChatMessageLocation, getApplyBoxId } from '../markdown/ChatMarkdownRender.js'; import { URI } from '../../../../../../../base/common/uri.js'; @@ -762,6 +762,50 @@ const ToolHeaderWrapper = ({ }; + + +const SimplifiedToolHeader = ({ + title, + children, +}: { + title: string; + children?: React.ReactNode; +}) => { + const [isOpen, setIsOpen] = useState(false); + const isDropdown = children !== undefined; + return ( +
    +
    + {/* header */} +
    { + if (isDropdown) { setIsOpen(v => !v); } + }} + > + {isDropdown && ( + + )} +
    + {title} +
    +
    + {/* children */} + {
    + {children} +
    } +
    +
    + ); +}; + + + + const UserMessageComponent = ({ chatMessage, messageIdx, isCommitted }: { chatMessage: ChatMessage & { role: 'user' }, messageIdx: number, isCommitted: boolean, }) => { const accessor = useAccessor() @@ -1812,6 +1856,34 @@ const ChatBubble = ({ chatMessage, isCommitted, messageIdx, isLast, chatIsRunnin } + + +const CommandBarInChat = () => { + const { state: commandBarState, sortedURIs: sortedCommandBarURIs } = useCommandBarState() + const [isExpanded, setIsExpanded] = useState(false) + + const accessor = useAccessor() + const commandService = accessor.get('ICommandService') + + if (!sortedCommandBarURIs || sortedCommandBarURIs.length === 0) { + return null + } + + return ( + + {sortedCommandBarURIs.map((uri, i) => ( + { commandService.executeCommand('vscode.open', uri, { preview: true }) }} + /> + ))} + + + ) +} + + export const SidebarChat = () => { const textAreaRef = useRef(null) const textAreaFnsRef = useRef(null) diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx index 2eebdfbf..057261b8 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx @@ -45,13 +45,22 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { const { state: commandBarState, sortedURIs: sortedCommandBarURIs } = useCommandBarState() + // latestUriIdx is used to remember place in leftRight + const _latestValidUriIdxRef = useRef(null) - // changes if the user clicks left/right or if the user goes on a uri with changes - const [currUriIdx, setUriIdx] = useState(null) + // i is the current index of the URI in sortedCommandBarURIs + const i_ = sortedCommandBarURIs.findIndex(e => e.fsPath === uri?.fsPath) + const currFileIdx = i_ === -1 ? null : i_ useEffect(() => { - const i = sortedCommandBarURIs.findIndex(e => e.fsPath === uri?.fsPath) - if (i !== -1) { setUriIdx(i) } - }, [sortedCommandBarURIs, uri]) + if (currFileIdx !== null) _latestValidUriIdxRef.current = currFileIdx + }, [currFileIdx]) + + const uriIdxInStepper = currFileIdx !== null ? currFileIdx // use currFileIdx if it exists, else use latestNotNullUriIdxRef + : _latestValidUriIdxRef.current === null ? null + : _latestValidUriIdxRef.current < sortedCommandBarURIs.length ? _latestValidUriIdxRef.current + : null + + const getNextDiffIdx = (step: 1 | -1) => { // check undefined @@ -74,14 +83,12 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { const diffid = sortedDiffIds[idx] const diff = editCodeService.diffOfId[diffid] const range = { startLineNumber: diff.startLine, endLineNumber: diff.startLine, startColumn: 1, endColumn: 1 }; - editor.revealRange(range, ScrollType.Immediate) + editor.revealRangeInCenter(range, ScrollType.Immediate) commandBarService.setDiffIdx(uri, idx) } } - - const getNextUriIdx = (step: 1 | -1) => { - return stepIdx(currUriIdx, sortedCommandBarURIs.length, step) + return stepIdx(uriIdxInStepper, sortedCommandBarURIs.length, step) } const goToURIIdx = async (idx: number | null) => { if (idx === null) return @@ -101,13 +108,12 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { setTimeout(() => { // check undefined if (!uri) return - const s = commandBarState[uri.fsPath] + const s = commandBarService.stateOfURI[uri.fsPath] if (!s) return const { diffIdx } = s goToDiffIdx(diffIdx) }, 50) - - }, [uri]) + }, [uri, commandBarService]) const currDiffIdx = uri ? commandBarState[uri.fsPath]?.diffIdx ?? null : null @@ -115,11 +121,6 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { const sortedDiffZoneIds = uri ? commandBarState[uri.fsPath]?.sortedDiffZoneIds ?? [] : [] - const nextDiffIdx = getNextDiffIdx(1) - const prevDiffIdx = getNextDiffIdx(-1) - const nextURIIdx = getNextUriIdx(1) - const prevURIIdx = getNextUriIdx(-1) - const isAChangeInThisFile = sortedDiffIds.length !== 0 const isADiffZoneInThisFile = sortedDiffZoneIds.length !== 0 const isADiffZoneInAnyFile = sortedCommandBarURIs.length !== 0 @@ -127,14 +128,13 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { const streamState = uri ? commandBarService.getStreamState(uri) : null const showAcceptRejectAll = streamState === 'idle-has-changes' - - if (!isADiffZoneInAnyFile) { // no changes for the user to accept - return null - } - + const nextDiffIdx = getNextDiffIdx(1) + const prevDiffIdx = getNextDiffIdx(-1) + const nextURIIdx = getNextUriIdx(1) + const prevURIIdx = getNextUriIdx(-1) const upDownDisabled = prevDiffIdx === null || nextDiffIdx === null - const leftRightDisabled = prevURIIdx === null || currUriIdx === null + const leftRightDisabled = prevURIIdx === null || nextURIIdx === null const upButton = - const filesDescription = (isADiffZoneInThisFile ? - currUriIdx !== null && sortedCommandBarURIs.length !== 0 && - `File ${currUriIdx + 1} of ${sortedCommandBarURIs.length}` - : `${sortedCommandBarURIs.length} file${sortedCommandBarURIs.length === 1 ? '' : 's'} changed` - ); - - const changesDescription = (isADiffZoneInThisFile ? - isAChangeInThisFile ? - `Diff ${(currDiffIdx ?? 0) + 1} of ${sortedDiffIds.length}` - : `No changes` - : '' - ); // accept/reject if current URI has changes const onAcceptAll = () => { @@ -231,6 +219,8 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { } + if (!isADiffZoneInAnyFile) return null + const acceptAllButton = + const acceptRejectAllButtons =
    + {acceptAllButton} + {rejectAllButton} +
    // const closeCommandBar = useCallback(() => { // commandService.executeCommand('void.hideCommandBar'); @@ -283,41 +277,43 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { // >x // - const gridLayout =
    - {/* First row */} - {filesDescription && -
    - {leftButton} -
    {/* Divider */} - {rightButton} -
    {/* Divider */} -
    {filesDescription}
    -
    - } + const leftRightUpDownButtons =
    +
    + {/* Changes in file */} + {isADiffZoneInThisFile && +
    + {downButton} + {upButton} +
    + {isAChangeInThisFile ? + `Diff ${(currDiffIdx ?? 0) + 1} of ${sortedDiffIds.length}` + : `No changes` + } +
    +
    + } - {/* Second row */} - {changesDescription && -
    - {upButton} -
    {/* Divider */} - {downButton} -
    {/* Divider */} -
    {changesDescription}
    -
    - } + {/* Files */} + {isADiffZoneInAnyFile && +
    + {leftButton} + {/*
    */} + {rightButton} + {/*
    */} +
    + {currFileIdx !== null ? + `File ${currFileIdx + 1} of ${sortedCommandBarURIs.length}` + : `${sortedCommandBarURIs.length} file${sortedCommandBarURIs.length === 1 ? '' : 's'} changed` + } +
    +
    + } +
    - return
    - {showAcceptRejectAll && -
    - {acceptAllButton} - {rejectAllButton} -
    - } -
    - {gridLayout} - {/* {oldLayout} */} -
    + return
    + {showAcceptRejectAll && acceptRejectAllButtons} + {leftRightUpDownButtons}
    } diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index 271322d5..45048458 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -12,6 +12,7 @@ import { ToolCallParams, ToolDirectoryItem, ToolName, ToolResultType } from '../ 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' // tool use for AI @@ -193,6 +194,7 @@ export class ToolsService implements IToolsService { @IVoidModelService voidModelService: IVoidModelService, @IEditCodeService editCodeService: IEditCodeService, @ITerminalToolService private readonly terminalToolService: ITerminalToolService, + @IVoidCommandBarService private readonly commandBarService: IVoidCommandBarService, ) { const queryBuilder = instantiationService.createInstance(QueryBuilder); @@ -348,6 +350,9 @@ export class ToolsService implements IToolsService { edit: async ({ uri, changeDescription }) => { await voidModelService.initializeModel(uri) + if (this.commandBarService.getStreamState(uri) === 'streaming') { + throw new Error(`The Apply model was already running. This can happen if two agents try editing the same file at the same time. Please try again in a moment.`) + } const res = await editCodeService.startApplying({ uri, applyStr: changeDescription, diff --git a/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts b/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts index 2b01e37b..9fd0c654 100644 --- a/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts +++ b/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts @@ -31,7 +31,11 @@ export interface IVoidCommandBarService { onDidChangeActiveURI: Event<{ uri: URI | null }>; getStreamState: (uri: URI) => 'streaming' | 'idle-has-changes' | 'idle-no-changes'; - setDiffIdx(uri: URI, newIdx: number | null): void + setDiffIdx(uri: URI, newIdx: number | null): void; + + acceptOrRejectAllFiles(opts: { behavior: 'reject' | 'accept' }): void; + anyFileIsStreaming(): boolean; + } @@ -64,7 +68,7 @@ export class VoidCommandBarService extends Disposable implements IVoidCommandBar // depends on uri -> diffZone -> {streaming, diffs} public stateOfURI: { [uri: string]: CommandBarStateType } = {} public sortedURIs: URI[] = [] // keys of state (depends on diffZones in the uri) - private readonly _hooks = new Set() // uriFsPaths + private readonly _listenToTheseURIs = new Set() // uriFsPaths // Emits when a URI's stream state changes between idle, streaming, and acceptRejectAll private readonly _onDidChangeState = new Emitter<{ uri: URI }>(); @@ -76,7 +80,6 @@ export class VoidCommandBarService extends Disposable implements IVoidCommandBar private readonly _onDidChangeActiveURI = new Emitter<{ uri: URI | null }>(); readonly onDidChangeActiveURI = this._onDidChangeActiveURI.event; - constructor( @IInstantiationService private readonly _instantiationService: IInstantiationService, @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, @@ -91,7 +94,7 @@ export class VoidCommandBarService extends Disposable implements IVoidCommandBar // do not add listeners to the same model twice - important, or will see duplicates if (registeredModelURIs.has(model.uri.fsPath)) return registeredModelURIs.add(model.uri.fsPath) - this._hooks.add(model.uri) + this._listenToTheseURIs.add(model.uri) } // initialize all existing models + initialize when a new model mounts this._modelService.getModels().forEach(model => { initializeModel(model) }) @@ -117,7 +120,7 @@ export class VoidCommandBarService extends Disposable implements IVoidCommandBar // mount the command bar const d1 = this._instantiationService.createInstance(AcceptRejectAllFloatingWidget, { editor }); disposablesOfEditorId[id].push(d1); - const d2 = editor.onDidChangeModel((e) => { if (e?.newModelUrl?.scheme === 'file') updateActiveURI() }) + const d2 = editor.onWillChangeModel((e) => { if (e?.newModelUrl?.scheme === 'file') updateActiveURI() }) disposablesOfEditorId[id].push(d2); } const onCodeEditorRemove = (editor: ICodeEditor) => { @@ -133,7 +136,7 @@ export class VoidCommandBarService extends Disposable implements IVoidCommandBar // state updaters this._register(this._editCodeService.onDidAddOrDeleteDiffZones(e => { - for (const uri of this._hooks) { + for (const uri of this._listenToTheseURIs) { if (e.uri.fsPath !== uri.fsPath) continue // --- sortedURIs: delete if empty, add if not in state yet const diffZones = this._getDiffZonesOnURI(uri) @@ -173,7 +176,7 @@ export class VoidCommandBarService extends Disposable implements IVoidCommandBar })) this._register(this._editCodeService.onDidChangeDiffsInDiffZone(e => { - for (const uri of this._hooks) { + for (const uri of this._listenToTheseURIs) { if (e.uri.fsPath !== uri.fsPath) continue // --- sortedURIs: no change // --- state: @@ -191,7 +194,7 @@ export class VoidCommandBarService extends Disposable implements IVoidCommandBar } })) this._register(this._editCodeService.onDidChangeStreamingInDiffZone(e => { - for (const uri of this._hooks) { + for (const uri of this._listenToTheseURIs) { if (e.uri.fsPath !== uri.fsPath) continue // --- sortedURIs: no change // --- state: @@ -342,6 +345,21 @@ export class VoidCommandBarService extends Disposable implements IVoidCommandBar } + anyFileIsStreaming() { + return this.sortedURIs.some(uri => this.getStreamState(uri) === 'streaming') + } + + acceptOrRejectAllFiles(opts: { behavior: 'reject' | 'accept' }) { + const { behavior } = opts + // if anything is streaming, do nothing + const anyIsStreaming = this.anyFileIsStreaming() + if (anyIsStreaming) return + for (const uri of this.sortedURIs) { + this._editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior, removeCtrlKs: false }) + } + } + + } registerSingleton(IVoidCommandBarService, VoidCommandBarService, InstantiationType.Delayed); // delayed is needed here :( diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index bd9dbb1b..aacc5627 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -19,9 +19,9 @@ export const tripleTick = ['```', '```'] const changesExampleContent = `\ // ... existing code ... // {{change 1}} -// // ... existing code ... +// ... existing code ... // {{change 2}} -// // ... existing code ... +// ... existing code ... // {{change 3}} // ... existing code ...` From d4e9dbb82a3dd19fbb6bc5ad402cc7bb753da478 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 21 Mar 2025 19:33:03 -0700 Subject: [PATCH 157/173] rm titles and add .fsPaths --- .../contrib/void/browser/_markerCheckService.ts | 6 +++--- .../contrib/void/browser/autocompleteService.ts | 4 ++-- .../contrib/void/browser/chatThreadService.ts | 10 +++++----- .../react/src/markdown/ApplyBlockHoverButtons.tsx | 11 +---------- .../browser/react/src/sidebar-tsx/SidebarChat.tsx | 2 +- .../react/src/void-command-bar-tsx/VoidCommandBar.tsx | 4 ---- 6 files changed, 12 insertions(+), 25 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/_markerCheckService.ts b/src/vs/workbench/contrib/void/browser/_markerCheckService.ts index 13edb167..be1ca007 100644 --- a/src/vs/workbench/contrib/void/browser/_markerCheckService.ts +++ b/src/vs/workbench/contrib/void/browser/_markerCheckService.ts @@ -39,7 +39,7 @@ class MarkerCheckService extends Disposable implements IMarkerCheckService { console.log(`----------------------------------------------`); - console.log(`${error.resource.toString()}: ${error.startLineNumber} ${error.message} ${error.severity}`); // ! all errors in the file + console.log(`${error.resource.fsPath}: ${error.startLineNumber} ${error.message} ${error.severity}`); // ! all errors in the file try { // Get the text model for the file @@ -122,11 +122,11 @@ class MarkerCheckService extends Disposable implements IMarkerCheckService { // const markers = this._markerService.read({ resource }); // if (markers.length === 0) { - // console.log(`${resource.toString()}: No diagnostics`); + // console.log(`${resource.fsPath}: No diagnostics`); // continue; // } - // console.log(`Diagnostics for ${resource.toString()}:`); + // console.log(`Diagnostics for ${resource.fsPath}:`); // markers.forEach(marker => this._logMarker(marker)); // } // }; diff --git a/src/vs/workbench/contrib/void/browser/autocompleteService.ts b/src/vs/workbench/contrib/void/browser/autocompleteService.ts index 894316bd..34c7025c 100644 --- a/src/vs/workbench/contrib/void/browser/autocompleteService.ts +++ b/src/vs/workbench/contrib/void/browser/autocompleteService.ts @@ -642,7 +642,7 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ const testMode = false - const docUriStr = model.uri.toString(); + const docUriStr = model.uri.fsPath; const prefixAndSuffix = getPrefixAndSuffixInfo(model, position) const { prefix, suffix } = prefixAndSuffix @@ -916,7 +916,7 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ if (!resource) return; const model = this._modelService.getModel(resource) if (!model) return; - const docUriStr = resource.toString(); + const docUriStr = resource.fsPath; if (!this._autocompletionsOfDocument[docUriStr]) return; const { prefix, } = getPrefixAndSuffixInfo(model, position) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index e1152c3b..daac5fa3 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -269,7 +269,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { const newStagingSelections: StagingSelectionItem[] = oldStagingSelections.filter(s => !s.state?.wasAddedAsCurrentFile); // add the new file if it doesn't exist - const fileIsAdded = oldStagingSelections.some(s => s.type === 'File' && s.fileURI.toString() === newStagingSelection.fileURI.toString()) + const fileIsAdded = oldStagingSelections.some(s => s.type === 'File' && s.fileURI.fsPath === newStagingSelection.fileURI.fsPath) if (!fileIsAdded) { newStagingSelections.push(newStagingSelection) } @@ -288,7 +288,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { // this.setCurrentMessageState(focusedMessageIdx, { stagingSelections: newSelections }); // // if the file already exists, do nothing - // const alreadyHasFile = oldStagingSelections.some(s => s.type === 'File' && s.fileURI.toString() === newSelection.fileURI.toString()) + // const alreadyHasFile = oldStagingSelections.some(s => s.type === 'File' && s.fileURI.fsPath === newSelection.fileURI.fsPath) // if (alreadyHasFile) { return; } // const filteredStagingSelections = oldStagingSelections.filter(s => !s.state?.wasAddedDuringFileChange); // remove all old selectons that were added during a file change @@ -1023,7 +1023,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { // get history of all AI and user added files in conversation + store in reverse order (MRU) const prevUris = this._getAllSelections(threadId) .map(s => s.fileURI) - .filter((uri, index, array) => array.findIndex(u => u.toString() === uri.toString()) === index) // O(n^2) but this is small + .filter((uri, index, array) => array.findIndex(u => u.fsPath === uri.fsPath) === index) // O(n^2) but this is small .reverse() @@ -1039,7 +1039,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { // shorten it // TODO make this logic more general - const prevUriStrs = prevUris.map(uri => uri.toString()) + const prevUriStrs = prevUris.map(uri => uri.fsPath) const shortenedUriStrs = shorten(prevUriStrs) let displayText = shortenedUriStrs[idx] const ellipsisIdx = displayText.lastIndexOf('…/'); @@ -1064,7 +1064,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { if (doesUriMatchTarget(uri)) { // TODO make this logic more general - const prevUriStrs = prevUris.map(uri => uri.toString()) + const prevUriStrs = prevUris.map(uri => uri.fsPath) const shortenedUriStrs = shorten(prevUriStrs) let displayText = shortenedUriStrs[idx] const ellipsisIdx = displayText.lastIndexOf('…/'); 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 b37f4c69..b09a22ba 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 @@ -17,15 +17,13 @@ enum CopyButtonText { type IconButtonProps = { onClick: () => void; - title: string Icon: LucideIcon disabled?: boolean className?: string } -export const IconShell1 = ({ onClick, title, Icon, disabled, className }: IconButtonProps) => ( +export const IconShell1 = ({ onClick, Icon, disabled, className }: IconButtonProps) => ( const downButton = const leftButton = const rightButton = From 847b57c4068c015a676363c3766ac52e1f1b7ef8 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Fri, 21 Mar 2025 19:40:57 -0700 Subject: [PATCH 158/173] bug --- .../contrib/void/browser/chatThreadService.ts | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index daac5fa3..cf641fa6 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -122,22 +122,14 @@ export type ThreadStreamState = { } } -const defaultThreadAttributes = () => { +const newThreadObject = () => { const now = new Date().toISOString() return { + id: generateUuid(), createdAt: now, lastModified: now, messages: [], state: defaultThreadState, - } -} - -const newThreadObject = () => { - - return { - id: generateUuid(), - ...defaultThreadAttributes() - } satisfies ChatThreads[string] } @@ -1253,11 +1245,16 @@ class ChatThreadService extends Disposable implements IChatThreadService { const { allThreads: currentThreads } = this.state for (const threadId in currentThreads) { if (currentThreads[threadId]!.messages.length === 0) { - // clear the thread - currentThreads[threadId]! = { id: currentThreads[threadId]!.id, ...defaultThreadAttributes() } + // switch to the thread this.switchToThread(threadId) - return + + // add the current file as a staging selection + const model = this._codeEditorService.getActiveCodeEditor()?.getModel() + if (model) { + this._setCurrentThreadState({ ...defaultThreadState, stagingSelections: [{ type: 'File', fileURI: model.uri, language: model.getLanguageId(), selectionStr: null, range: null, state: { isOpened: false, wasAddedAsCurrentFile: true } }] }) + } + return; } } // otherwise, start a new thread From 3e584d01a30e3836dc688fccc22752f3576741ea Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Fri, 21 Mar 2025 19:53:12 -0700 Subject: [PATCH 159/173] focus on chatmessage edit --- .../contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx | 2 ++ 1 file changed, 2 insertions(+) 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 4af3c5da..7622e310 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 @@ -810,6 +810,7 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isCommitted }: { chatMe const accessor = useAccessor() const chatThreadsService = accessor.get('IChatThreadService') + const sidebarStateService = accessor.get('ISidebarStateService') // global state let isBeingEdited = false @@ -905,6 +906,7 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isCommitted }: { chatMe } catch (e) { console.error('Error while editing message:', e) } + sidebarStateService.fireFocusChat() } const onAbort = () => { From 00734ce327a1947a8962c5290d136a0f297260e9 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 21 Mar 2025 19:58:27 -0700 Subject: [PATCH 160/173] update commandbar --- .../void-command-bar-tsx/VoidCommandBar.tsx | 32 +++++++++---------- .../void/electron-main/metricsMainService.ts | 3 +- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx index 6b8b0c1b..6655306e 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx @@ -121,7 +121,7 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { const sortedDiffZoneIds = uri ? commandBarState[uri.fsPath]?.sortedDiffZoneIds ?? [] : [] - const isAChangeInThisFile = sortedDiffIds.length !== 0 + const isADiffInThisFile = sortedDiffIds.length !== 0 const isADiffZoneInThisFile = sortedDiffZoneIds.length !== 0 const isADiffZoneInAnyFile = sortedCommandBarURIs.length !== 0 @@ -134,11 +134,11 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { const prevURIIdx = getNextUriIdx(-1) const upDownDisabled = prevDiffIdx === null || nextDiffIdx === null - const leftRightDisabled = prevURIIdx === null || nextURIIdx === null + const leftRightDisabled = prevURIIdx === null || nextURIIdx === null // || (sortedCommandBarURIs.length === 1 && isADiffZoneInThisFile) const upButton = - const leftRightUpDownButtons =
    + const leftRightUpDownButtons =
    {/* Changes in file */} - {isADiffZoneInThisFile && -
    + { +
    {downButton} {upButton} -
    - {isAChangeInThisFile ? + + {isADiffInThisFile ? `Diff ${(currDiffIdx ?? 0) + 1} of ${sortedDiffIds.length}` : `No changes` } -
    +
    } {/* Files */} - {isADiffZoneInAnyFile && -
    + { +
    {leftButton} {/*
    */} {rightButton} {/*
    */} -
    + {currFileIdx !== null ? `File ${currFileIdx + 1} of ${sortedCommandBarURIs.length}` : `${sortedCommandBarURIs.length} file${sortedCommandBarURIs.length === 1 ? '' : 's'} changed` } -
    +
    }
    diff --git a/src/vs/workbench/contrib/void/electron-main/metricsMainService.ts b/src/vs/workbench/contrib/void/electron-main/metricsMainService.ts index 592f79c4..bfc0a8c9 100644 --- a/src/vs/workbench/contrib/void/electron-main/metricsMainService.ts +++ b/src/vs/workbench/contrib/void/electron-main/metricsMainService.ts @@ -96,7 +96,7 @@ export class MetricsMainService extends Disposable implements IMetricsService { // very important to await whenReady! await this._appStorage.whenReady - const { commit, version, quality } = this._productService + const { commit, version, voidVersion, quality } = this._productService const isDevMode = !this._envMainService.isBuilt // found in abstractUpdateService.ts @@ -104,6 +104,7 @@ export class MetricsMainService extends Disposable implements IMetricsService { this._initProperties = { commit, vscodeVersion: version, + voidVersion, os, quality, distinctId: this.distinctId, From 1675879fa4ea87a37571b1b6a668688fd24f7af7 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Fri, 21 Mar 2025 20:10:38 -0700 Subject: [PATCH 161/173] ux for commandbar --- .../src/void-command-bar-tsx/VoidCommandBar.tsx | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx index 6655306e..99cf9bc5 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx @@ -82,7 +82,7 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { if (idx !== null) { const diffid = sortedDiffIds[idx] const diff = editCodeService.diffOfId[diffid] - const range = { startLineNumber: diff.startLine, endLineNumber: diff.startLine, startColumn: 1, endColumn: 1 }; + const range = { startLineNumber: diff.startLine - 1, endLineNumber: diff.startLine - 1, startColumn: 1, endColumn: 1 }; editor.revealRangeInCenter(range, ScrollType.Immediate) commandBarService.setDiffIdx(uri, idx) } @@ -276,19 +276,20 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { const leftRightUpDownButtons =
    {/* Changes in file */} - { -
    - {downButton} + {isADiffZoneInThisFile && +
    {upButton} - + {downButton} +
    {isADiffInThisFile ? `Diff ${(currDiffIdx ?? 0) + 1} of ${sortedDiffIds.length}` - : `No changes` + : streamState === 'streaming' ? + 'No changes yet' + : `No changes` } - +
    } - {/* Files */} {
    From 22dd42ba702e281c42afeec8d24052f21dd663e0 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 21 Mar 2025 21:17:04 -0700 Subject: [PATCH 162/173] misc improvements --- .../contrib/void/browser/chatThreadService.ts | 7 +- .../void-command-bar-tsx/VoidCommandBar.tsx | 105 ++++++++---------- .../void/browser/voidCommandBarService.ts | 2 +- .../contrib/void/common/prompt/prompts.ts | 2 +- 4 files changed, 53 insertions(+), 63 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index cf641fa6..e3024792 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -732,11 +732,10 @@ class ChatThreadService extends Disposable implements IChatThreadService { }) { // define helper functions so we can tell what's going on + // for now, do not recompute selections as we run (it seems to confuse tool-use models) + const selectionsStr = await chat_selectionsString(prevSelns, currSelns, this._voidModelService) // all the file CONTENTS or "selections" de-duped + const userMessageFullContent = chat_lastUserMessageWithFilesAdded(userMessageContent, selectionsStr) // full last message: user message + CONTENTS of all files const getLatestMessages = async () => { - // recompute files in last message - const selectionsStr = await chat_selectionsString(prevSelns, currSelns, this._voidModelService) // all the file CONTENTS or "selections" de-duped - const userMessageFullContent = chat_lastUserMessageWithFilesAdded(userMessageContent, selectionsStr) // full last message: user message + CONTENTS of all files - // replace last userMessage with userMessageFullContent (which contains all the files too) const thread = this.state.allThreads[threadId] const latestMessages = thread?.messages ?? [] diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx index 99cf9bc5..ca8437eb 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx @@ -15,8 +15,6 @@ import { VoidCommandBarProps } from '../../../voidCommandBarService.js'; export const VoidCommandBarMain = ({ uri, editor }: VoidCommandBarProps) => { const isDark = useIsDark() - if (uri?.scheme !== 'file') return null // don't show in editors that we made, they must be files - return
    @@ -26,7 +24,6 @@ export const VoidCommandBarMain = ({ uri, editor }: VoidCommandBarProps) => { - const stepIdx = (currIdx: number | null, len: number, step: -1 | 1) => { if (len === 0) return null return ((currIdx ?? 0) + step + len) % len // for some reason, small negatives are kept negative. just add len to offset @@ -45,6 +42,10 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { const { state: commandBarState, sortedURIs: sortedCommandBarURIs } = useCommandBarState() + // useEffect(() => { + // console.log('MOUNTING!!!') + // }, []) + // latestUriIdx is used to remember place in leftRight const _latestValidUriIdxRef = useRef(null) @@ -60,7 +61,19 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { : _latestValidUriIdxRef.current < sortedCommandBarURIs.length ? _latestValidUriIdxRef.current : null + // when change URI, scroll to the proper spot + useEffect(() => { + setTimeout(() => { + // check undefined + if (!uri) return + const s = commandBarService.stateOfURI[uri.fsPath] + if (!s) return + const { diffIdx } = s + goToDiffIdx(diffIdx ?? 0) + }, 50) + }, [uri, commandBarService]) + if (uri?.scheme !== 'file') return null // don't show in editors that we made, they must be files const getNextDiffIdx = (step: 1 | -1) => { // check undefined @@ -73,19 +86,19 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { return nextDiffIdx } const goToDiffIdx = (idx: number | null) => { + if (idx === null) return // check undefined if (!uri) return const s = commandBarState[uri.fsPath] if (!s) return const { sortedDiffIds } = s // reveal - if (idx !== null) { - const diffid = sortedDiffIds[idx] - const diff = editCodeService.diffOfId[diffid] - const range = { startLineNumber: diff.startLine - 1, endLineNumber: diff.startLine - 1, startColumn: 1, endColumn: 1 }; - editor.revealRangeInCenter(range, ScrollType.Immediate) - commandBarService.setDiffIdx(uri, idx) - } + const diffid = sortedDiffIds[idx] + if (diffid === undefined) return + const diff = editCodeService.diffOfId[diffid] + if (!diff) return + editor.revealLineNearTop(diff.startLine, ScrollType.Immediate) + commandBarService.setDiffIdx(uri, idx) } const getNextUriIdx = (step: 1 | -1) => { return stepIdx(uriIdxInStepper, sortedCommandBarURIs.length, step) @@ -101,21 +114,6 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { } } - - - // when change URI, scroll to the proper spot - useEffect(() => { - setTimeout(() => { - // check undefined - if (!uri) return - const s = commandBarService.stateOfURI[uri.fsPath] - if (!s) return - const { diffIdx } = s - goToDiffIdx(diffIdx) - }, 50) - }, [uri, commandBarService]) - - const currDiffIdx = uri ? commandBarState[uri.fsPath]?.diffIdx ?? null : null const sortedDiffIds = uri ? commandBarState[uri.fsPath]?.sortedDiffIds ?? [] : [] const sortedDiffZoneIds = uri ? commandBarState[uri.fsPath]?.sortedDiffZoneIds ?? [] : [] @@ -140,8 +138,7 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { className={` size-6 rounded cursor-default hover:bg-void-bg-1-alt - --border border-void-border-3 focus:border-void-border-1 - `} + `}// --border border-void-border-3 focus:border-void-border-1 disabled={upDownDisabled} onClick={() => { goToDiffIdx(prevDiffIdx) }} onKeyDown={(e) => { @@ -156,7 +153,6 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { className={` size-6 rounded cursor-default hover:bg-void-bg-1-alt - --border border-void-border-3 focus:border-void-border-1 `} disabled={upDownDisabled} onClick={() => { goToDiffIdx(nextDiffIdx) }} @@ -172,7 +168,6 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { className={` size-6 rounded cursor-default hover:bg-void-bg-1-alt - --border border-void-border-3 focus:border-void-border-1 `} disabled={leftRightDisabled} onClick={() => goToURIIdx(prevURIIdx)} @@ -188,7 +183,6 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { className={` size-6 rounded cursor-default hover:bg-void-bg-1-alt - --border border-void-border-3 focus:border-void-border-1 `} disabled={leftRightDisabled} onClick={() => goToURIIdx(nextURIIdx)} @@ -218,7 +212,7 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { if (!isADiffZoneInAnyFile) return null const acceptAllButton = - const leftRightUpDownButtons =
    + const leftRightUpDownButtons =
    {/* Changes in file */} - {isADiffZoneInThisFile && -
    - {upButton} - {downButton} -
    - {isADiffInThisFile ? +
    + {downButton} + {upButton} + + {isADiffInThisFile ? `Diff ${(currDiffIdx ?? 0) + 1} of ${sortedDiffIds.length}` : streamState === 'streaming' ? 'No changes yet' : `No changes` } -
    -
    - } + +
    + {/* Files */} - { -
    - {leftButton} - {/*
    */} - {rightButton} - {/*
    */} - - {currFileIdx !== null ? - `File ${currFileIdx + 1} of ${sortedCommandBarURIs.length}` - : `${sortedCommandBarURIs.length} file${sortedCommandBarURIs.length === 1 ? '' : 's'} changed` - } - -
    - } +
    + {leftButton} + {/*
    */} + {rightButton} + {/*
    */} + + {currFileIdx !== null ? + `File ${currFileIdx + 1} of ${sortedCommandBarURIs.length}` + : `${sortedCommandBarURIs.length} file${sortedCommandBarURIs.length === 1 ? '' : 's'} changed` + } + +
    - return
    + return
    {showAcceptRejectAll && acceptRejectAllButtons} {leftRightUpDownButtons} diff --git a/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts b/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts index 9fd0c654..4b298ac8 100644 --- a/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts +++ b/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts @@ -420,7 +420,7 @@ class AcceptRejectAllFloatingWidget extends Widget implements IOverlayWidget { this._register(editor.onDidChangeModel((model) => { const uri = model.newModelUrl - res.rerender({ uri, editor }) + res.rerender({ uri, editor } satisfies VoidCommandBarProps) })) }); diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index aacc5627..b84d3af5 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -147,7 +147,7 @@ Here's an example of a good description:\n${editToolDescription}.` export const chat_systemMessage = (workspaces: string[], runningTerminalIds: string[], mode: ChatMode) => `\ You are an expert coding ${mode === 'agent' ? 'agent' : 'assistant'} that runs in the Void code editor. Your job is \ ${mode === 'agent' ? `to help the user develop, run, deploy, and make changes to their codebase. You should ALWAYS bring user's task to completion to the fullest extent possible, calling tools to make all necessary changes. Do not be lazy.` - : mode === 'gather' ? `to search and understand their codebase by reading files and content and providing references to help with their query.` + : mode === 'gather' ? `to search and understand the user's codebase. You MUST use tools to read files and help the user understand the codebase, even if you were initially given files.` : mode === 'normal' ? `to assist the user with their coding tasks.` : ''} You will be given instructions to follow from the user, \`INSTRUCTIONS\`. You may also be given a list of files that the user has specifically selected, \`SELECTIONS\`. From 75f1a2d79b9b021bc35348b2bc89842f50f0ec45 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 21 Mar 2025 21:20:57 -0700 Subject: [PATCH 163/173] format --- .../src/void-command-bar-tsx/VoidCommandBar.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx index ca8437eb..7ad03ee9 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx @@ -274,12 +274,12 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { {downButton} {upButton} - {isADiffInThisFile ? - `Diff ${(currDiffIdx ?? 0) + 1} of ${sortedDiffIds.length}` - : streamState === 'streaming' ? - 'No changes yet' - : `No changes` - } + {isADiffInThisFile ? + `Diff ${(currDiffIdx ?? 0) + 1} of ${sortedDiffIds.length}` + : streamState === 'streaming' ? + 'No changes yet' + : `No changes` + }
    From f60d5cad2fc9da125c1e7cad1dd66bdde6d22c99 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 21 Mar 2025 21:22:21 -0700 Subject: [PATCH 164/173] void agent --- src/vs/workbench/contrib/void/browser/editCodeService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index e768fe6d..fcb4e06f 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -805,7 +805,7 @@ class EditCodeService extends Disposable implements IEditCodeService { const elt: IUndoRedoElement = { type: UndoRedoElementType.Resource, resource: uri, - label: 'Void Changes', + label: 'Void Agent', code: 'undoredo.editCode', undo: () => { opts?.onWillUndo?.(); restoreDiffAreas(beforeSnapshot); }, redo: () => { if (afterSnapshot) restoreDiffAreas(afterSnapshot) } From da6710a079fcf360e46cb92d8e965b4e726b49e7 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 21 Mar 2025 21:45:17 -0700 Subject: [PATCH 165/173] current file fix will vs did --- .../void/browser/terminalToolService.ts | 2 +- .../void/browser/voidCommandBarService.ts | 22 ++++++------------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/terminalToolService.ts b/src/vs/workbench/contrib/void/browser/terminalToolService.ts index c9cbd178..5876e54b 100644 --- a/src/vs/workbench/contrib/void/browser/terminalToolService.ts +++ b/src/vs/workbench/contrib/void/browser/terminalToolService.ts @@ -143,8 +143,8 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ openTerminal: ITerminalToolService['openTerminal'] = async (terminalId) => { if (!terminalId) return - const terminal = this.terminalInstanceOfId[terminalId] + if (!terminal) return // should never happen this.terminalService.setActiveInstance(terminal) await this.terminalService.focusActiveInstance() } diff --git a/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts b/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts index 4b298ac8..7711068d 100644 --- a/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts +++ b/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts @@ -103,12 +103,6 @@ export class VoidCommandBarService extends Disposable implements IVoidCommandBar - const updateActiveURI = () => { - const currentUri = this._codeEditorService.getActiveCodeEditor()?.getModel()?.uri ?? null - this.activeURI = currentUri - if (!currentUri) return; - this._onDidChangeActiveURI.fire({ uri: currentUri }) - } // for every new editor, add the floating widget and update active URI @@ -120,7 +114,11 @@ export class VoidCommandBarService extends Disposable implements IVoidCommandBar // mount the command bar const d1 = this._instantiationService.createInstance(AcceptRejectAllFloatingWidget, { editor }); disposablesOfEditorId[id].push(d1); - const d2 = editor.onWillChangeModel((e) => { if (e?.newModelUrl?.scheme === 'file') updateActiveURI() }) + const d2 = editor.onDidChangeModel((e) => { + if (e.newModelUrl?.scheme !== 'file') return + this.activeURI = e.newModelUrl; + this._onDidChangeActiveURI.fire({ uri: e.newModelUrl }) + }) disposablesOfEditorId[id].push(d2); } const onCodeEditorRemove = (editor: ICodeEditor) => { @@ -410,21 +408,15 @@ class AcceptRejectAllFloatingWidget extends Widget implements IOverlayWidget { editor.addOverlayWidget(this); this.instantiationService.invokeFunction(accessor => { - const uri = editor.getModel()?.uri || null - const res = mountVoidCommandBar(root, accessor, { uri, editor } satisfies VoidCommandBarProps) if (!res) return - this._register(toDisposable(() => res.dispose?.())) - - this._register(editor.onDidChangeModel((model) => { + this._register(editor.onWillChangeModel((model) => { const uri = model.newModelUrl res.rerender({ uri, editor } satisfies VoidCommandBarProps) })) - - }); - + }) } From 9edca589a003b4e283342af88b0b8993fed92ca1 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 21 Mar 2025 21:49:08 -0700 Subject: [PATCH 166/173] 1.0.2 --- product.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/product.json b/product.json index cea8f170..00cc284c 100644 --- a/product.json +++ b/product.json @@ -1,7 +1,7 @@ { "nameShort": "Void", "nameLong": "Void", - "voidVersion": "1.0.3", + "voidVersion": "1.0.2", "applicationName": "void", "dataFolderName": ".void-editor", "win32MutexName": "voideditor", From a6f1b1e0ad39afa74da7409b39cf2f32aa665455 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 21 Mar 2025 21:52:53 -0700 Subject: [PATCH 167/173] window --- .../workbench/contrib/void/browser/voidUpdateActions.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/voidUpdateActions.ts b/src/vs/workbench/contrib/void/browser/voidUpdateActions.ts index 4296c358..d08d0890 100644 --- a/src/vs/workbench/contrib/void/browser/voidUpdateActions.ts +++ b/src/vs/workbench/contrib/void/browser/voidUpdateActions.ts @@ -80,13 +80,13 @@ class VoidUpdateWorkbenchContribution extends Disposable implements IWorkbenchCo } // check once 5 seconds after mount - - const initId = setTimeout(() => autoCheck(), 5 * 1000) - this._register({ dispose: () => clearTimeout(initId) }) - // check every 3 hours const { window } = dom.getActiveWindow() + const initId = window.setTimeout(() => autoCheck(), 5 * 1000) + this._register({ dispose: () => window.clearTimeout(initId) }) + + const intervalId = window.setInterval(() => autoCheck(), 3 * 60 * 60 * 1000) // every 3 hrs this._register({ dispose: () => window.clearInterval(intervalId) }) From db2c60db821d402065e412ec8c30a8d702b9f425 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 21 Mar 2025 21:53:42 -0700 Subject: [PATCH 168/173] rm --- .../workbench/contrib/void/browser/editCodeServiceInterface.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts b/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts index 2e0239e4..8173f837 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts @@ -32,9 +32,6 @@ export type AddCtrlKOpts = { editor: ICodeEditor, } -export type URIAcceptRejectState = 'idle' | 'acceptRejectAll' | 'streaming' - - export const IEditCodeService = createDecorator('editCodeService'); export interface IEditCodeService { From c6376eb1dbc05b13e7805af629d01ee5240590c2 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 21 Mar 2025 21:55:20 -0700 Subject: [PATCH 169/173] 1 --- src/vs/workbench/contrib/void/browser/metricsPollService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/void/browser/metricsPollService.ts b/src/vs/workbench/contrib/void/browser/metricsPollService.ts index 037aa1d6..ab4d92d8 100644 --- a/src/vs/workbench/contrib/void/browser/metricsPollService.ts +++ b/src/vs/workbench/contrib/void/browser/metricsPollService.ts @@ -33,7 +33,7 @@ class MetricsPollService extends Disposable implements IMetricsPollService { // initial state const { window } = dom.getActiveWindow() - let i = 0 + let i = 1 this.intervalID = window.setInterval(() => { this.metricsService.capture('Alive', { iv1: i }) From ec2ae0e190ecaa313b111761cfd26fd347dab4a6 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 21 Mar 2025 22:31:24 -0700 Subject: [PATCH 170/173] more persistent update message --- .../contrib/void/browser/voidUpdateActions.ts | 41 ++++++++++++++++--- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/voidUpdateActions.ts b/src/vs/workbench/contrib/void/browser/voidUpdateActions.ts index d08d0890..ce71b601 100644 --- a/src/vs/workbench/contrib/void/browser/voidUpdateActions.ts +++ b/src/vs/workbench/contrib/void/browser/voidUpdateActions.ts @@ -17,11 +17,41 @@ import * as dom from '../../../../base/browser/dom.js'; -const notifyYesUpdate = (notifService: INotificationService, msg?: string) => { - const message = msg || 'This is a very old version of void, please download the latest version! [Void Editor](https://voideditor.com/download-beta)!' - notifService.notify({ +const notifyYesUpdate = (notifService: INotificationService, res: { message?: string } = {}) => { + const message = res?.message || 'This is a very old version of Void, please download the latest version! [Void Editor](https://voideditor.com/download-beta)!' + const notifController = notifService.notify({ severity: Severity.Info, message: message, + sticky: true, + source: 'Void Team', + actions: { + primary: [{ + id: 'void.updater.update', + enabled: true, + label: `Reinstall`, + tooltip: '', + class: undefined, + run: () => { + const { window } = dom.getActiveWindow() + window.open('https://voideditor.com/download-beta') + } + }, + { + id: 'void.updater.site', + enabled: true, + label: `Visit Site`, + tooltip: '', + class: undefined, + run: () => { + const { window } = dom.getActiveWindow() + window.open('https://voideditor.com/') + } + }] + }, + }) + const d = notifController.onDidClose(() => { + notifyYesUpdate(notifService, res) + d.dispose() }) } const notifyNoUpdate = (notifService: INotificationService) => { @@ -35,6 +65,7 @@ const notifyErrChecking = (notifService: INotificationService) => { notifService.notify({ severity: Severity.Info, message: message, + sticky: true, }) } @@ -57,7 +88,7 @@ registerAction2(class extends Action2 { metricsService.capture('Void Update Manual: Checking...', {}) const res = await voidUpdateService.check() if (!res) { notifyErrChecking(notifService); metricsService.capture('Void Update Manual: Error', { res }) } - else if (res.hasUpdate) { notifyYesUpdate(notifService, res.message); metricsService.capture('Void Update Manual: Yes', { res }) } + else if (res.hasUpdate) { notifyYesUpdate(notifService, res); metricsService.capture('Void Update Manual: Yes', { res }) } else if (!res.hasUpdate) { notifyNoUpdate(notifService); metricsService.capture('Void Update Manual: No', { res }) } } }) @@ -75,7 +106,7 @@ class VoidUpdateWorkbenchContribution extends Disposable implements IWorkbenchCo this.metricsService.capture('Void Update Startup: Checking...', {}) const res = await this.voidUpdateService.check() if (!res) { notifyErrChecking(this.notifService); this.metricsService.capture('Void Update Startup: Error', { res }) } - else if (res.hasUpdate) { notifyYesUpdate(this.notifService, res.message); this.metricsService.capture('Void Update Startup: Yes', { res }) } + else if (res.hasUpdate) { notifyYesUpdate(this.notifService, res); this.metricsService.capture('Void Update Startup: Yes', { res }) } else if (!res.hasUpdate) { this.metricsService.capture('Void Update Startup: No', { res }) } // display nothing if up to date } From 995f39b61f03952af6ccb26d3ccaea6590540850 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 21 Mar 2025 22:38:26 -0700 Subject: [PATCH 171/173] No X button --- src/vs/workbench/contrib/void/browser/voidUpdateActions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/voidUpdateActions.ts b/src/vs/workbench/contrib/void/browser/voidUpdateActions.ts index ce71b601..da0e5fc7 100644 --- a/src/vs/workbench/contrib/void/browser/voidUpdateActions.ts +++ b/src/vs/workbench/contrib/void/browser/voidUpdateActions.ts @@ -23,7 +23,7 @@ const notifyYesUpdate = (notifService: INotificationService, res: { message?: st severity: Severity.Info, message: message, sticky: true, - source: 'Void Team', + progress: { worked: 0, total: 100 }, actions: { primary: [{ id: 'void.updater.update', @@ -39,7 +39,7 @@ const notifyYesUpdate = (notifService: INotificationService, res: { message?: st { id: 'void.updater.site', enabled: true, - label: `Visit Site`, + label: `Void Site`, tooltip: '', class: undefined, run: () => { From 6ecf7be826778a3d34bdf0aa2ad3d1d2cac7b65e Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Sat, 22 Mar 2025 00:02:06 -0700 Subject: [PATCH 172/173] fix search+replace on empty files --- .../contrib/void/browser/editCodeService.ts | 39 +++++++++++++------ .../void-command-bar-tsx/VoidCommandBar.tsx | 2 +- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index fcb4e06f..ccf5484e 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -1172,7 +1172,14 @@ class EditCodeService extends Disposable implements IEditCodeService { } else if (opts.from === 'ClickApply') { if (this._settingsService.state.globalSettings.enableFastApply) { - res = await this._initializeSearchAndReplaceStream(opts) // fast apply + const numCharsInFile = this._fileLengthOfGivenURI(opts.uri) + if (numCharsInFile === null) return null + if (numCharsInFile < 1000) { // slow apply for short files (especially important for empty files) + res = await this._initializeWriteoverStream(opts) + } + else { + res = await this._initializeSearchAndReplaceStream(opts) // fast apply + } } else { res = await this._initializeWriteoverStream(opts) // rewrite @@ -1311,7 +1318,7 @@ class EditCodeService extends Disposable implements IEditCodeService { let ctrlKZoneIfQuickEdit: CtrlKZone | null = null if (from === 'ClickApply') { - const uri_ = this._getActiveEditorURI() + const uri_ = this._uriOfGivenURI(opts.uri) if (!uri_) return uri = uri_ startRange = 'fullFile' @@ -1505,19 +1512,29 @@ class EditCodeService extends Disposable implements IEditCodeService { - - private async _initializeSearchAndReplaceStream(opts: StartApplyingOpts & { from: 'ClickApply' }): Promise<[DiffZone, Promise] | undefined> { - const { from, applyStr, uri: givenURI, } = opts - let uri: URI - + _uriOfGivenURI(givenURI: URI | 'current') { if (givenURI === 'current') { const uri_ = this._getActiveEditorURI() if (!uri_) return - uri = uri_ - } - else { - uri = givenURI + return uri_ } + return givenURI + } + _fileLengthOfGivenURI(givenURI: URI | 'current') { + const uri = this._uriOfGivenURI(givenURI) + if (!uri) return null + const { model } = this._voidModelService.getModel(uri) + if (!model) return null + const numCharsInFile = model.getValueLength(EndOfLinePreference.LF) + return numCharsInFile + } + + + private async _initializeSearchAndReplaceStream(opts: StartApplyingOpts & { from: 'ClickApply' }): Promise<[DiffZone, Promise] | undefined> { + const { from, applyStr, uri: givenURI, } = opts + + const uri = this._uriOfGivenURI(givenURI) + if (!uri) return await this._voidModelService.initializeModel(uri) const { model } = this._voidModelService.getModel(uri) diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx index 7ad03ee9..25ffbaec 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx @@ -271,8 +271,8 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
    {/* Changes in file */}
    - {downButton} {upButton} + {downButton} {isADiffInThisFile ? `Diff ${(currDiffIdx ?? 0) + 1} of ${sortedDiffIds.length}` From f75b4f34d86d7e7c7dd04622641d7c2a9e3dbb73 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Sat, 22 Mar 2025 00:39:22 -0700 Subject: [PATCH 173/173] fixes --- .../contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx | 2 +- src/vs/workbench/contrib/void/common/prompt/prompts.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) 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 7622e310..77697ee8 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 @@ -2020,7 +2020,7 @@ export const SidebarChat = () => { const proposed = toolNameSoFar && toolNames.includes(toolNameSoFar as ToolName) ? titleOfToolName[toolNameSoFar as ToolName]?.proposed : toolNameSoFar const toolTitle = typeof proposed === 'function' ? proposed(null) : proposed const currStreamingToolHTML = toolIsLoading ? - writing} /> + Getting parameters} /> : null const allMessagesHTML = [...previousMessagesHTML, currStreamingMessageHTML, currStreamingToolHTML] diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index b84d3af5..b6e27ce6 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -165,7 +165,8 @@ ${mode === 'agent' ? `\ - Only use tools if they help you accomplish the user's goal. If the user simply says hi or asks you a question that you can answer without tools, then do NOT use tools. - ALWAYS use tools to take actions. For example, if you would like to edit a file, you MUST use a tool.` : mode === 'gather' ? `\ -- Your primary use of tools should be to gather information to help the user understand the codebase and answer their query.` +- Your primary use of tools should be to gather information to help the user understand the codebase and answer their query. +- You should extensively read files, types, etc and gather relevant context.` : ''} - If you think you should use tools, you do not need to ask for permission. Feel free to call tools whenever you'd like. You can use them to understand the codebase, ${mode === 'agent' ? 'run terminal commands, edit files, ' : 'gather relevant files and information, '}etc. - NEVER refer to a tool by name when speaking with the user (NEVER say something like "I'm going to use \`tool_name\`"). Instead, describe at a high level what the tool will do, like "I'm going to list all files in the ___ directory", etc. Also do not refer to "pages" of results, just say you're getting more results.