mirror of
https://github.com/voideditor/void
synced 2026-05-22 17:08:25 +00:00
Merge pull request #310 from voideditor/model-selection
Tool use progress + UI
This commit is contained in:
commit
5eb2bcfef6
43 changed files with 4154 additions and 2707 deletions
|
|
@ -12,7 +12,7 @@ There are a few ways to contribute:
|
|||
|
||||
### Codebase Guide
|
||||
|
||||
We highly recommend reading [this](https://github.com/microsoft/vscode/wiki/Source-Code-Organization) article on VSCode's sourcecode organization.
|
||||
We [highly recommend reading this](https://github.com/microsoft/vscode/wiki/Source-Code-Organization) article on VSCode's sourcecode organization too. Void's codebase is pretty simple when you know what a service is and what `browser/` and `common/` mean, and the article covers all the jargon.
|
||||
|
||||
<!-- ADD BLOG HERE
|
||||
We wrote a [guide to working in VSCode].
|
||||
|
|
@ -61,13 +61,14 @@ To build Void, open `void/` inside VSCode. Then open your terminal and run:
|
|||
3. Build Void.
|
||||
- Press <kbd>Cmd+Shift+B</kbd> (Mac).
|
||||
- Press <kbd>Ctrl+Shift+B</kbd> (Windows/Linux).
|
||||
- This step can take ~5 min. The build is done when you see two check marks.
|
||||
- This step can take ~5 min. The build is done when you see two check marks (one of the items will continue spinning indefinitely - it compiles our React code).
|
||||
4. Run Void.
|
||||
- Run `./scripts/code.sh` (Mac/Linux).
|
||||
- Run `./scripts/code.bat` (Windows).
|
||||
6. Nice-to-knows.
|
||||
- You can always press <kbd>Ctrl+R</kbd> (<kbd>Cmd+R</kbd>) inside the new window to reload and see your new changes. It's faster than <kbd>Ctrl+Shift+P</kbd> and `Reload Window`.
|
||||
- You might want to add the flags `--user-data-dir ./.tmp/user-data --extensions-dir ./.tmp/extensions` to the above run command, which lets you delete the `.tmp` folder to reset any IDE changes you made when testing.
|
||||
- You can kill any of the build scripts by pressing `Ctrl+D` in VSCode terminal. If you press `Ctrl+C` the script will close but will keep running in the background (to open all background scripts, just re-build).
|
||||
|
||||
#### Building Void from Terminal
|
||||
|
||||
|
|
|
|||
53
package-lock.json
generated
53
package-lock.json
generated
|
|
@ -75,6 +75,7 @@
|
|||
"devDependencies": {
|
||||
"@playwright/test": "^1.50.0",
|
||||
"@stylistic/eslint-plugin-ts": "^2.8.0",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@types/cookie": "^0.3.3",
|
||||
"@types/debug": "^4.1.5",
|
||||
"@types/diff": "^7.0.1",
|
||||
|
|
@ -168,7 +169,7 @@
|
|||
"pump": "^1.0.1",
|
||||
"rcedit": "^1.1.0",
|
||||
"rimraf": "^2.7.1",
|
||||
"scope-tailwind": "^1.0.6",
|
||||
"scope-tailwind": "^1.0.9",
|
||||
"sinon": "^12.0.1",
|
||||
"sinon-test": "^3.1.3",
|
||||
"source-map": "0.6.1",
|
||||
|
|
@ -3598,6 +3599,36 @@
|
|||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/typography": {
|
||||
"version": "0.5.16",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.16.tgz",
|
||||
"integrity": "sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lodash.castarray": "^4.4.0",
|
||||
"lodash.isplainobject": "^4.0.6",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"postcss-selector-parser": "6.0.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": {
|
||||
"version": "6.0.10",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
|
||||
"integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cssesc": "^3.0.0",
|
||||
"util-deprecate": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/@thisismanta/pessimist": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@thisismanta/pessimist/-/pessimist-1.2.0.tgz",
|
||||
|
|
@ -14039,6 +14070,13 @@
|
|||
"integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY= sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/lodash.castarray": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz",
|
||||
"integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.clone": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.clone/-/lodash.clone-4.5.0.tgz",
|
||||
|
|
@ -14057,6 +14095,13 @@
|
|||
"integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/lodash.isplainobject": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.memoize": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
|
||||
|
|
@ -18820,9 +18865,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/scope-tailwind": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/scope-tailwind/-/scope-tailwind-1.0.6.tgz",
|
||||
"integrity": "sha512-tkISLsaesYKKXL9YrLsRWFOD/FhrRVGKeinjgTuFtEidryLzwlBB3G17ArmHWHYcfdMp00XwnRMcGFkF8wwG6w==",
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/scope-tailwind/-/scope-tailwind-1.0.9.tgz",
|
||||
"integrity": "sha512-sxtAKxJq143lYK/RCE36YGq13ficBZ9/9Z0TZa78k0AEiKNT5nH4kfhD8YAfEXR/qPR+G7tl9KL4UoHh+Cs93g==",
|
||||
"dev": true,
|
||||
"license": "AGPL-3.0-only",
|
||||
"dependencies": {
|
||||
|
|
|
|||
|
|
@ -136,6 +136,7 @@
|
|||
"devDependencies": {
|
||||
"@playwright/test": "^1.50.0",
|
||||
"@stylistic/eslint-plugin-ts": "^2.8.0",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@types/cookie": "^0.3.3",
|
||||
"@types/debug": "^4.1.5",
|
||||
"@types/diff": "^7.0.1",
|
||||
|
|
@ -229,7 +230,7 @@
|
|||
"pump": "^1.0.1",
|
||||
"rcedit": "^1.1.0",
|
||||
"rimraf": "^2.7.1",
|
||||
"scope-tailwind": "^1.0.6",
|
||||
"scope-tailwind": "^1.0.9",
|
||||
"sinon": "^12.0.1",
|
||||
"sinon-test": "^3.1.3",
|
||||
"source-map": "0.6.1",
|
||||
|
|
|
|||
|
|
@ -4,23 +4,20 @@
|
|||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { coalesce, isNonEmptyArray } from '../../../../base/common/arrays.js';
|
||||
import { Codicon } from '../../../../base/common/codicons.js';
|
||||
import { toErrorMessage } from '../../../../base/common/errorMessage.js';
|
||||
import { Event } from '../../../../base/common/event.js';
|
||||
import { MarkdownString } from '../../../../base/common/htmlContent.js';
|
||||
import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
|
||||
import { Disposable, DisposableMap, DisposableStore } from '../../../../base/common/lifecycle.js';
|
||||
import * as strings from '../../../../base/common/strings.js';
|
||||
import { localize, localize2 } from '../../../../nls.js';
|
||||
import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
|
||||
import { localize } from '../../../../nls.js';
|
||||
import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
|
||||
import { ExtensionIdentifier, IExtensionManifest } from '../../../../platform/extensions/common/extensions.js';
|
||||
import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js';
|
||||
import { ILogService } from '../../../../platform/log/common/log.js';
|
||||
import { IProductService } from '../../../../platform/product/common/productService.js';
|
||||
import { Registry } from '../../../../platform/registry/common/platform.js';
|
||||
import { ViewPaneContainer } from '../../../browser/parts/views/viewPaneContainer.js';
|
||||
import { IWorkbenchContribution } from '../../../common/contributions.js';
|
||||
import { IViewContainersRegistry, IViewDescriptor, IViewsRegistry, ViewContainer, ViewContainerLocation, Extensions as ViewExtensions } from '../../../common/views.js';
|
||||
import { IViewsRegistry, Extensions as ViewExtensions } from '../../../common/views.js';
|
||||
import { IExtensionFeatureTableRenderer, IRenderedData, ITableData, IRowData, IExtensionFeaturesRegistry, Extensions } from '../../../services/extensionManagement/common/extensionFeatures.js';
|
||||
import { isProposedApiEnabled } from '../../../services/extensions/common/extensions.js';
|
||||
import * as extensionsRegistry from '../../../services/extensions/common/extensionsRegistry.js';
|
||||
|
|
@ -30,90 +27,91 @@ import { ChatAgentLocation, IChatAgentData, IChatAgentService } from '../common/
|
|||
import { ChatContextKeys } from '../common/chatContextKeys.js';
|
||||
import { IRawChatParticipantContribution } from '../common/chatParticipantContribTypes.js';
|
||||
import { ChatViewId } from './chat.js';
|
||||
import { CHAT_EDITING_SIDEBAR_PANEL_ID, CHAT_SIDEBAR_PANEL_ID, ChatViewPane } from './chatViewPane.js';
|
||||
|
||||
// --- Chat Container & View Registration
|
||||
|
||||
const chatViewContainer: ViewContainer = Registry.as<IViewContainersRegistry>(ViewExtensions.ViewContainersRegistry).registerViewContainer({
|
||||
id: CHAT_SIDEBAR_PANEL_ID,
|
||||
title: localize2('chat.viewContainer.label', "Chat"),
|
||||
icon: Codicon.commentDiscussion,
|
||||
ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [CHAT_SIDEBAR_PANEL_ID, { mergeViewWithContainerWhenSingleView: true }]),
|
||||
storageId: CHAT_SIDEBAR_PANEL_ID,
|
||||
hideIfEmpty: true,
|
||||
order: 100,
|
||||
}, ViewContainerLocation.AuxiliaryBar, { isDefault: true, doNotRegisterOpenCommand: true });
|
||||
// Void commented this out
|
||||
// const chatViewContainer: ViewContainer = Registry.as<IViewContainersRegistry>(ViewExtensions.ViewContainersRegistry).registerViewContainer({
|
||||
// id: CHAT_SIDEBAR_PANEL_ID,
|
||||
// title: localize2('chat.viewContainer.label', "Chat"),
|
||||
// icon: Codicon.commentDiscussion,
|
||||
// ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [CHAT_SIDEBAR_PANEL_ID, { mergeViewWithContainerWhenSingleView: true }]),
|
||||
// storageId: CHAT_SIDEBAR_PANEL_ID,
|
||||
// hideIfEmpty: true,
|
||||
// order: 100,
|
||||
// }, ViewContainerLocation.AuxiliaryBar, { isDefault: true, doNotRegisterOpenCommand: true });
|
||||
|
||||
const chatViewDescriptor: IViewDescriptor[] = [{
|
||||
id: ChatViewId,
|
||||
containerIcon: chatViewContainer.icon,
|
||||
containerTitle: chatViewContainer.title.value,
|
||||
singleViewPaneContainerTitle: chatViewContainer.title.value,
|
||||
name: localize2('chat.viewContainer.label', "Chat"),
|
||||
canToggleVisibility: false,
|
||||
canMoveView: true,
|
||||
openCommandActionDescriptor: {
|
||||
id: CHAT_SIDEBAR_PANEL_ID,
|
||||
title: chatViewContainer.title,
|
||||
mnemonicTitle: localize({ key: 'miToggleChat', comment: ['&& denotes a mnemonic'] }, "&&Chat"),
|
||||
keybindings: {
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyI,
|
||||
mac: {
|
||||
primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KeyI
|
||||
}
|
||||
},
|
||||
order: 1
|
||||
},
|
||||
ctorDescriptor: new SyncDescriptor(ChatViewPane, [{ location: ChatAgentLocation.Panel }]),
|
||||
when: ContextKeyExpr.or(
|
||||
ChatContextKeys.Setup.hidden.negate(),
|
||||
ChatContextKeys.Setup.installed,
|
||||
ChatContextKeys.panelParticipantRegistered,
|
||||
ChatContextKeys.extensionInvalid
|
||||
)
|
||||
}];
|
||||
Registry.as<IViewsRegistry>(ViewExtensions.ViewsRegistry).registerViews(chatViewDescriptor, chatViewContainer);
|
||||
// const chatViewDescriptor: IViewDescriptor[] = [{
|
||||
// id: ChatViewId,
|
||||
// containerIcon: chatViewContainer.icon,
|
||||
// containerTitle: chatViewContainer.title.value,
|
||||
// singleViewPaneContainerTitle: chatViewContainer.title.value,
|
||||
// name: localize2('chat.viewContainer.label', "Chat"),
|
||||
// canToggleVisibility: false,
|
||||
// canMoveView: true,
|
||||
// openCommandActionDescriptor: {
|
||||
// id: CHAT_SIDEBAR_PANEL_ID,
|
||||
// title: chatViewContainer.title,
|
||||
// mnemonicTitle: localize({ key: 'miToggleChat', comment: ['&& denotes a mnemonic'] }, "&&Chat"),
|
||||
// keybindings: {
|
||||
// primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyI,
|
||||
// mac: {
|
||||
// primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KeyI
|
||||
// }
|
||||
// },
|
||||
// order: 1
|
||||
// },
|
||||
// ctorDescriptor: new SyncDescriptor(ChatViewPane, [{ location: ChatAgentLocation.Panel }]),
|
||||
// when: ContextKeyExpr.or(
|
||||
// ChatContextKeys.Setup.hidden.negate(),
|
||||
// ChatContextKeys.Setup.installed,
|
||||
// ChatContextKeys.panelParticipantRegistered,
|
||||
// ChatContextKeys.extensionInvalid
|
||||
// )
|
||||
// }];
|
||||
// Registry.as<IViewsRegistry>(ViewExtensions.ViewsRegistry).registerViews(chatViewDescriptor, chatViewContainer);
|
||||
|
||||
// --- Edits Container & View Registration
|
||||
|
||||
const editsViewContainer: ViewContainer = Registry.as<IViewContainersRegistry>(ViewExtensions.ViewContainersRegistry).registerViewContainer({
|
||||
id: CHAT_EDITING_SIDEBAR_PANEL_ID,
|
||||
title: localize2('chatEditing.viewContainer.label', "Copilot Edits"),
|
||||
icon: Codicon.editSession,
|
||||
ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [CHAT_EDITING_SIDEBAR_PANEL_ID, { mergeViewWithContainerWhenSingleView: true }]),
|
||||
storageId: CHAT_EDITING_SIDEBAR_PANEL_ID,
|
||||
hideIfEmpty: true,
|
||||
order: 101,
|
||||
}, ViewContainerLocation.AuxiliaryBar, { doNotRegisterOpenCommand: true });
|
||||
// Void commented this out
|
||||
// const editsViewContainer: ViewContainer = Registry.as<IViewContainersRegistry>(ViewExtensions.ViewContainersRegistry).registerViewContainer({
|
||||
// id: CHAT_EDITING_SIDEBAR_PANEL_ID,
|
||||
// title: localize2('chatEditing.viewContainer.label', "Copilot Edits"),
|
||||
// icon: Codicon.editSession,
|
||||
// ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [CHAT_EDITING_SIDEBAR_PANEL_ID, { mergeViewWithContainerWhenSingleView: true }]),
|
||||
// storageId: CHAT_EDITING_SIDEBAR_PANEL_ID,
|
||||
// hideIfEmpty: true,
|
||||
// order: 101,
|
||||
// }, ViewContainerLocation.AuxiliaryBar, { doNotRegisterOpenCommand: true });
|
||||
|
||||
const editsViewDescriptor: IViewDescriptor[] = [{
|
||||
id: 'workbench.panel.chat.view.edits',
|
||||
containerIcon: editsViewContainer.icon,
|
||||
containerTitle: editsViewContainer.title.value,
|
||||
singleViewPaneContainerTitle: editsViewContainer.title.value,
|
||||
name: editsViewContainer.title,
|
||||
canToggleVisibility: false,
|
||||
canMoveView: true,
|
||||
openCommandActionDescriptor: {
|
||||
id: CHAT_EDITING_SIDEBAR_PANEL_ID,
|
||||
title: editsViewContainer.title,
|
||||
mnemonicTitle: localize({ key: 'miToggleEdits', comment: ['&& denotes a mnemonic'] }, "Copilot Ed&&its"),
|
||||
keybindings: {
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyI,
|
||||
linux: {
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.KeyI
|
||||
}
|
||||
},
|
||||
order: 2
|
||||
},
|
||||
ctorDescriptor: new SyncDescriptor(ChatViewPane, [{ location: ChatAgentLocation.EditingSession }]),
|
||||
when: ContextKeyExpr.or(
|
||||
ChatContextKeys.Setup.hidden.negate(),
|
||||
ChatContextKeys.Setup.installed,
|
||||
ChatContextKeys.editingParticipantRegistered
|
||||
)
|
||||
}];
|
||||
Registry.as<IViewsRegistry>(ViewExtensions.ViewsRegistry).registerViews(editsViewDescriptor, editsViewContainer);
|
||||
// const editsViewDescriptor: IViewDescriptor[] = [{
|
||||
// id: 'workbench.panel.chat.view.edits',
|
||||
// containerIcon: editsViewContainer.icon,
|
||||
// containerTitle: editsViewContainer.title.value,
|
||||
// singleViewPaneContainerTitle: editsViewContainer.title.value,
|
||||
// name: editsViewContainer.title,
|
||||
// canToggleVisibility: false,
|
||||
// canMoveView: true,
|
||||
// openCommandActionDescriptor: {
|
||||
// id: CHAT_EDITING_SIDEBAR_PANEL_ID,
|
||||
// title: editsViewContainer.title,
|
||||
// mnemonicTitle: localize({ key: 'miToggleEdits', comment: ['&& denotes a mnemonic'] }, "Copilot Ed&&its"),
|
||||
// keybindings: {
|
||||
// primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyI,
|
||||
// linux: {
|
||||
// primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.KeyI
|
||||
// }
|
||||
// },
|
||||
// order: 2
|
||||
// },
|
||||
// ctorDescriptor: new SyncDescriptor(ChatViewPane, [{ location: ChatAgentLocation.EditingSession }]),
|
||||
// when: ContextKeyExpr.or(
|
||||
// ChatContextKeys.Setup.hidden.negate(),
|
||||
// ChatContextKeys.Setup.installed,
|
||||
// ChatContextKeys.editingParticipantRegistered
|
||||
// )
|
||||
// }];
|
||||
// Registry.as<IViewsRegistry>(ViewExtensions.ViewsRegistry).registerViews(editsViewDescriptor, editsViewContainer);
|
||||
|
||||
const chatParticipantExtensionPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint<IRawChatParticipantContribution[]>({
|
||||
extensionPoint: 'chatParticipants',
|
||||
|
|
|
|||
|
|
@ -18,9 +18,14 @@ import { IModelService } from '../../../../editor/common/services/model.js';
|
|||
import { extractCodeFromRegular } from './helpers/extractCodeFromResult.js';
|
||||
import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js';
|
||||
import { ILLMMessageService } from '../common/llmMessageService.js';
|
||||
import { _ln, allLinebreakSymbols } from '../common/voidFileService.js';
|
||||
import { isWindows } from '../../../../base/common/platform.js';
|
||||
// import { IContextGatheringService } from './contextGatheringService.js';
|
||||
|
||||
|
||||
|
||||
const allLinebreakSymbols = ['\r\n', '\n']
|
||||
const _ln = isWindows ? allLinebreakSymbols[0] : allLinebreakSymbols[1]
|
||||
|
||||
// The extension this was called from is here - https://github.com/voideditor/void/blob/autocomplete/extensions/void/src/extension/extension.ts
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -11,13 +11,15 @@ 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 './llmMessageService.js';
|
||||
import { chat_userMessageContent, chat_systemMessage, chat_userMessageContentWithAllFilesToo, chat_selectionsString } from '../browser/prompt/prompts.js';
|
||||
import { InternalToolInfo, IToolsService, ToolCallReturnType, ToolFns, ToolName, voidTools } from './toolsService.js';
|
||||
import { toLLMChatMessage } from './llmMessageTypes.js';
|
||||
import { ILLMMessageService } from '../common/llmMessageService.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 { LLMChatMessage, toLLMChatMessage, ToolCallType } from '../common/llmMessageTypes.js';
|
||||
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
|
||||
import { IVoidFileService } from './voidFileService.js';
|
||||
import { IVoidFileService } from '../common/voidFileService.js';
|
||||
import { generateUuid } from '../../../../base/common/uuid.js';
|
||||
import { getErrorMessage } from '../../../../base/common/errors.js';
|
||||
import { ChatMode } from '../common/voidSettingsTypes.js';
|
||||
|
||||
|
||||
const findLastIndex = <T>(arr: T[], condition: (t: T) => boolean): number => {
|
||||
|
|
@ -57,12 +59,17 @@ export type StagingSelectionItem = CodeSelection | FileSelection
|
|||
export type ToolMessage<T extends ToolName> = {
|
||||
role: 'tool';
|
||||
name: T; // internal use
|
||||
params: string; // internal use
|
||||
paramsStr: string; // internal use
|
||||
id: string; // apis require this tool use id
|
||||
content: string; // result
|
||||
result: ToolCallReturnType[T]; // text message of result
|
||||
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<T extends ToolName> = {
|
||||
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 =
|
||||
|
|
@ -81,6 +88,7 @@ export type ChatMessage =
|
|||
reasoning: string | null; // reasoning from the LLM, used for step-by-step thinking
|
||||
}
|
||||
| ToolMessage<ToolName>
|
||||
| ToolRequestApproval<ToolName>
|
||||
|
||||
type UserMessageType = ChatMessage & { role: 'user' }
|
||||
type UserMessageState = UserMessageType['state']
|
||||
|
|
@ -143,7 +151,6 @@ const newThreadObject = () => {
|
|||
export const THREAD_STORAGE_KEY = 'void.chatThreadStorage'
|
||||
|
||||
|
||||
type ChatMode = 'agent' | 'chat'
|
||||
export interface IChatThreadService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
|
|
@ -169,6 +176,9 @@ export interface IChatThreadService {
|
|||
getCurrentThreadState: () => ThreadType['state']
|
||||
setCurrentThreadState: (newState: Partial<ThreadType['state']>) => void
|
||||
|
||||
closeStagingSelectionsInCurrentThread(): void;
|
||||
closeStagingSelectionsInMessage(messageIdx: number): void;
|
||||
|
||||
|
||||
// call to edit a message
|
||||
editUserMessageAndStreamResponse({ userMessage, chatMode, messageIdx }: { userMessage: string, chatMode: ChatMode, messageIdx: number }): Promise<void>;
|
||||
|
|
@ -179,6 +189,8 @@ export interface IChatThreadService {
|
|||
cancelStreaming(threadId: string): void;
|
||||
dismissStreamError(threadId: string): void;
|
||||
|
||||
approveTool(toolId: string): void;
|
||||
rejectTool(toolId: string): void;
|
||||
}
|
||||
|
||||
export const IChatThreadService = createDecorator<IChatThreadService>('voidChatThreadService');
|
||||
|
|
@ -317,6 +329,18 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
}
|
||||
|
||||
|
||||
private resRejOfToolAwaitingApproval: { [toolId: string]: { res: () => void, rej: () => void } } = {}
|
||||
approveTool(toolId: string) {
|
||||
const resRej = this.resRejOfToolAwaitingApproval[toolId]
|
||||
resRej?.res()
|
||||
delete this.resRejOfToolAwaitingApproval[toolId]
|
||||
}
|
||||
rejectTool(toolId: string) {
|
||||
const resRej = this.resRejOfToolAwaitingApproval[toolId]
|
||||
resRej?.rej()
|
||||
delete this.resRejOfToolAwaitingApproval[toolId]
|
||||
}
|
||||
|
||||
|
||||
async addUserMessageAndStreamResponse({ userMessage, chatMode, chatSelections }: { userMessage: string, chatMode: ChatMode, chatSelections?: { prevSelns?: StagingSelectionItem[], currSelns?: StagingSelectionItem[] } }) {
|
||||
|
||||
|
|
@ -329,10 +353,8 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
|
||||
// add user's message to chat history
|
||||
const instructions = userMessage
|
||||
const userMessageContent = await chat_userMessageContent(instructions, currSelns)
|
||||
const selectionsStr = await chat_selectionsString(prevSelns, currSelns, this._voidFileService)
|
||||
const userMessageFullContent = chat_userMessageContentWithAllFilesToo(userMessageContent, selectionsStr)
|
||||
|
||||
const userMessageContent = await chat_userMessageContent(instructions, currSelns) // user message + names of files (NOT content)
|
||||
const userHistoryElt: ChatMessage = { role: 'user', content: userMessageContent, displayContent: instructions, selections: currSelns, state: defaultMessageState }
|
||||
this._addMessageToThread(threadId, userHistoryElt)
|
||||
|
||||
|
|
@ -351,31 +373,34 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
let nMessagesSent = 0
|
||||
|
||||
while (shouldSendAnotherMessage) {
|
||||
shouldSendAnotherMessage = false
|
||||
// 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
|
||||
|
||||
shouldSendAnotherMessage = false // false by default
|
||||
nMessagesSent += 1
|
||||
|
||||
let res_: () => void
|
||||
let res_: () => void // resolves when user approves this tool use (or if tool doesn't require approval)
|
||||
const awaitable = new Promise<void>((res, rej) => { res_ = res })
|
||||
|
||||
// replace last userMessage with userMessageFullContent (which contains all the files too)
|
||||
const messages_ = this.getCurrentThread().messages.map(m => (toLLMChatMessage(m)))
|
||||
const messages_ = this.getCurrentThread().messages.map(m => (toLLMChatMessage(m))).filter(m => !!m)
|
||||
const lastUserMsgIdx = findLastIndex(messages_, m => m.role === 'user')
|
||||
let messages = messages_
|
||||
if (lastUserMsgIdx !== -1) { // should never be -1
|
||||
messages = [
|
||||
...messages.slice(0, lastUserMsgIdx),
|
||||
{ role: 'user', content: userMessageFullContent },
|
||||
...messages.slice(lastUserMsgIdx + 1, Infinity)]
|
||||
}
|
||||
|
||||
if (lastUserMsgIdx === -1) throw new Error(`Void: No user message found.`) // should never be -1
|
||||
|
||||
const messages: LLMChatMessage[] = [
|
||||
{ role: 'system', content: chat_systemMessage(this._workspaceContextService.getWorkspace().folders.map(f => f.uri.fsPath), chatMode), },
|
||||
...messages_.slice(0, lastUserMsgIdx),
|
||||
{ role: 'user', content: userMessageFullContent },
|
||||
...messages_.slice(lastUserMsgIdx + 1, Infinity),
|
||||
]
|
||||
|
||||
const llmCancelToken = this._llmMessageService.sendLLMMessage({
|
||||
messagesType: 'chatMessages',
|
||||
useProviderFor: 'Ctrl+L',
|
||||
logging: { loggingName: `Agent` },
|
||||
messages: [
|
||||
{ role: 'system', content: chat_systemMessage(this._workspaceContextService.getWorkspace().folders.map(f => f.uri.fsPath)) },
|
||||
...messages,
|
||||
],
|
||||
messages,
|
||||
|
||||
tools: tools,
|
||||
|
||||
|
|
@ -390,37 +415,93 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
else {
|
||||
this._addMessageToThread(threadId, { role: 'assistant', content: fullText, reasoning: fullReasoning || null })
|
||||
this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined }) // clear streaming message
|
||||
for (const tool of toolCalls ?? []) {
|
||||
const toolName = tool.name as ToolName
|
||||
|
||||
// 1.
|
||||
let toolResult: Awaited<ReturnType<ToolFns[ToolName]>>
|
||||
let toolResultVal: ToolCallReturnType[ToolName]
|
||||
try {
|
||||
toolResult = await this._toolsService.toolFns[toolName](tool.params)
|
||||
toolResultVal = toolResult
|
||||
} catch (error) {
|
||||
this._setStreamState(threadId, { error })
|
||||
shouldSendAnotherMessage = false
|
||||
break
|
||||
}
|
||||
// deal with the tool
|
||||
const tool: ToolCallType | undefined = toolCalls?.[0]
|
||||
if (!tool) {
|
||||
res_()
|
||||
return
|
||||
}
|
||||
const toolName = tool.name
|
||||
shouldSendAnotherMessage = true
|
||||
|
||||
// 2.
|
||||
let toolResultStr: string
|
||||
try {
|
||||
toolResultStr = this._toolsService.toolResultToString[toolName](toolResult 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) {
|
||||
this._setStreamState(threadId, { error })
|
||||
shouldSendAnotherMessage = false
|
||||
break
|
||||
}
|
||||
// 1. validate tool params
|
||||
let toolParams: ToolCallParams[typeof toolName]
|
||||
try {
|
||||
console.log('A')
|
||||
|
||||
this._addMessageToThread(threadId, { role: 'tool', name: toolName, params: tool.params, id: tool.id, content: toolResultStr, result: toolResultVal, })
|
||||
shouldSendAnotherMessage = true
|
||||
const params = await this._toolsService.validateParams[toolName](tool.paramsStr)
|
||||
console.log('B')
|
||||
|
||||
toolParams = params
|
||||
} catch (error) {
|
||||
console.log('ERR1')
|
||||
const errorMessage = getErrorMessage(error)
|
||||
this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: errorMessage, result: { type: 'error', value: errorMessage }, })
|
||||
res_()
|
||||
return
|
||||
}
|
||||
|
||||
// 2. if tool requires approval, await the approval
|
||||
if (toolNamesThatRequireApproval.has(toolName)) {
|
||||
console.log('C')
|
||||
|
||||
const voidToolId = generateUuid()
|
||||
console.log('D')
|
||||
const toolApprovalPromise = new Promise<void>((res, rej) => { this.resRejOfToolAwaitingApproval[voidToolId] = { res, rej } })
|
||||
console.log('E')
|
||||
this._addMessageToThread(threadId, { role: 'tool_request', name: toolName, params: toolParams, voidToolId: voidToolId })
|
||||
try {
|
||||
console.log('F')
|
||||
|
||||
await toolApprovalPromise
|
||||
// accepted tool
|
||||
}
|
||||
catch (e) {
|
||||
console.log('ERR2')
|
||||
|
||||
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 }, })
|
||||
res_()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 3. call the tool
|
||||
let toolResult: ToolResultType[typeof toolName]
|
||||
try {
|
||||
console.log('G')
|
||||
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) {
|
||||
console.log('ERR3')
|
||||
const errorMessage = getErrorMessage(error)
|
||||
this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: errorMessage, result: { type: 'error', value: errorMessage }, })
|
||||
res_()
|
||||
return
|
||||
}
|
||||
|
||||
// 4. stringify the result to give the LLM
|
||||
let toolResultStr: string
|
||||
try {
|
||||
|
||||
console.log('H')
|
||||
toolResultStr = this._toolsService.stringOfResult[toolName](toolParams as any, toolResult as any)
|
||||
// if (Math.random() > 0) throw new Error('This is not an allowed repo.')
|
||||
|
||||
} 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 }, })
|
||||
res_()
|
||||
return
|
||||
}
|
||||
|
||||
console.log('I')
|
||||
|
||||
// 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 }, })
|
||||
res_()
|
||||
}
|
||||
res_()
|
||||
|
||||
},
|
||||
onError: (error) => {
|
||||
const messageSoFar = this.streamState[threadId]?.messageSoFar ?? ''
|
||||
|
|
@ -432,11 +513,13 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
if (llmCancelToken === null) break
|
||||
this._setStreamState(threadId, { streamingToken: llmCancelToken })
|
||||
|
||||
console.log('awaiting agentloop')
|
||||
await awaitable
|
||||
console.log('done')
|
||||
}
|
||||
}
|
||||
|
||||
agentLoop() // DO NOT AWAIT THIS, this fn should resolve when ready to clear inputs
|
||||
agentLoop() // DO NOT AWAIT THIS, add fn should resolve when we've added message (this lets us interrupt the agent loop correctly instead of waiting for it to resolve)
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -596,6 +679,35 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
|
||||
}
|
||||
|
||||
|
||||
closeStagingSelectionsInCurrentThread = () => {
|
||||
const currThread = this.getCurrentThreadState()
|
||||
|
||||
// close all stagingSelections
|
||||
const closedStagingSelections = currThread.stagingSelections.map(s => ({ ...s, state: { ...s.state, isOpened: false } }))
|
||||
|
||||
const newThread = currThread
|
||||
newThread.stagingSelections = closedStagingSelections
|
||||
|
||||
this.setCurrentThreadState(newThread)
|
||||
|
||||
}
|
||||
|
||||
closeStagingSelectionsInMessage = (messageIdx: number) => {
|
||||
const currMessage = this.getCurrentMessageState(messageIdx)
|
||||
|
||||
// close all stagingSelections
|
||||
const closedStagingSelections = currMessage.stagingSelections.map(s => ({ ...s, state: { ...s.state, isOpened: false } }))
|
||||
|
||||
const newMessage = currMessage
|
||||
newMessage.stagingSelections = closedStagingSelections
|
||||
|
||||
this.setCurrentMessageState(messageIdx, newMessage)
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
getCurrentThreadState = () => {
|
||||
const currentThread = this.getCurrentThread()
|
||||
return currentThread.state
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator, IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { ICodeEditor, IOverlayWidget, IViewZone, OverlayWidgetPositionPreference } from '../../../../editor/browser/editorBrowser.js';
|
||||
|
||||
// import { IUndoRedoService } from '../../../../platform/undoRedo/common/undoRedo.js';
|
||||
|
|
@ -31,17 +31,18 @@ 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 { filenameToVscodeLanguage } from './helpers/detectLanguage.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';
|
||||
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||
import { Emitter } from '../../../../base/common/event.js';
|
||||
import { VOID_OPEN_SETTINGS_ACTION_ID } from './voidSettingsPane.js';
|
||||
import { ICommandService } from '../../../../platform/commands/common/commands.js';
|
||||
import { ILLMMessageService } from '../common/llmMessageService.js';
|
||||
import { LLMChatMessage, OnError, errorDetails } from '../common/llmMessageTypes.js';
|
||||
import { IMetricsService } from '../common/metricsService.js';
|
||||
import { IVoidFileService } from '../common/voidFileService.js';
|
||||
import { IEditCodeService, URIStreamState, AddCtrlKOpts, StartApplyingOpts } from './editCodeServiceInterface.js';
|
||||
|
||||
const configOfBG = (color: Color) => {
|
||||
return { dark: color, light: color, hcDark: color, hcLight: color, }
|
||||
|
|
@ -121,27 +122,8 @@ const findTextInCode = (text: string, fileContents: string, startingAtLine?: num
|
|||
}
|
||||
|
||||
|
||||
export type URIStreamState = 'idle' | 'acceptRejectAll' | 'streaming'
|
||||
|
||||
|
||||
export type StartApplyingOpts = {
|
||||
from: 'QuickEdit';
|
||||
type: 'rewrite';
|
||||
diffareaid: number; // id of the CtrlK area (contains text selection)
|
||||
} | {
|
||||
from: 'ClickApply';
|
||||
type: 'searchReplace' | 'rewrite';
|
||||
applyStr: string;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export type AddCtrlKOpts = {
|
||||
startLine: number,
|
||||
endLine: number,
|
||||
editor: ICodeEditor,
|
||||
}
|
||||
|
||||
// // TODO diffArea should be removed if we just discovered it has no more diffs in it
|
||||
// for (const diffareaid of this.diffAreasOfURI[uri.fsPath] || []) {
|
||||
// const diffArea = this.diffAreaOfId[diffareaid]
|
||||
|
|
@ -247,28 +229,6 @@ type HistorySnapshot = {
|
|||
type StreamLocationMutable = { line: number, col: number, addedSplitYet: boolean, originalCodeStartLine: number }
|
||||
|
||||
|
||||
export interface IEditCodeService {
|
||||
readonly _serviceBrand: undefined;
|
||||
startApplying(opts: StartApplyingOpts): URI | null;
|
||||
|
||||
addCtrlKZone(opts: AddCtrlKOpts): number | undefined;
|
||||
removeCtrlKZone(opts: { diffareaid: number }): void;
|
||||
removeDiffAreas(opts: { uri: URI, removeCtrlKs: boolean, behavior: 'reject' | 'accept' }): void;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
export const IEditCodeService = createDecorator<IEditCodeService>('editCodeService');
|
||||
|
||||
class EditCodeService extends Disposable implements IEditCodeService {
|
||||
_serviceBrand: undefined;
|
||||
|
|
@ -813,7 +773,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
|
||||
|
||||
|
||||
private _addToHistory(uri: URI) {
|
||||
private _addToHistory(uri: URI, opts?: { onUndo?: () => void }) {
|
||||
|
||||
const getCurrentSnapshot = (): HistorySnapshot => {
|
||||
const snapshottedDiffAreaOfId: Record<string, DiffAreaSnapshot> = {}
|
||||
|
|
@ -895,7 +855,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
resource: uri,
|
||||
label: 'Void Changes',
|
||||
code: 'undoredo.editCode',
|
||||
undo: () => { restoreDiffAreas(beforeSnapshot) },
|
||||
undo: () => { restoreDiffAreas(beforeSnapshot); opts?.onUndo?.() },
|
||||
redo: () => { if (afterSnapshot) restoreDiffAreas(afterSnapshot) }
|
||||
}
|
||||
this._undoRedoService.pushElement(elt)
|
||||
|
|
@ -1226,14 +1186,20 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
|
||||
|
||||
|
||||
public startApplying(opts: StartApplyingOpts) {
|
||||
// throws if there's an error
|
||||
public startApplying(opts: StartApplyingOpts): [URI, Promise<void>] | null {
|
||||
if (opts.type === 'rewrite') {
|
||||
const addedDiffArea = this._initializeWriteoverStream(opts)
|
||||
return addedDiffArea?._URI ?? null
|
||||
const added = this._initializeWriteoverStream(opts)
|
||||
if (!added) return null
|
||||
const [diffZone, promise] = added
|
||||
return [diffZone._URI, promise]
|
||||
}
|
||||
else if (opts.type === 'searchReplace') {
|
||||
const addedDiffArea = this._initializeSearchAndReplaceStream(opts)
|
||||
return addedDiffArea?._URI ?? null
|
||||
const added = this._initializeSearchAndReplaceStream(opts)
|
||||
if (!added) return null
|
||||
if (!added) return null
|
||||
const [diffZone, promise] = added
|
||||
return [diffZone._URI, promise]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
|
@ -1260,9 +1226,9 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
|
||||
|
||||
|
||||
private _initializeWriteoverStream(opts: StartApplyingOpts): DiffZone | undefined {
|
||||
private _initializeWriteoverStream(opts: StartApplyingOpts): [DiffZone, Promise<void>] | undefined {
|
||||
|
||||
const { from } = opts
|
||||
const { from, } = opts
|
||||
|
||||
let startLine: number
|
||||
let endLine: number
|
||||
|
|
@ -1306,8 +1272,16 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
let streamRequestIdRef: { current: string | null } = { current: null }
|
||||
|
||||
|
||||
// promise that resolves when the apply is done
|
||||
let resApplyPromise: () => void
|
||||
let rejApplyPromise: (e: any) => void
|
||||
const applyPromise = new Promise<void>((res_, rej_) => { resApplyPromise = res_; rejApplyPromise = rej_ })
|
||||
|
||||
|
||||
// add to history
|
||||
const { onFinishEdit } = this._addToHistory(uri)
|
||||
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
|
||||
|
|
@ -1403,56 +1377,75 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
let fullTextSoFar = '' // so far (INCLUDING ignored suffix)
|
||||
let prevIgnoredSuffix = ''
|
||||
|
||||
streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({
|
||||
messagesType: 'chatMessages',
|
||||
useProviderFor: opts.from === 'ClickApply' ? 'Apply' : 'Ctrl+K',
|
||||
logging: { loggingName: `startApplying - ${from}` },
|
||||
messages,
|
||||
onText: ({ fullText: fullText_ }) => {
|
||||
const newText_ = fullText_.substring(fullTextSoFar.length, Infinity)
|
||||
const writeover = async () => {
|
||||
|
||||
const newText = prevIgnoredSuffix + newText_ // add the previously ignored suffix because it's no longer the suffix!
|
||||
fullTextSoFar += newText // full text, including ```, etc
|
||||
let resMessageDonePromise: () => void = () => { }
|
||||
const messageDonePromise = new Promise<void>((res_) => { resMessageDonePromise = res_ })
|
||||
|
||||
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
|
||||
streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({
|
||||
messagesType: 'chatMessages',
|
||||
useProviderFor: opts.from === 'ClickApply' ? 'Apply' : 'Ctrl+K',
|
||||
logging: { loggingName: `Edit (Writeover) - ${from}` },
|
||||
messages,
|
||||
onText: (params) => {
|
||||
const { fullText: fullText_ } = params
|
||||
const newText_ = fullText_.substring(fullTextSoFar.length, Infinity)
|
||||
|
||||
this._refreshStylesAndDiffsInURI(uri)
|
||||
const newText = prevIgnoredSuffix + newText_ // add the previously ignored suffix because it's no longer the suffix!
|
||||
fullTextSoFar += newText // full text, including ```, etc
|
||||
|
||||
prevIgnoredSuffix = croppedSuffix
|
||||
},
|
||||
onFinalMessage: ({ fullText }) => {
|
||||
// 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,
|
||||
{ startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed
|
||||
{ shouldRealignDiffAreas: true }
|
||||
)
|
||||
onDone()
|
||||
},
|
||||
onError: (e) => {
|
||||
this._notifyError(e)
|
||||
onDone()
|
||||
this._undoHistory(uri)
|
||||
},
|
||||
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
|
||||
|
||||
})
|
||||
this._refreshStylesAndDiffsInURI(uri)
|
||||
|
||||
return diffZone
|
||||
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._writeText(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()
|
||||
},
|
||||
})
|
||||
|
||||
await messageDonePromise
|
||||
}
|
||||
|
||||
writeover().then(() => resApplyPromise()).catch((e) => rejApplyPromise(e))
|
||||
|
||||
return [diffZone, applyPromise]
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
private _initializeSearchAndReplaceStream(opts: StartApplyingOpts & { from: 'ClickApply' }) {
|
||||
const { applyStr } = opts
|
||||
private _initializeSearchAndReplaceStream(opts: StartApplyingOpts & { from: 'ClickApply' }): [DiffZone, Promise<void>] | undefined {
|
||||
const { from, applyStr, uri: givenURI, } = opts
|
||||
let uri: URI
|
||||
|
||||
if (givenURI === 'current') {
|
||||
const uri_ = this._getActiveEditorURI()
|
||||
if (!uri_) return
|
||||
uri = uri_
|
||||
}
|
||||
else {
|
||||
uri = givenURI
|
||||
}
|
||||
|
||||
const uri_ = this._getActiveEditorURI()
|
||||
if (!uri_) return
|
||||
const uri = uri_
|
||||
|
||||
// generate search/replace block text
|
||||
const originalFileCode = this._voidFileService.readModel(uri)
|
||||
|
|
@ -1476,16 +1469,24 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
// can use this as a proxy to set the diffArea's stream state requestId
|
||||
let streamRequestIdRef: { current: string | null } = { current: null }
|
||||
|
||||
let { onFinishEdit } = this._addToHistory(uri)
|
||||
|
||||
// TODO replace these with whatever block we're on initially if already started
|
||||
// promise that resolves when the apply is done
|
||||
let resApplyPromise: () => void
|
||||
let rejApplyPromise: (e: any) => void
|
||||
const applyPromise = new Promise<void>((res_, rej_) => { resApplyPromise = res_; rejApplyPromise = rej_ })
|
||||
|
||||
// add to history
|
||||
const { onFinishEdit } = this._addToHistory(uri, {
|
||||
onUndo: () => { if (diffZone._streamState.isStreaming) rejApplyPromise(new Error('Edit was interrupted by pressing undo.')) }
|
||||
})
|
||||
|
||||
// TODO replace these with whatever block we're on initially if already started (caching apply)
|
||||
|
||||
type SearchReplaceDiffAreaMetadata = {
|
||||
originalBounds: [number, number], // 1-indexed
|
||||
originalCode: string,
|
||||
}
|
||||
|
||||
const addedTrackingZoneOfBlockNum: TrackingZone<SearchReplaceDiffAreaMetadata>[] = []
|
||||
|
||||
const adding: Omit<DiffZone, 'diffareaid'> = {
|
||||
type: 'DiffZone',
|
||||
|
|
@ -1506,12 +1507,6 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
this._onDidAddOrDeleteDiffZones.fire({ uri })
|
||||
|
||||
|
||||
const revertAndContinueHistory = () => {
|
||||
this._undoHistory(uri)
|
||||
const { onFinishEdit: onFinishEdit_ } = this._addToHistory(uri)
|
||||
onFinishEdit = onFinishEdit_
|
||||
}
|
||||
|
||||
|
||||
const convertOriginalRangeToFinalRange = (originalRange: readonly [number, number]): [number, number] => {
|
||||
// adjust based on the changes by computing line offset
|
||||
|
|
@ -1531,12 +1526,12 @@ class EditCodeService extends Disposable implements IEditCodeService {
|
|||
}
|
||||
|
||||
|
||||
const errMsgOfInvalidStr = (str: string & ReturnType<typeof findTextInCode>) => {
|
||||
return str === 'Not found' ?
|
||||
'I interrupted you because the latest ORIGINAL code 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.'
|
||||
: str === 'Not unique' ?
|
||||
'I interrupted you because the latest ORIGINAL code 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.'
|
||||
: ''
|
||||
const errMsgOfInvalidStr = (str: string & ReturnType<typeof findTextInCode>, 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)}`
|
||||
: 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)}`
|
||||
: ``
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -1555,169 +1550,209 @@ 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<SearchReplaceDiffAreaMetadata>[] = []
|
||||
|
||||
// stream style related
|
||||
let latestStreamLocationMutable: StreamLocationMutable | null = null
|
||||
let shouldUpdateOrigStreamStyle = true
|
||||
|
||||
let oldBlocks: ExtractedSearchReplaceBlock[] = []
|
||||
|
||||
// this generates >>>>>>> ORIGINAL <<<<<<< REPLACE blocks and and simultaneously applies it
|
||||
let shouldSendAnotherMessage = true
|
||||
let nMessagesSent = 0
|
||||
let currStreamingBlockNum = 0
|
||||
while (shouldSendAnotherMessage) {
|
||||
shouldSendAnotherMessage = false
|
||||
nMessagesSent += 1
|
||||
const retryLoop = async () => {
|
||||
// this generates >>>>>>> ORIGINAL <<<<<<< REPLACE blocks and and simultaneously applies it
|
||||
let shouldSendAnotherMessage = true
|
||||
let nMessagesSent = 0
|
||||
let currStreamingBlockNum = 0
|
||||
while (shouldSendAnotherMessage) {
|
||||
shouldSendAnotherMessage = false
|
||||
nMessagesSent += 1
|
||||
|
||||
streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({
|
||||
messagesType: 'chatMessages',
|
||||
useProviderFor: 'Apply',
|
||||
logging: { loggingName: `generateSearchAndReplace` },
|
||||
messages,
|
||||
onText: ({ fullText }) => {
|
||||
// blocks are [done done done ... {writingFinal|writingOriginal}]
|
||||
// ^
|
||||
// currStreamingBlockNum
|
||||
let resMessageDonePromise: () => void = () => { }
|
||||
const messageDonePromise = new Promise<void>((res_) => { resMessageDonePromise = res_ })
|
||||
|
||||
const blocks = extractSearchReplaceBlocks(fullText)
|
||||
streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({
|
||||
messagesType: 'chatMessages',
|
||||
useProviderFor: 'Apply',
|
||||
logging: { loggingName: `Edit (Search/Replace) - ${from}` },
|
||||
messages,
|
||||
onText: (params) => {
|
||||
const { fullText } = params
|
||||
// blocks are [done done done ... {writingFinal|writingOriginal}]
|
||||
// ^
|
||||
// currStreamingBlockNum
|
||||
|
||||
for (let blockNum = currStreamingBlockNum; blockNum < blocks.length; blockNum += 1) {
|
||||
const block = blocks[blockNum]
|
||||
const blocks = extractSearchReplaceBlocks(fullText)
|
||||
|
||||
if (block.state === 'writingOriginal') {
|
||||
// update stream state to the first line of original if some portion of original has been written
|
||||
if (shouldUpdateOrigStreamStyle && block.orig.trim().length >= 20) {
|
||||
const startingAtLine = diffZone._streamState.line ?? 1 // dont go backwards if already have a stream line
|
||||
const originalRange = findTextInCode(block.orig, originalFileCode, startingAtLine)
|
||||
if (typeof originalRange !== 'string') {
|
||||
const [startLine, _] = convertOriginalRangeToFinalRange(originalRange)
|
||||
diffZone._streamState.line = startLine
|
||||
shouldUpdateOrigStreamStyle = false
|
||||
for (let blockNum = currStreamingBlockNum; blockNum < blocks.length; blockNum += 1) {
|
||||
const block = blocks[blockNum]
|
||||
|
||||
if (block.state === 'writingOriginal') {
|
||||
// update stream state to the first line of original if some portion of original has been written
|
||||
if (shouldUpdateOrigStreamStyle && block.orig.trim().length >= 20) {
|
||||
const startingAtLine = diffZone._streamState.line ?? 1 // dont go backwards if already have a stream line
|
||||
const originalRange = findTextInCode(block.orig, originalFileCode, startingAtLine)
|
||||
if (typeof originalRange !== 'string') {
|
||||
const [startLine, _] = convertOriginalRangeToFinalRange(originalRange)
|
||||
diffZone._streamState.line = startLine
|
||||
shouldUpdateOrigStreamStyle = false
|
||||
}
|
||||
}
|
||||
// must be done writing original to move on to writing streamed content
|
||||
continue
|
||||
}
|
||||
// must be done writing original to move on to writing streamed content
|
||||
continue
|
||||
}
|
||||
shouldUpdateOrigStreamStyle = true
|
||||
shouldUpdateOrigStreamStyle = true
|
||||
|
||||
|
||||
// if this is the first time we're seeing this block, add it as a diffarea so we can start streaming
|
||||
if (!(blockNum in addedTrackingZoneOfBlockNum)) {
|
||||
const originalBounds = findTextInCode(block.orig, originalFileCode)
|
||||
// if this is the first time we're seeing this block, add it as a diffarea so we can start streaming
|
||||
if (!(blockNum in addedTrackingZoneOfBlockNum)) {
|
||||
console.log('finding text in code...', { orig: block.orig })
|
||||
const originalBounds = findTextInCode(block.orig, originalFileCode)
|
||||
|
||||
// if error
|
||||
if (typeof originalBounds === 'string') {
|
||||
messages.push(
|
||||
{ role: 'assistant', content: fullText }, // latest output
|
||||
{ role: 'user', content: errMsgOfInvalidStr(originalBounds) } // user explanation of what's wrong
|
||||
// if error
|
||||
if (typeof originalBounds === 'string') {
|
||||
const content = errMsgOfInvalidStr(originalBounds, block.orig)
|
||||
messages.push(
|
||||
{ role: 'assistant', content: fullText }, // latest output
|
||||
{ role: 'user', content: content } // user explanation of what's wrong
|
||||
)
|
||||
|
||||
if (streamRequestIdRef.current) this._llmMessageService.abort(streamRequestIdRef.current)
|
||||
|
||||
// REVERT
|
||||
const numLines = this._getNumLines(uri)
|
||||
if (numLines !== null) this._writeText(uri, originalFileCode,
|
||||
{ startLineNumber: 1, startColumn: 1, endLineNumber: numLines, endColumn: Number.MAX_SAFE_INTEGER },
|
||||
{ shouldRealignDiffAreas: false }
|
||||
)
|
||||
// reset state
|
||||
diffZone.startLine = 1
|
||||
diffZone.endLine = numLines ?? 1
|
||||
if (diffZone._streamState.isStreaming) {
|
||||
diffZone._streamState.line = 1
|
||||
}
|
||||
|
||||
currStreamingBlockNum = 0
|
||||
latestStreamLocationMutable = null
|
||||
shouldUpdateOrigStreamStyle = true
|
||||
oldBlocks = []
|
||||
addedTrackingZoneOfBlockNum.splice(0, Infinity) // clear the array
|
||||
|
||||
shouldSendAnotherMessage = true
|
||||
this._refreshStylesAndDiffsInURI(uri)
|
||||
resMessageDonePromise()
|
||||
return
|
||||
}
|
||||
|
||||
const [startLine, endLine] = convertOriginalRangeToFinalRange(originalBounds)
|
||||
|
||||
// otherwise if no error, add the position as a diffarea
|
||||
const adding: Omit<TrackingZone<SearchReplaceDiffAreaMetadata>, 'diffareaid'> = {
|
||||
type: 'TrackingZone',
|
||||
startLine: startLine,
|
||||
endLine: endLine,
|
||||
_URI: uri,
|
||||
metadata: {
|
||||
originalBounds: [...originalBounds],
|
||||
originalCode: block.orig,
|
||||
},
|
||||
}
|
||||
const trackingZone = this._addDiffArea(adding)
|
||||
addedTrackingZoneOfBlockNum.push(trackingZone)
|
||||
latestStreamLocationMutable = { line: startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 }
|
||||
} // <-- done adding diffarea
|
||||
|
||||
|
||||
// should always be in streaming state here
|
||||
if (!diffZone._streamState.isStreaming) {
|
||||
console.error('DiffZone was not in streaming state in _initializeSearchAndReplaceStream')
|
||||
continue
|
||||
}
|
||||
if (!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,
|
||||
{ startLineNumber: finalStartLine, startColumn: 1, endLineNumber: finalEndLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed
|
||||
{ shouldRealignDiffAreas: true }
|
||||
)
|
||||
if (streamRequestIdRef.current) this._llmMessageService.abort(streamRequestIdRef.current)
|
||||
shouldSendAnotherMessage = true
|
||||
revertAndContinueHistory()
|
||||
diffZone._streamState.line = finalEndLine + 1
|
||||
currStreamingBlockNum = blockNum + 1
|
||||
continue
|
||||
}
|
||||
|
||||
const [startLine, endLine] = convertOriginalRangeToFinalRange(originalBounds)
|
||||
// write the added text to the file
|
||||
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
|
||||
|
||||
// otherwise if no error, add the position as a diffarea
|
||||
const adding: Omit<TrackingZone<SearchReplaceDiffAreaMetadata>, 'diffareaid'> = {
|
||||
type: 'TrackingZone',
|
||||
startLine: startLine,
|
||||
endLine: endLine,
|
||||
_URI: uri,
|
||||
metadata: {
|
||||
originalBounds: [...originalBounds],
|
||||
originalCode: block.orig,
|
||||
},
|
||||
}
|
||||
const trackingZone = this._addDiffArea(adding)
|
||||
addedTrackingZoneOfBlockNum.push(trackingZone)
|
||||
latestStreamLocationMutable = { line: startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 }
|
||||
} // <-- done adding diffarea
|
||||
// const { endLine: currentEndLine } = addedTrackingZoneOfBlockNum[blockNum] // would be bad to do this because a lot of the bottom lines might be the same. more accurate to go with latestStreamLocationMutable
|
||||
// diffZone._streamState.line = currentEndLine
|
||||
diffZone._streamState.line = latestStreamLocationMutable.line
|
||||
|
||||
|
||||
// should always be in streaming state here
|
||||
if (!diffZone._streamState.isStreaming) {
|
||||
console.error('DiffZone was not in streaming state in _initializeSearchAndReplaceStream')
|
||||
continue
|
||||
|
||||
} // end for
|
||||
|
||||
this._refreshStylesAndDiffsInURI(uri)
|
||||
},
|
||||
onFinalMessage: async (params) => {
|
||||
const { fullText } = params
|
||||
console.log('final message!!', fullText)
|
||||
|
||||
// 1. wait 500ms and fix lint errors - call lint error workflow
|
||||
// (update react state to say "Fixing errors")
|
||||
const blocks = extractSearchReplaceBlocks(fullText)
|
||||
|
||||
if (blocks.length === 0) {
|
||||
this._notificationService.info(`Void: We ran Apply, but the LLM didn't output any changes.`)
|
||||
}
|
||||
if (!latestStreamLocationMutable) continue
|
||||
// writeover the whole file
|
||||
let newCode = originalFileCode
|
||||
for (let blockNum = addedTrackingZoneOfBlockNum.length - 1; blockNum >= 0; blockNum -= 1) {
|
||||
const { originalBounds } = addedTrackingZoneOfBlockNum[blockNum].metadata
|
||||
const finalCode = blocks[blockNum].final
|
||||
|
||||
// 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,
|
||||
{ startLineNumber: finalStartLine, startColumn: 1, endLineNumber: finalEndLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed
|
||||
if (finalCode === null) continue
|
||||
|
||||
const [originalStart, originalEnd] = originalBounds
|
||||
const lines = newCode.split('\n')
|
||||
newCode = [
|
||||
...lines.slice(0, (originalStart - 1)),
|
||||
...finalCode.split('\n'),
|
||||
...lines.slice((originalEnd - 1) + 1, Infinity)
|
||||
].join('\n')
|
||||
}
|
||||
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 }
|
||||
)
|
||||
diffZone._streamState.line = finalEndLine + 1
|
||||
currStreamingBlockNum = blockNum + 1
|
||||
continue
|
||||
}
|
||||
|
||||
// write the added text to the file
|
||||
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
|
||||
onDone()
|
||||
resMessageDonePromise()
|
||||
},
|
||||
onError: (e) => {
|
||||
this._notifyError(e)
|
||||
onDone()
|
||||
this._undoHistory(uri)
|
||||
resMessageDonePromise()
|
||||
},
|
||||
|
||||
// const { endLine: currentEndLine } = addedTrackingZoneOfBlockNum[blockNum] // would be bad to do this because a lot of the bottom lines might be the same. more accurate to go with latestStreamLocationMutable
|
||||
// diffZone._streamState.line = currentEndLine
|
||||
diffZone._streamState.line = latestStreamLocationMutable.line
|
||||
})
|
||||
|
||||
await messageDonePromise
|
||||
|
||||
} // end while
|
||||
|
||||
} // end for
|
||||
} // end retryLoop
|
||||
|
||||
this._refreshStylesAndDiffsInURI(uri)
|
||||
},
|
||||
onFinalMessage: async ({ fullText }) => {
|
||||
console.log('final message!!', fullText)
|
||||
retryLoop().then(() => resApplyPromise()).catch((e) => rejApplyPromise(e))
|
||||
|
||||
// 1. wait 500ms and fix lint errors - call lint error workflow
|
||||
// (update react state to say "Fixing errors")
|
||||
const blocks = extractSearchReplaceBlocks(fullText)
|
||||
|
||||
if (blocks.length === 0) {
|
||||
this._notificationService.info(`Void: We ran Apply, but the LLM didn't output any changes.`)
|
||||
}
|
||||
|
||||
// writeover the whole file
|
||||
let newCode = originalFileCode
|
||||
for (let blockNum = addedTrackingZoneOfBlockNum.length - 1; blockNum >= 0; blockNum -= 1) {
|
||||
const { originalBounds } = addedTrackingZoneOfBlockNum[blockNum].metadata
|
||||
const finalCode = blocks[blockNum].final
|
||||
|
||||
if (finalCode === null) continue
|
||||
|
||||
const [originalStart, originalEnd] = originalBounds
|
||||
const lines = newCode.split('\n')
|
||||
newCode = [
|
||||
...lines.slice(0, (originalStart - 1)),
|
||||
...finalCode.split('\n'),
|
||||
...lines.slice((originalEnd - 1) + 1, Infinity)
|
||||
].join('\n')
|
||||
}
|
||||
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 }
|
||||
)
|
||||
}
|
||||
|
||||
onDone()
|
||||
},
|
||||
onError: (e) => {
|
||||
this._notifyError(e)
|
||||
onDone()
|
||||
this._undoHistory(uri)
|
||||
},
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
return diffZone
|
||||
return [diffZone, applyPromise]
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,57 @@
|
|||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
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';
|
||||
|
||||
|
||||
|
||||
|
||||
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;
|
||||
})
|
||||
|
||||
|
||||
|
||||
export type AddCtrlKOpts = {
|
||||
startLine: number,
|
||||
endLine: number,
|
||||
editor: ICodeEditor,
|
||||
}
|
||||
|
||||
export type URIStreamState = 'idle' | 'acceptRejectAll' | 'streaming'
|
||||
|
||||
|
||||
export const IEditCodeService = createDecorator<IEditCodeService>('editCodeService');
|
||||
|
||||
export interface IEditCodeService {
|
||||
readonly _serviceBrand: undefined;
|
||||
startApplying(opts: StartApplyingOpts): [URI, Promise<void>] | null;
|
||||
|
||||
addCtrlKZone(opts: AddCtrlKOpts): number | undefined;
|
||||
removeCtrlKZone(opts: { diffareaid: number }): void;
|
||||
removeDiffAreas(opts: { uri: URI, removeCtrlKs: boolean, behavior: 'reject' | 'accept' }): void;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
|
@ -171,6 +171,9 @@ export type ExtractedSearchReplaceBlock = {
|
|||
}
|
||||
|
||||
|
||||
// JS substring swaps indices, so "ab".substr(1,0) will NOT be '', it will be 'a'!
|
||||
const voidSubstr = (str: string, start: number, end: number) => end < start ? '' : str.substring(start, end)
|
||||
|
||||
const endsWithAnyPrefixOf = (str: string, anyPrefix: string) => {
|
||||
// for each prefix
|
||||
for (let i = anyPrefix.length; i >= 1; i--) { // i >= 1 because must not be empty string
|
||||
|
|
@ -187,7 +190,6 @@ export const extractSearchReplaceBlocks = (str: string) => {
|
|||
const DIVIDER_ = '\n' + DIVIDER + `\n`
|
||||
// logic for FINAL_ is slightly more complicated - should be '\n' + FINAL, but that ignores if the final output is empty
|
||||
|
||||
|
||||
const blocks: ExtractedSearchReplaceBlock[] = []
|
||||
|
||||
let i = 0 // search i and beyond (this is done by plain index, not by line number. much simpler this way)
|
||||
|
|
@ -196,41 +198,42 @@ export const extractSearchReplaceBlocks = (str: string) => {
|
|||
if (origStart === -1) { return blocks }
|
||||
origStart += ORIGINAL_.length
|
||||
i = origStart
|
||||
// wrote <<<< ORIGINAL
|
||||
// wrote <<<< ORIGINAL\n
|
||||
|
||||
let dividerStart = str.indexOf(DIVIDER_, i)
|
||||
if (dividerStart === -1) { // if didnt find DIVIDER_, either writing originalStr or DIVIDER_ right now
|
||||
const isWritingDIVIDER = endsWithAnyPrefixOf(str, DIVIDER_)
|
||||
const writingDIVIDERlen = endsWithAnyPrefixOf(str, DIVIDER_)?.length ?? 0
|
||||
blocks.push({
|
||||
orig: str.substring(origStart, str.length - (isWritingDIVIDER?.length ?? 0)),
|
||||
orig: voidSubstr(str, origStart, str.length - writingDIVIDERlen),
|
||||
final: '',
|
||||
state: 'writingOriginal'
|
||||
})
|
||||
return blocks
|
||||
}
|
||||
const origStrDone = str.substring(origStart, dividerStart)
|
||||
const origStrDone = voidSubstr(str, origStart, dividerStart)
|
||||
dividerStart += DIVIDER_.length
|
||||
i = dividerStart
|
||||
// wrote =====
|
||||
// wrote \n=====\n
|
||||
|
||||
const fullFINALStart = str.indexOf(FINAL, i)
|
||||
const fullFINALStart_ = str.indexOf('\n' + FINAL, i) // go with B if possible, else fallback to A, it's more permissive
|
||||
const matchedFullFINAL_ = fullFINALStart_ !== -1 && fullFINALStart === fullFINALStart_ + 1 // this logic is really important, otherwise we might look for FINAL_ at a much later part of the string
|
||||
|
||||
|
||||
const finalStartA = str.indexOf(FINAL, i)
|
||||
const finalStartB = str.indexOf('\n' + FINAL, i) // go with B if possible, else fallback to A, it's more permissive
|
||||
const FINAL_ = finalStartB !== -1 ? '\n' + FINAL : FINAL
|
||||
let finalStart = finalStartB !== -1 ? finalStartB : finalStartA
|
||||
|
||||
if (finalStart === -1) { // if didnt find FINAL_, either writing finalStr or FINAL_ right now
|
||||
const isWritingFINAL = endsWithAnyPrefixOf(str, FINAL_)
|
||||
let finalStart = matchedFullFINAL_ ? fullFINALStart_ : fullFINALStart
|
||||
if (finalStart === -1) { // if didnt find FINAL_, either writing finalStr or FINAL or FINAL_ right now
|
||||
const writingFINALlen = endsWithAnyPrefixOf(str, FINAL)?.length ?? 0
|
||||
const writingFINALlen_ = endsWithAnyPrefixOf(str, '\n' + FINAL)?.length ?? 0 // this gets priority
|
||||
const usingWritingFINALlen = Math.max(writingFINALlen, writingFINALlen_)
|
||||
blocks.push({
|
||||
orig: origStrDone,
|
||||
final: str.substring(dividerStart, str.length - (isWritingFINAL?.length ?? 0)),
|
||||
final: voidSubstr(str, dividerStart, str.length - usingWritingFINALlen),
|
||||
state: 'writingFinal'
|
||||
})
|
||||
return blocks
|
||||
}
|
||||
const finalStrDone = str.substring(dividerStart, finalStart)
|
||||
finalStart += FINAL_.length
|
||||
const usingFINAL = matchedFullFINAL_ ? '\n' + FINAL : FINAL
|
||||
const finalStrDone = voidSubstr(str, dividerStart, finalStart)
|
||||
finalStart += usingFINAL.length
|
||||
i = finalStart
|
||||
// wrote >>>>> FINAL
|
||||
|
||||
|
|
@ -247,9 +250,6 @@ export const extractSearchReplaceBlocks = (str: string) => {
|
|||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// could simplify this - this assumes we can never add a tag without committing it to the user's screen, but that's not true
|
||||
export const extractReasoningOnTextWrapper = (onText: OnText, thinkTags: [string, string]): OnText => {
|
||||
let latestAddIdx = 0 // exclusive index in fullText_
|
||||
|
|
@ -357,3 +357,220 @@ export const extractReasoningOnFinalMessage = (fullText_: string, thinkTags: [st
|
|||
const fullText = fullText_.substring(0, tag1Idx) + fullText_.substring(tag2Idx + thinkTags[1].length, Infinity)
|
||||
return { fullText, fullReasoning }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// const tests: [string, { shape: Partial<ExtractedSearchReplaceBlock>[] }][] = [[
|
||||
// `\
|
||||
// \`\`\`
|
||||
// <<<<<<< ORIGINA`, { shape: [] }
|
||||
// ], [
|
||||
// `\
|
||||
// \`\`\`
|
||||
// <<<<<<< ORIGINAL`, { shape: [], }
|
||||
// ], [
|
||||
// `\
|
||||
// \`\`\`
|
||||
// <<<<<<< ORIGINAL
|
||||
// A`, { shape: [{ state: 'writingOriginal', orig: 'A' }], }
|
||||
// ], [
|
||||
// `\
|
||||
// \`\`\`
|
||||
// <<<<<<< ORIGINAL
|
||||
// A
|
||||
// B`, { shape: [{ state: 'writingOriginal', orig: 'A\nB' }], }
|
||||
// ], [
|
||||
// `\
|
||||
// \`\`\`
|
||||
// <<<<<<< ORIGINAL
|
||||
// A
|
||||
// B
|
||||
// `, { shape: [{ state: 'writingOriginal', orig: 'A\nB' }], }
|
||||
// ], [
|
||||
// `\
|
||||
// \`\`\`
|
||||
// <<<<<<< ORIGINAL
|
||||
// A
|
||||
// B
|
||||
// ===`, { shape: [{ state: 'writingOriginal', orig: 'A\nB' }], }
|
||||
// ], [
|
||||
// `\
|
||||
// \`\`\`
|
||||
// <<<<<<< ORIGINAL
|
||||
// A
|
||||
// B
|
||||
// ======`, { shape: [{ state: 'writingOriginal', orig: 'A\nB' }], }
|
||||
// ], [
|
||||
// `\
|
||||
// \`\`\`
|
||||
// <<<<<<< ORIGINAL
|
||||
// A
|
||||
// B
|
||||
// =======`, { shape: [{ state: 'writingOriginal', orig: 'A\nB' }], }
|
||||
// ], [
|
||||
// `\
|
||||
// \`\`\`
|
||||
// <<<<<<< ORIGINAL
|
||||
// A
|
||||
// B
|
||||
// =======
|
||||
// `, { shape: [{ state: 'writingFinal', orig: 'A\nB', final: '' }], }
|
||||
// ], [
|
||||
// `\
|
||||
// \`\`\`
|
||||
// <<<<<<< ORIGINAL
|
||||
// A
|
||||
// B
|
||||
// =======
|
||||
// >>>>>>> UPDAT`, { shape: [{ state: 'writingFinal', orig: 'A\nB', final: '' }], }
|
||||
// ], [
|
||||
// `\
|
||||
// \`\`\`
|
||||
// <<<<<<< ORIGINAL
|
||||
// A
|
||||
// B
|
||||
// =======
|
||||
// >>>>>>> UPDATED`, { shape: [{ state: 'done', orig: 'A\nB', final: '' }], }
|
||||
// ], [
|
||||
// `\
|
||||
// \`\`\`
|
||||
// <<<<<<< ORIGINAL
|
||||
// A
|
||||
// B
|
||||
// =======
|
||||
// >>>>>>> UPDATED
|
||||
// \`\`\``, { shape: [{ state: 'done', orig: 'A\nB', final: '' }], }
|
||||
// ],
|
||||
|
||||
|
||||
// // alternatively
|
||||
// [
|
||||
// `\
|
||||
// \`\`\`
|
||||
// <<<<<<< ORIGINAL
|
||||
// A
|
||||
// B
|
||||
// =======
|
||||
// X`, { shape: [{ state: 'writingFinal', orig: 'A\nB', final: 'X' }], }
|
||||
// ],
|
||||
// [
|
||||
// `\
|
||||
// \`\`\`
|
||||
// <<<<<<< ORIGINAL
|
||||
// A
|
||||
// B
|
||||
// =======
|
||||
// X
|
||||
// Y`, { shape: [{ state: 'writingFinal', orig: 'A\nB', final: 'X\nY' }], }
|
||||
// ],
|
||||
// [
|
||||
// `\
|
||||
// \`\`\`
|
||||
// <<<<<<< ORIGINAL
|
||||
// A
|
||||
// B
|
||||
// =======
|
||||
// X
|
||||
// Y
|
||||
// `, { shape: [{ state: 'writingFinal', orig: 'A\nB', final: 'X\nY' }], }
|
||||
// ],
|
||||
// [
|
||||
// `\
|
||||
// \`\`\`
|
||||
// <<<<<<< ORIGINAL
|
||||
// A
|
||||
// B
|
||||
// =======
|
||||
// X
|
||||
// Y
|
||||
// >>>>>>> UPDAT`, { shape: [{ state: 'writingFinal', orig: 'A\nB', final: 'X\nY' }], }
|
||||
// ], [
|
||||
// `\
|
||||
// \`\`\`
|
||||
// <<<<<<< ORIGINAL
|
||||
// A
|
||||
// B
|
||||
// =======
|
||||
// X
|
||||
// Y
|
||||
// >>>>>>> UPDATED`, { shape: [{ state: 'done', orig: 'A\nB', final: 'X\nY' }], }
|
||||
// ], [
|
||||
// `\
|
||||
// \`\`\`
|
||||
// <<<<<<< ORIGINAL
|
||||
// A
|
||||
// B
|
||||
// =======
|
||||
// X
|
||||
// Y
|
||||
// >>>>>>> UPDATED
|
||||
// \`\`\``, { shape: [{ state: 'done', orig: 'A\nB', final: 'X\nY' }], }
|
||||
// ]]
|
||||
|
||||
|
||||
|
||||
|
||||
// function runTests() {
|
||||
|
||||
|
||||
// let passedTests = 0;
|
||||
// let failedTests = 0;
|
||||
|
||||
// for (let i = 0; i < tests.length; i++) {
|
||||
// const [input, expected] = tests[i];
|
||||
// const result = extractSearchReplaceBlocks(input);
|
||||
|
||||
// // Compare result with expected shape
|
||||
// let passed = true;
|
||||
// if (result.length !== expected.shape.length) {
|
||||
// passed = false;
|
||||
// } else {
|
||||
// for (let j = 0; j < result.length; j++) { // block
|
||||
// const expectedItem = expected.shape[j];
|
||||
// const resultItem = result[j];
|
||||
|
||||
// if ((expectedItem.state !== undefined) && (expectedItem.state !== resultItem.state) ||
|
||||
// (expectedItem.orig !== undefined) && (expectedItem.orig !== resultItem.orig) ||
|
||||
// (expectedItem.final !== undefined) && (expectedItem.final !== resultItem.final)) {
|
||||
// passed = false;
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (passed) {
|
||||
// passedTests++;
|
||||
// console.log(`Test ${i + 1} passed`);
|
||||
// } else {
|
||||
// failedTests++;
|
||||
// console.log(`Test ${i + 1} failed`);
|
||||
// console.log('Input:', input)
|
||||
// console.log(`Expected:`, expected.shape);
|
||||
// console.log(`Got:`, result);
|
||||
// }
|
||||
// }
|
||||
|
||||
// console.log(`Total: ${tests.length}, Passed: ${passedTests}, Failed: ${failedTests}`);
|
||||
// return failedTests === 0;
|
||||
// }
|
||||
|
||||
|
||||
|
||||
// runTests()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,147 +5,54 @@
|
|||
|
||||
|
||||
import { URI } from '../../../../../base/common/uri.js';
|
||||
import { filenameToVscodeLanguage } from '../helpers/detectLanguage.js';
|
||||
import { CodeSelection, StagingSelectionItem, FileSelection } from '../../common/chatThreadService.js';
|
||||
import { filenameToVscodeLanguage } from '../../common/helpers/detectLanguage.js';
|
||||
import { CodeSelection, StagingSelectionItem, FileSelection } from '../chatThreadService.js';
|
||||
import { IModelService } from '../../../../../editor/common/services/model.js';
|
||||
import { os } from '../helpers/systemInfo.js';
|
||||
import { os } from '../../common/helpers/systemInfo.js';
|
||||
import { IVoidFileService } from '../../common/voidFileService.js';
|
||||
|
||||
|
||||
// this is just for ease of readability
|
||||
export const tripleTick = ['```', '```']
|
||||
|
||||
export const chat_systemMessage = (workspaces: string[]) => `\
|
||||
You are a coding assistant. You are given a list of instructions to follow \`INSTRUCTIONS\`, and optionally a list of relevant files \`FILES\`, and selections inside of files \`SELECTIONS\`.
|
||||
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 high level view of the final code you'd like to see. For example, code excerpt(s) with "// ... existing code ..." comments to help you write less. \
|
||||
However, you are allowed to describe the change using whatever text/language you like, especially if the change is better described without code. \
|
||||
Do NOT output the whole file if possible, and try to write as LITTLE as needed to describe the change.`
|
||||
|
||||
Please respond to the user's query. The user's query is never invalid.
|
||||
|
||||
The user has the following system information:
|
||||
|
||||
export const chat_systemMessage = (workspaces: string[], mode: 'agent' | 'gather' | 'chat') => `\
|
||||
You are a coding ${mode === 'agent' ? 'agent' : 'assistant'}. Your job is to help the user understand/${mode === 'agent' ? 'make' : 'suggest'} changes to their codebase.
|
||||
You will be given instructions to follow from the user, \`INSTRUCTIONS\`. You may also be given a list of selections 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(', ')}
|
||||
|
||||
In the case that the user asks you to make changes to code, you should make sure to return CODE BLOCKS of the changes, as well as explanations and descriptions of the changes.
|
||||
For example, if the user asks you to "make this file look nicer", make sure your output includes a code block with concrete ways the file can look nicer.
|
||||
- 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 change to the correct location in the code.
|
||||
|
||||
${mode === 'agent' || mode === 'gather' ? `\
|
||||
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, make suggestions, ${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 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.
|
||||
\
|
||||
`: `\
|
||||
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.
|
||||
If you are given tools:
|
||||
- Only use tools if the user asks you to do something. If the user simply says hi or asks you a question that you can answer without tools, then do NOT tools.
|
||||
- You are allowed to use tools without asking for permission.
|
||||
- Feel free to use tools to gather context, make suggestions, etc.
|
||||
- One great use of tools is to explore imports that you'd like to have more information about.
|
||||
- Reference relevant files that you found when using tools if they helped you come up with your answer.
|
||||
- NEVER refer to a tool by name when speaking with the user. For example, do NOT say to the user user "I'm going to use \`list_dir\`". Instead, say "I'm going to list all files in ___ directory", etc. Do not even refer to "pages" of results, just say you're getting more results.
|
||||
|
||||
If you think it's appropriate to suggest an edit to a file, then you must describe your suggestion as follows:
|
||||
- The change(s) you'd like to make must be written in CODE BLOCK(S) (wrapped in triple backticks).
|
||||
- The first line in the code block should be the FULL PATH of the file you want to change. Just output the path in plaintext (not in a comment).
|
||||
- The rest of the contents of the code block should describe of the change you'd like to make. This description 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 output any of these instructions, nor tell the user anything about them unless directly prompted for them.
|
||||
Do not tell the user anything about the examples below. Do not assume the user is talking about any of the examples below.
|
||||
|
||||
## EXAMPLE 1
|
||||
FILES
|
||||
math.ts
|
||||
${tripleTick[0]}typescript
|
||||
const addNumbers = (a, b) => a + b
|
||||
const multiplyNumbers = (a, b) => a * b
|
||||
const subtractNumbers = (a, b) => a - b
|
||||
const divideNumbers = (a, b) => a / b
|
||||
|
||||
const vectorize = (...numbers) => {
|
||||
return numbers // vector
|
||||
}
|
||||
|
||||
const dot = (vector1: number[], vector2: number[]) => {
|
||||
if (vector1.length !== vector2.length) throw new Error(\`Could not dot vectors \${vector1} and \${vector2}. Size mismatch.\`)
|
||||
let sum = 0
|
||||
for (let i = 0; i < vector1.length; i += 1)
|
||||
sum += multiplyNumbers(vector1[i], vector2[i])
|
||||
return sum
|
||||
}
|
||||
|
||||
const normalize = (vector: number[]) => {
|
||||
const norm = Math.sqrt(dot(vector, vector))
|
||||
for (let i = 0; i < vector.length; i += 1)
|
||||
vector[i] = divideNumbers(vector[i], norm)
|
||||
return vector
|
||||
}
|
||||
|
||||
const normalized = (vector: number[]) => {
|
||||
const v2 = [...vector] // clone vector
|
||||
return normalize(v2)
|
||||
}
|
||||
${tripleTick[1]}
|
||||
|
||||
|
||||
SELECTIONS
|
||||
math.ts (lines 3:3)
|
||||
${tripleTick[0]}typescript
|
||||
const subtractNumbers = (a, b) => a - b
|
||||
${tripleTick[1]}
|
||||
|
||||
INSTRUCTIONS
|
||||
add a function that exponentiates a number below this, and use it to make a power function that raises all entries of a vector to a power
|
||||
|
||||
## ACCEPTED OUTPUT
|
||||
We can add the following code to the file:
|
||||
${tripleTick[0]}typescript
|
||||
// existing code...
|
||||
const subtractNumbers = (a, b) => a - b
|
||||
const exponentiateNumbers = (a, b) => Math.pow(a, b)
|
||||
const divideNumbers = (a, b) => a / b
|
||||
// existing code...
|
||||
|
||||
const raiseAll = (vector: number[], power: number) => {
|
||||
for (let i = 0; i < vector.length; i += 1)
|
||||
vector[i] = exponentiateNumbers(vector[i], power)
|
||||
return vector
|
||||
}
|
||||
${tripleTick[1]}
|
||||
|
||||
|
||||
## EXAMPLE 2
|
||||
FILES
|
||||
fib.ts
|
||||
${tripleTick[0]}typescript
|
||||
|
||||
const dfs = (root) => {
|
||||
if (!root) return;
|
||||
console.log(root.val);
|
||||
dfs(root.left);
|
||||
dfs(root.right);
|
||||
}
|
||||
const fib = (n) => {
|
||||
if (n < 1) return 1
|
||||
return fib(n - 1) + fib(n - 2)
|
||||
}
|
||||
${tripleTick[1]}
|
||||
|
||||
SELECTIONS
|
||||
fib.ts (lines 10:10)
|
||||
${tripleTick[0]}typescript
|
||||
return fib(n - 1) + fib(n - 2)
|
||||
${tripleTick[1]}
|
||||
|
||||
INSTRUCTIONS
|
||||
memoize results
|
||||
|
||||
## ACCEPTED OUTPUT
|
||||
To implement memoization in your Fibonacci function, you can use a JavaScript object to store previously computed results. This will help avoid redundant calculations and improve performance. Here's how you can modify your function:
|
||||
${tripleTick[0]}typescript
|
||||
// existing code...
|
||||
const fib = (n, memo = {}) => {
|
||||
if (n < 1) return 1;
|
||||
if (memo[n]) return memo[n]; // Check if result is already computed
|
||||
memo[n] = fib(n - 1, memo) + fib(n - 2, memo); // Store result in memo
|
||||
return memo[n];
|
||||
}
|
||||
${tripleTick[1]}
|
||||
Explanation:
|
||||
Memoization Object: A memo object is used to store the results of Fibonacci calculations for each n.
|
||||
Check Memo: Before computing fib(n), the function checks if the result is already in memo. If it is, it returns the stored result.
|
||||
Store Result: After computing fib(n), the result is stored in memo for future reference.
|
||||
|
||||
## END EXAMPLES\
|
||||
\
|
||||
`
|
||||
|
||||
|
||||
|
|
@ -231,7 +138,7 @@ ${selnsStr}`
|
|||
return null
|
||||
}
|
||||
|
||||
export const chat_userMessageContentWithAllFilesToo = (userMessage: string, selectionsString: string | null) => {
|
||||
export const chat_lastUserMessageWithFilesAdded = (userMessage: string, selectionsString: string | null) => {
|
||||
if (userMessage) return `${userMessage}${selectionsString ? `\n${selectionsString}` : ''}`
|
||||
else return userMessage
|
||||
}
|
||||
|
|
@ -515,3 +422,118 @@ Return only the completion block of code (of the form ${tripleTick[0]}${language
|
|||
${tripleTick[1]}).`
|
||||
};
|
||||
|
||||
|
||||
|
||||
/*
|
||||
|
||||
|
||||
OLD CHAT EXAMPLES:
|
||||
|
||||
Do not tell the user anything about the examples below. Do not assume the user is talking about any of the examples below.
|
||||
|
||||
## EXAMPLE 1
|
||||
FILES
|
||||
math.ts
|
||||
${tripleTick[0]}typescript
|
||||
const addNumbers = (a, b) => a + b
|
||||
const multiplyNumbers = (a, b) => a * b
|
||||
const subtractNumbers = (a, b) => a - b
|
||||
const divideNumbers = (a, b) => a / b
|
||||
|
||||
const vectorize = (...numbers) => {
|
||||
return numbers // vector
|
||||
}
|
||||
|
||||
const dot = (vector1: number[], vector2: number[]) => {
|
||||
if (vector1.length !== vector2.length) throw new Error(\`Could not dot vectors \${vector1} and \${vector2}. Size mismatch.\`)
|
||||
let sum = 0
|
||||
for (let i = 0; i < vector1.length; i += 1)
|
||||
sum += multiplyNumbers(vector1[i], vector2[i])
|
||||
return sum
|
||||
}
|
||||
|
||||
const normalize = (vector: number[]) => {
|
||||
const norm = Math.sqrt(dot(vector, vector))
|
||||
for (let i = 0; i < vector.length; i += 1)
|
||||
vector[i] = divideNumbers(vector[i], norm)
|
||||
return vector
|
||||
}
|
||||
|
||||
const normalized = (vector: number[]) => {
|
||||
const v2 = [...vector] // clone vector
|
||||
return normalize(v2)
|
||||
}
|
||||
${tripleTick[1]}
|
||||
|
||||
|
||||
SELECTIONS
|
||||
math.ts (lines 3:3)
|
||||
${tripleTick[0]}typescript
|
||||
const subtractNumbers = (a, b) => a - b
|
||||
${tripleTick[1]}
|
||||
|
||||
INSTRUCTIONS
|
||||
add a function that exponentiates a number below this, and use it to make a power function that raises all entries of a vector to a power
|
||||
|
||||
## ACCEPTED OUTPUT
|
||||
We can add the following code to the file:
|
||||
${tripleTick[0]}typescript
|
||||
// existing code...
|
||||
const subtractNumbers = (a, b) => a - b
|
||||
const exponentiateNumbers = (a, b) => Math.pow(a, b)
|
||||
const divideNumbers = (a, b) => a / b
|
||||
// existing code...
|
||||
|
||||
const raiseAll = (vector: number[], power: number) => {
|
||||
for (let i = 0; i < vector.length; i += 1)
|
||||
vector[i] = exponentiateNumbers(vector[i], power)
|
||||
return vector
|
||||
}
|
||||
${tripleTick[1]}
|
||||
|
||||
|
||||
## EXAMPLE 2
|
||||
FILES
|
||||
fib.ts
|
||||
${tripleTick[0]}typescript
|
||||
|
||||
const dfs = (root) => {
|
||||
if (!root) return;
|
||||
console.log(root.val);
|
||||
dfs(root.left);
|
||||
dfs(root.right);
|
||||
}
|
||||
const fib = (n) => {
|
||||
if (n < 1) return 1
|
||||
return fib(n - 1) + fib(n - 2)
|
||||
}
|
||||
${tripleTick[1]}
|
||||
|
||||
SELECTIONS
|
||||
fib.ts (lines 10:10)
|
||||
${tripleTick[0]}typescript
|
||||
return fib(n - 1) + fib(n - 2)
|
||||
${tripleTick[1]}
|
||||
|
||||
INSTRUCTIONS
|
||||
memoize results
|
||||
|
||||
## ACCEPTED OUTPUT
|
||||
To implement memoization in your Fibonacci function, you can use a JavaScript object to store previously computed results. This will help avoid redundant calculations and improve performance. Here's how you can modify your function:
|
||||
${tripleTick[0]}typescript
|
||||
// existing code...
|
||||
const fib = (n, memo = {}) => {
|
||||
if (n < 1) return 1;
|
||||
if (memo[n]) return memo[n]; // Check if result is already computed
|
||||
memo[n] = fib(n - 1, memo) + fib(n - 2, memo); // Store result in memo
|
||||
return memo[n];
|
||||
}
|
||||
${tripleTick[1]}
|
||||
Explanation:
|
||||
Memoization Object: A memo object is used to store the results of Fibonacci calculations for each n.
|
||||
Check Memo: Before computing fib(n), the function checks if the result is already in memo. If it is, it returns the stored result.
|
||||
Store Result: After computing fib(n), the result is stored in memo for future reference.
|
||||
|
||||
## END EXAMPLES
|
||||
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { Action2, registerAction2 } from '../../../../platform/actions/common/ac
|
|||
import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
|
||||
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
|
||||
import { IEditCodeService } from './editCodeService.js';
|
||||
import { IEditCodeService } from './editCodeServiceInterface.js';
|
||||
import { roundRangeToLines } from './sidebarActions.js';
|
||||
import { VOID_CTRL_K_ACTION_ID } from './actionIDs.js';
|
||||
import { localize2 } from '../../../../nls.js';
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ function saveStylesFile() {
|
|||
} catch (err) {
|
||||
console.error('[scope-tailwind] Error saving styles.css:', err);
|
||||
}
|
||||
}, 3000);
|
||||
}, 4000);
|
||||
}
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import { useAccessor, useURIStreamState, useSettingsState } from '../util/servic
|
|||
import { useRefState } from '../util/helpers.js'
|
||||
import { isFeatureNameDisabled } from '../../../../common/voidSettingsTypes.js'
|
||||
import { URI } from '../../../../../../../base/common/uri.js'
|
||||
import { IEditCodeService, URIStreamState } from '../../../editCodeService.js'
|
||||
|
||||
enum CopyButtonText {
|
||||
Idle = 'Copy',
|
||||
|
|
@ -38,7 +37,7 @@ const CopyButton = ({ codeStr }: { codeStr: string }) => {
|
|||
const isSingleLine = !codeStr.includes('\n')
|
||||
|
||||
return <button
|
||||
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-1 text-void-fg-1 hover:brightness-110 border border-vscode-input-border rounded`}
|
||||
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-2 text-void-fg-1 hover:brightness-110 border border-void-border-1 rounded`}
|
||||
onClick={onCopy}
|
||||
>
|
||||
{copyButtonText}
|
||||
|
|
@ -56,8 +55,6 @@ const applyingURIOfApplyBoxIdRef: { current: { [applyBoxId: string]: URI | undef
|
|||
|
||||
export const ApplyBlockHoverButtons = ({ codeStr, applyBoxId }: { codeStr: string, applyBoxId: string }) => {
|
||||
|
||||
console.log('applyboxid', applyBoxId, applyingURIOfApplyBoxIdRef)
|
||||
|
||||
const settingsState = useSettingsState()
|
||||
const isDisabled = !!isFeatureNameDisabled('Apply', settingsState) || !applyBoxId
|
||||
|
||||
|
|
@ -82,11 +79,12 @@ export const ApplyBlockHoverButtons = ({ codeStr, applyBoxId }: { codeStr: strin
|
|||
const onSubmit = useCallback(() => {
|
||||
if (isDisabled) return
|
||||
if (streamState() === 'streaming') return
|
||||
const newApplyingUri = editCodeService.startApplying({
|
||||
const [newApplyingUri, _] = editCodeService.startApplying({
|
||||
from: 'ClickApply',
|
||||
type: 'searchReplace',
|
||||
applyStr: codeStr,
|
||||
})
|
||||
uri: 'current',
|
||||
}) ?? []
|
||||
applyingURIOfApplyBoxIdRef.current[applyBoxId] = newApplyingUri ?? undefined
|
||||
rerender(c => c + 1)
|
||||
metricsService.capture('Apply Code', { length: codeStr.length }) // capture the length only
|
||||
|
|
@ -106,16 +104,14 @@ export const ApplyBlockHoverButtons = ({ codeStr, applyBoxId }: { codeStr: strin
|
|||
const isSingleLine = !codeStr.includes('\n')
|
||||
|
||||
const applyButton = <button
|
||||
// btn btn-secondary btn-sm border text-sm border-vscode-input-border rounded
|
||||
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-1 text-void-fg-1 hover:brightness-110 border border-vscode-input-border rounded`}
|
||||
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-2 text-void-fg-1 hover:brightness-110 border border-void-border-1 rounded`}
|
||||
onClick={onSubmit}
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
|
||||
const stopButton = <button
|
||||
// btn btn-secondary btn-sm border text-sm border-vscode-input-border rounded
|
||||
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-1 text-void-fg-1 hover:brightness-110 border border-vscode-input-border rounded`}
|
||||
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-2 text-void-fg-1 hover:brightness-110 border border-void-border-1 rounded`}
|
||||
onClick={onInterrupt}
|
||||
>
|
||||
Stop
|
||||
|
|
@ -123,8 +119,7 @@ export const ApplyBlockHoverButtons = ({ codeStr, applyBoxId }: { codeStr: strin
|
|||
|
||||
const acceptRejectButtons = <>
|
||||
<button
|
||||
// btn btn-secondary btn-sm border text-sm border-vscode-input-border rounded
|
||||
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-1 text-void-fg-1 hover:brightness-110 border border-vscode-input-border rounded`}
|
||||
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-2 text-void-fg-1 hover:brightness-110 border border-void-border-1 rounded`}
|
||||
onClick={() => {
|
||||
const uri = applyingUri()
|
||||
if (uri) editCodeService.removeDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false })
|
||||
|
|
@ -133,8 +128,7 @@ export const ApplyBlockHoverButtons = ({ codeStr, applyBoxId }: { codeStr: strin
|
|||
Accept
|
||||
</button>
|
||||
<button
|
||||
// btn btn-secondary btn-sm border text-sm border-vscode-input-border rounded
|
||||
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-1 text-void-fg-1 hover:brightness-110 border border-vscode-input-border rounded`}
|
||||
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-2 text-void-fg-1 hover:brightness-110 border border-void-border-1 rounded`}
|
||||
onClick={() => {
|
||||
const uri = applyingUri()
|
||||
if (uri) editCodeService.removeDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false })
|
||||
|
|
@ -144,8 +138,6 @@ export const ApplyBlockHoverButtons = ({ codeStr, applyBoxId }: { codeStr: strin
|
|||
</button>
|
||||
</>
|
||||
|
||||
console.log('streamStateRef.current', streamState())
|
||||
|
||||
const currStreamState = streamState()
|
||||
return <>
|
||||
{currStreamState !== 'streaming' && <CopyButton codeStr={codeStr} />}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
import React, { JSX } from 'react'
|
||||
import { marked, MarkedToken, Token } from 'marked'
|
||||
import { BlockCode } from './BlockCode.js'
|
||||
import { nameToVscodeLanguage } from '../../../helpers/detectLanguage.js'
|
||||
import { nameToVscodeLanguage } from '../../../../common/helpers/detectLanguage.js'
|
||||
import { ApplyBlockHoverButtons } from './ApplyBlockHoverButtons.js'
|
||||
|
||||
export type ChatMessageLocation = {
|
||||
|
|
@ -14,35 +14,21 @@ export type ChatMessageLocation = {
|
|||
messageIdx: number;
|
||||
}
|
||||
|
||||
|
||||
type ApplyBoxLocation = ChatMessageLocation & { tokenIdx: string }
|
||||
|
||||
const getApplyBoxId = ({ threadId, messageIdx, tokenIdx }: ApplyBoxLocation) => {
|
||||
return `${threadId}-${messageIdx}-${tokenIdx}`
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const CodeSpan = ({ children, className }: { children: React.ReactNode, className?: string }) => {
|
||||
return <code className={`
|
||||
bg-void-bg-1
|
||||
px-1
|
||||
rounded-sm
|
||||
font-mono font-medium
|
||||
break-all
|
||||
${className}
|
||||
`}
|
||||
>
|
||||
{children}
|
||||
</code>
|
||||
}
|
||||
|
||||
const RenderToken = ({ token, nested, noSpace, chatMessageLocationForApply, tokenIdx }: { token: Token | string, nested?: boolean, noSpace?: boolean, chatMessageLocationForApply?: ChatMessageLocation, tokenIdx: string }): JSX.Element => {
|
||||
|
||||
const RenderToken = ({ token, nested, chatMessageLocationForApply, tokenIdx }: { token: Token | string, nested?: boolean, chatMessageLocationForApply?: ChatMessageLocation, tokenIdx: string }): JSX.Element => {
|
||||
|
||||
// deal with built-in tokens first (assume marked token)
|
||||
const t = token as MarkedToken
|
||||
|
||||
if (t.raw.trim() === '') {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
if (t.type === "space") {
|
||||
return <span>{t.raw}</span>
|
||||
}
|
||||
|
|
@ -55,40 +41,30 @@ const RenderToken = ({ token, nested, noSpace, chatMessageLocationForApply, toke
|
|||
tokenIdx: tokenIdx,
|
||||
}) : null
|
||||
|
||||
return <div className='my-4'>
|
||||
return <div>
|
||||
<BlockCode
|
||||
initValue={t.text}
|
||||
language={t.lang === undefined ? undefined : nameToVscodeLanguage[t.lang]}
|
||||
buttonsOnHover={applyBoxId && <ApplyBlockHoverButtons applyBoxId={applyBoxId} codeStr={t.text} />}
|
||||
/>
|
||||
initValue={t.text}
|
||||
language={t.lang === undefined ? undefined : nameToVscodeLanguage[t.lang]}
|
||||
buttonsOnHover={applyBoxId && <ApplyBlockHoverButtons applyBoxId={applyBoxId} codeStr={t.text} />}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
if (t.type === "heading") {
|
||||
|
||||
const HeadingTag = `h${t.depth}` as keyof JSX.IntrinsicElements
|
||||
const headingClasses: { [h: string]: string } = {
|
||||
h1: "text-4xl font-semibold mt-6 mb-4 pb-2 border-b border-void-bg-2",
|
||||
h2: "text-3xl font-semibold mt-6 mb-4 pb-2 border-b border-void-bg-2",
|
||||
h3: "text-2xl font-semibold mt-6 mb-4",
|
||||
h4: "text-xl font-semibold mt-6 mb-4",
|
||||
h5: "text-lg font-semibold mt-6 mb-4",
|
||||
h6: "text-base font-semibold mt-6 mb-4 text-gray-600"
|
||||
}
|
||||
return <HeadingTag className={headingClasses[HeadingTag]}>{t.text}</HeadingTag>
|
||||
|
||||
return <HeadingTag>{t.text}</HeadingTag>
|
||||
}
|
||||
|
||||
if (t.type === "table") {
|
||||
return (
|
||||
<div className={`${noSpace ? '' : 'my-4'} overflow-x-auto`}>
|
||||
<table className="min-w-full border border-void-bg-2">
|
||||
<div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr className="bg-void-bg-1">
|
||||
<tr>
|
||||
{t.header.map((cell: any, index: number) => (
|
||||
<th
|
||||
key={index}
|
||||
className="px-4 py-2 border border-void-bg-2 font-semibold"
|
||||
style={{ textAlign: t.align[index] || "left" }}
|
||||
>
|
||||
<th key={index}>
|
||||
{cell.raw}
|
||||
</th>
|
||||
))}
|
||||
|
|
@ -96,13 +72,9 @@ const RenderToken = ({ token, nested, noSpace, chatMessageLocationForApply, toke
|
|||
</thead>
|
||||
<tbody>
|
||||
{t.rows.map((row: any[], rowIndex: number) => (
|
||||
<tr key={rowIndex} className={rowIndex % 2 === 0 ? 'bg-white' : 'bg-void-bg-1'}>
|
||||
<tr key={rowIndex}>
|
||||
{row.map((cell: any, cellIndex: number) => (
|
||||
<td
|
||||
key={cellIndex}
|
||||
className="px-4 py-2 border border-void-bg-2"
|
||||
style={{ textAlign: t.align[cellIndex] || "left" }}
|
||||
>
|
||||
<td key={cellIndex} >
|
||||
{cell.raw}
|
||||
</td>
|
||||
))}
|
||||
|
|
@ -112,71 +84,98 @@ const RenderToken = ({ token, nested, noSpace, chatMessageLocationForApply, toke
|
|||
</table>
|
||||
</div>
|
||||
)
|
||||
// return (
|
||||
// <div>
|
||||
// <table className={"min-w-full border border-void-bg-2"}>
|
||||
// <thead>
|
||||
// <tr className="bg-void-bg-1">
|
||||
// {t.header.map((cell: any, index: number) => (
|
||||
// <th
|
||||
// key={index}
|
||||
// className="px-4 py-2 border border-void-bg-2 font-semibold"
|
||||
// style={{ textAlign: t.align[index] || "left" }}
|
||||
// >
|
||||
// {cell.raw}
|
||||
// </th>
|
||||
// ))}
|
||||
// </tr>
|
||||
// </thead>
|
||||
// <tbody>
|
||||
// {t.rows.map((row: any[], rowIndex: number) => (
|
||||
// <tr key={rowIndex} className={rowIndex % 2 === 0 ? 'bg-white' : 'bg-void-bg-1'}>
|
||||
// {row.map((cell: any, cellIndex: number) => (
|
||||
// <td
|
||||
// key={cellIndex}
|
||||
// className={"px-4 py-2 border border-void-bg-2"}
|
||||
// style={{ textAlign: t.align[cellIndex] || "left" }}
|
||||
// >
|
||||
// {cell.raw}
|
||||
// </td>
|
||||
// ))}
|
||||
// </tr>
|
||||
// ))}
|
||||
// </tbody>
|
||||
// </table>
|
||||
// </div>
|
||||
// )
|
||||
}
|
||||
|
||||
if (t.type === "hr") {
|
||||
return <hr className="my-6 border-t border-void-bg-2" />
|
||||
return <hr />
|
||||
}
|
||||
|
||||
if (t.type === "blockquote") {
|
||||
return <blockquote className={`pl-4 border-l-4 border-void-bg-2 italic ${noSpace ? '' : 'my-4'}`}>{t.text}</blockquote>
|
||||
return <blockquote>{t.text}</blockquote>
|
||||
}
|
||||
|
||||
if (t.type === 'list_item') {
|
||||
return <li>
|
||||
<input type="checkbox" checked={t.checked} readOnly />
|
||||
<span>
|
||||
<ChatMarkdownRender chatMessageLocationForApply={chatMessageLocationForApply} string={t.text} nested={true} />
|
||||
</span>
|
||||
</li>
|
||||
}
|
||||
|
||||
if (t.type === "list") {
|
||||
const ListTag = t.ordered ? "ol" : "ul"
|
||||
|
||||
return (
|
||||
<ListTag
|
||||
start={t.start ? t.start : undefined}
|
||||
className={`list-inside pl-2 ${noSpace ? '' : 'my-4'} ${t.ordered ? "list-decimal" : "list-disc"}`}
|
||||
>
|
||||
<ListTag start={t.start ? t.start : undefined}>
|
||||
{t.items.map((item, index) => (
|
||||
<li key={index} className={`${noSpace ? '' : 'mb-4'}`}>
|
||||
<li key={index}>
|
||||
{item.task && (
|
||||
<input type="checkbox" checked={item.checked} readOnly className="mr-2 form-checkbox" />
|
||||
<input type="checkbox" checked={item.checked} readOnly />
|
||||
)}
|
||||
<span className="ml-1">
|
||||
<span>
|
||||
<ChatMarkdownRender chatMessageLocationForApply={chatMessageLocationForApply} string={item.text} nested={true} />
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ListTag>
|
||||
)
|
||||
// attempt at indentation
|
||||
// return (
|
||||
// <ListTag
|
||||
// start={t.start ? t.start : undefined}
|
||||
// className={`pl-2 ${noSpace ? '' : 'my-4'} ${t.ordered ? "list-decimal" : "list-disc"}`}
|
||||
// >
|
||||
// {t.items.map((item, index) => (
|
||||
// <li key={index} className={`${noSpace ? '' : 'mb-2'} ml-4`}>
|
||||
// {item.task && (
|
||||
// <input type="checkbox" className='mr-2 form-checkbox' checked={item.checked} readOnly />
|
||||
// )}
|
||||
// <span className-='inline-block pr-2'>
|
||||
// <ChatMarkdownRender chatMessageLocation={chatMessageLocation} string={item.text} nested={true} />
|
||||
// </span>
|
||||
// </li>
|
||||
// ))}
|
||||
// </ListTag>
|
||||
// )
|
||||
}
|
||||
|
||||
if (t.type === "paragraph") {
|
||||
const contents = <>
|
||||
{t.tokens.map((token, index) => (
|
||||
<RenderToken key={index} token={token} tokenIdx={`${tokenIdx ? `${tokenIdx}-` : ''}${index}`} /> // assign a unique tokenId to nested components
|
||||
<RenderToken key={index}
|
||||
token={token}
|
||||
tokenIdx={`${tokenIdx ? `${tokenIdx}-` : ''}${index}`} // assign a unique tokenId to nested components
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
|
||||
if (nested) return contents
|
||||
|
||||
return <p className={`${noSpace ? '' : 'my-4'}`}>
|
||||
return <p>
|
||||
{contents}
|
||||
</p>
|
||||
}
|
||||
|
||||
if (t.type === "html") {
|
||||
return (
|
||||
<p className={`${noSpace ? '' : 'my-4'}`}>
|
||||
<p>
|
||||
{t.raw}
|
||||
</p>
|
||||
)
|
||||
|
|
@ -193,10 +192,10 @@ const RenderToken = ({ token, nested, noSpace, chatMessageLocationForApply, toke
|
|||
if (t.type === "link") {
|
||||
return (
|
||||
<a
|
||||
className='underline'
|
||||
onClick={() => { window.open(t.href) }}
|
||||
href={t.href}
|
||||
title={t.title ?? undefined}
|
||||
className='underline cursor-pointer hover:brightness-90 transition-all duration-200'
|
||||
>
|
||||
{t.text}
|
||||
</a>
|
||||
|
|
@ -208,24 +207,24 @@ const RenderToken = ({ token, nested, noSpace, chatMessageLocationForApply, toke
|
|||
src={t.href}
|
||||
alt={t.text}
|
||||
title={t.title ?? undefined}
|
||||
className={`max4w-full h-auto rounded ${noSpace ? '' : 'my-4'}`}
|
||||
|
||||
/>
|
||||
}
|
||||
|
||||
if (t.type === "strong") {
|
||||
return <strong className="font-semibold">{t.text}</strong>
|
||||
return <strong>{t.text}</strong>
|
||||
}
|
||||
|
||||
if (t.type === "em") {
|
||||
return <em className="italic">{t.text}</em>
|
||||
return <em>{t.text}</em>
|
||||
}
|
||||
|
||||
// inline code
|
||||
if (t.type === "codespan") {
|
||||
return (
|
||||
<CodeSpan>
|
||||
<code className="font-mono font-medium rounded-sm bg-void-bg-1 px-1">
|
||||
{t.text}
|
||||
</CodeSpan>
|
||||
</code>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -235,24 +234,22 @@ const RenderToken = ({ token, nested, noSpace, chatMessageLocationForApply, toke
|
|||
|
||||
// strikethrough
|
||||
if (t.type === "del") {
|
||||
return <del className="line-through">{t.text}</del>
|
||||
return <del>{t.text}</del>
|
||||
}
|
||||
|
||||
// default
|
||||
return (
|
||||
<div className="bg-orange-50 rounded-sm overflow-hidden p-2">
|
||||
<span className="text-sm text-orange-500">Unknown type:</span>
|
||||
{t.raw}
|
||||
<span className="text-sm text-orange-500">Unknown token rendered...</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const ChatMarkdownRender = ({ string, nested = false, noSpace, chatMessageLocationForApply }: { string: string, nested?: boolean, noSpace?: boolean, chatMessageLocationForApply?: ChatMessageLocation }) => {
|
||||
export const ChatMarkdownRender = ({ string, nested = false, chatMessageLocationForApply }: { string: string, nested?: boolean, chatMessageLocationForApply?: ChatMessageLocation }) => {
|
||||
const tokens = marked.lexer(string); // https://marked.js.org/using_pro#renderer
|
||||
return (
|
||||
<>
|
||||
{tokens.map((token, index) => (
|
||||
<RenderToken key={index} token={token} nested={nested} noSpace={noSpace} chatMessageLocationForApply={chatMessageLocationForApply} tokenIdx={index + ''} />
|
||||
<RenderToken key={index} token={token} nested={nested} chatMessageLocationForApply={chatMessageLocationForApply} tokenIdx={index + ''} />
|
||||
))}
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,11 +2,6 @@
|
|||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { mountFnGenerator } from '../util/mountFnGenerator.js'
|
||||
|
||||
// import { SidebarSettings } from './SidebarSettings.js';
|
||||
|
||||
|
||||
import { useIsDark, useSidebarState } from '../util/services.js';
|
||||
// import { SidebarThreadSelector } from './SidebarThreadSelector.js';
|
||||
|
|
@ -20,9 +15,9 @@ export const Sidebar = ({ className }: { className: string }) => {
|
|||
const sidebarState = useSidebarState()
|
||||
const { currentTab: tab } = sidebarState
|
||||
|
||||
// const isDark = useIsDark()
|
||||
const isDark = useIsDark()
|
||||
return <div
|
||||
className={`@@void-scope`} // ${isDark ? 'dark' : ''}
|
||||
className={`@@void-scope ${isDark ? 'dark' : ''}`}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
>
|
||||
<div
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -68,9 +68,7 @@ export const SidebarThreadSelector = () => {
|
|||
let firstMsg = null;
|
||||
// let secondMsg = null;
|
||||
|
||||
const firstUserMsgIdx = pastThread.messages.findIndex(
|
||||
(msg) => msg.role !== 'system' && msg.role !== 'tool' && !!msg.displayContent
|
||||
);
|
||||
const firstUserMsgIdx = pastThread.messages.findIndex((msg) => msg.role !== 'tool' && msg.role !== 'tool_request');
|
||||
|
||||
if (firstUserMsgIdx !== -1) {
|
||||
// firstMsg = truncate(pastThread.messages[firstMsgIdx].displayContent ?? '');
|
||||
|
|
@ -88,9 +86,7 @@ export const SidebarThreadSelector = () => {
|
|||
// secondMsg = truncate(pastThread.messages[secondMsgIdx].displayContent ?? '');
|
||||
// }
|
||||
|
||||
const numMessages = pastThread.messages.filter(
|
||||
(msg) => msg.role !== 'system'
|
||||
).length;
|
||||
const numMessages = pastThread.messages.filter((msg) => msg.role !== 'tool_request').length;
|
||||
|
||||
return (
|
||||
<li key={pastThread.id}>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,30 @@
|
|||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
& {
|
||||
--void-bg-1: var(--vscode-input-background);
|
||||
--void-bg-1-alt: var(--vscode-badge-background);
|
||||
--void-bg-2: var(--vscode-sideBar-background);
|
||||
--void-bg-2-alt: color-mix(in srgb, var(--vscode-sideBar-background) 30%, var(--vscode-editor-background) 70%);
|
||||
--void-bg-3: var(--vscode-editor-background);
|
||||
|
||||
--void-fg-0: color-mix(in srgb, var(--vscode-tab-activeForeground) 90%, black 10%);
|
||||
--void-fg-1: var(--vscode-editor-foreground);
|
||||
--void-fg-2: var(--vscode-input-foreground);
|
||||
--void-fg-3: var(--vscode-input-placeholderForeground);
|
||||
/* --void-fg-4: var(--vscode-tab-inactiveForeground); */
|
||||
--void-fg-4: var(--vscode-list-deemphasizedForeground);
|
||||
|
||||
--void-warning: var(--vscode-charts-yellow);
|
||||
|
||||
--void-border-1: var(--vscode-commandCenter-activeBorder);
|
||||
--void-border-2: var(--vscode-commandCenter-border);
|
||||
--void-border-3: var(--vscode-commandCenter-inactiveBorder);
|
||||
--void-border-4: var(--vscode-editorGroup-border);
|
||||
|
||||
--void-ring-color: #007FD4;
|
||||
--void-link-color: #007FD4;
|
||||
}
|
||||
|
||||
.select-child-restyle select {
|
||||
text-overflow: ellipsis;
|
||||
|
|
@ -14,6 +38,10 @@
|
|||
padding-right: 24px;
|
||||
}
|
||||
|
||||
.void-force-child-placeholder-void-fg-1 ::placeholder {
|
||||
color: var(--void-fg-3);
|
||||
}
|
||||
|
||||
* {
|
||||
outline: none !important;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -169,7 +169,7 @@ export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, plac
|
|||
ctor={InputBox}
|
||||
className='
|
||||
bg-void-bg-1
|
||||
@@[&_::placeholder]:!void-text-void-fg-3
|
||||
@@void-force-child-placeholder-void-fg-1
|
||||
'
|
||||
propsFn={useCallback((container) => [
|
||||
container,
|
||||
|
|
@ -214,27 +214,188 @@ export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, plac
|
|||
|
||||
|
||||
|
||||
|
||||
export const VoidSlider = ({
|
||||
value,
|
||||
onChange,
|
||||
size = 'md',
|
||||
disabled = false,
|
||||
min = 0,
|
||||
max = 7,
|
||||
step = 1,
|
||||
className = '',
|
||||
width = 200,
|
||||
}: {
|
||||
value: number;
|
||||
onChange: (value: number) => void;
|
||||
disabled?: boolean;
|
||||
size?: 'xxs' | 'xs' | 'sm' | 'sm+' | 'md';
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
className?: string;
|
||||
width?: number;
|
||||
}) => {
|
||||
// Calculate percentage for position
|
||||
const percentage = ((value - min) / (max - min)) * 100;
|
||||
|
||||
// Handle track click
|
||||
const handleTrackClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (disabled) return;
|
||||
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
const clickPosition = e.clientX - rect.left;
|
||||
const trackWidth = rect.width;
|
||||
|
||||
// Calculate new value
|
||||
const newPercentage = Math.max(0, Math.min(1, clickPosition / trackWidth));
|
||||
const rawValue = min + newPercentage * (max - min);
|
||||
|
||||
// Special handling to ensure max value is always reachable
|
||||
if (rawValue >= max - step / 2) {
|
||||
onChange(max);
|
||||
return;
|
||||
}
|
||||
|
||||
// Normal step calculation
|
||||
const steppedValue = Math.round((rawValue - min) / step) * step + min;
|
||||
const clampedValue = Math.max(min, Math.min(max, steppedValue));
|
||||
|
||||
onChange(clampedValue);
|
||||
};
|
||||
|
||||
// Helper function to handle thumb dragging that respects steps and max
|
||||
const handleThumbDrag = (moveEvent: MouseEvent, track: Element) => {
|
||||
if (!track) return;
|
||||
|
||||
const rect = (track as HTMLElement).getBoundingClientRect();
|
||||
const movePosition = moveEvent.clientX - rect.left;
|
||||
const trackWidth = rect.width;
|
||||
|
||||
// Calculate new value
|
||||
const newPercentage = Math.max(0, Math.min(1, movePosition / trackWidth));
|
||||
const rawValue = min + newPercentage * (max - min);
|
||||
|
||||
// Special handling to ensure max value is always reachable
|
||||
if (rawValue >= max - step / 2) {
|
||||
onChange(max);
|
||||
return;
|
||||
}
|
||||
|
||||
// Normal step calculation
|
||||
const steppedValue = Math.round((rawValue - min) / step) * step + min;
|
||||
const clampedValue = Math.max(min, Math.min(max, steppedValue));
|
||||
|
||||
onChange(clampedValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`inline-flex items-center flex-shrink-0 ${className}`}>
|
||||
{/* Outer container with padding to account for thumb overhang */}
|
||||
<div className={`relative flex-shrink-0 ${disabled ? 'opacity-25' : ''}`}
|
||||
style={{
|
||||
width,
|
||||
// Add horizontal padding equal to half the thumb width
|
||||
// paddingLeft: thumbSizePx / 2,
|
||||
// paddingRight: thumbSizePx / 2
|
||||
}}>
|
||||
{/* Track container with adjusted width */}
|
||||
<div className="relative w-full">
|
||||
{/* Invisible wider clickable area that sits above the track */}
|
||||
<div
|
||||
className="absolute w-full cursor-pointer"
|
||||
style={{
|
||||
height: '16px',
|
||||
top: '50%',
|
||||
transform: 'translateY(-50%)',
|
||||
zIndex: 1
|
||||
}}
|
||||
onClick={handleTrackClick}
|
||||
/>
|
||||
|
||||
{/* Track */}
|
||||
<div
|
||||
className={`relative ${size === 'xxs' ? 'h-0.5' :
|
||||
size === 'xs' ? 'h-1' :
|
||||
size === 'sm' ? 'h-1.5' :
|
||||
size === 'sm+' ? 'h-2' : 'h-2.5'
|
||||
} bg-gray-200 dark:bg-gray-700 rounded-full cursor-pointer`}
|
||||
onClick={handleTrackClick}
|
||||
>
|
||||
{/* Filled part of track */}
|
||||
<div
|
||||
className={`absolute left-0 ${size === 'xxs' ? 'h-0.5' :
|
||||
size === 'xs' ? 'h-1' :
|
||||
size === 'sm' ? 'h-1.5' :
|
||||
size === 'sm+' ? 'h-2' : 'h-2.5'
|
||||
} bg-gray-900 dark:bg-white rounded-full`}
|
||||
style={{ width: `${percentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Thumb with sizes matching VoidSwitch */}
|
||||
<div
|
||||
className={`absolute top-1/2 transform -translate-x-1/2 -translate-y-1/2
|
||||
${size === 'xxs' ? 'h-2 w-2' :
|
||||
size === 'xs' ? 'h-2.5 w-2.5' :
|
||||
size === 'sm' ? 'h-3 w-3' :
|
||||
size === 'sm+' ? 'h-3.5 w-3.5' : 'h-4 w-4'
|
||||
}
|
||||
bg-white dark:bg-gray-900 rounded-full shadow-md ${disabled ? 'cursor-not-allowed' : 'cursor-grab active:cursor-grabbing'}`}
|
||||
style={{ left: `${percentage}%`, zIndex: 2 }} // Ensure thumb is above the invisible clickable area
|
||||
onMouseDown={(e) => {
|
||||
if (disabled) return;
|
||||
|
||||
const track = e.currentTarget.previousElementSibling;
|
||||
|
||||
const handleMouseMove = (moveEvent: MouseEvent) => {
|
||||
handleThumbDrag(moveEvent, track as Element);
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
document.body.style.cursor = '';
|
||||
document.body.style.userSelect = '';
|
||||
};
|
||||
|
||||
document.body.style.userSelect = 'none';
|
||||
document.body.style.cursor = 'grabbing';
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
|
||||
e.preventDefault();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
export const VoidSwitch = ({
|
||||
value,
|
||||
onChange,
|
||||
size = 'md',
|
||||
label,
|
||||
disabled = false,
|
||||
}: {
|
||||
value: boolean;
|
||||
onChange: (value: boolean) => void;
|
||||
label?: string;
|
||||
disabled?: boolean;
|
||||
size?: 'xs' | 'sm' | 'sm+' | 'md';
|
||||
size?: 'xxs' | 'xs' | 'sm' | 'sm+' | 'md';
|
||||
}) => {
|
||||
return (
|
||||
<label className="inline-flex items-center cursor-pointer">
|
||||
<label className="inline-flex items-center">
|
||||
<div
|
||||
onClick={() => !disabled && onChange(!value)}
|
||||
className={`
|
||||
cursor-pointer
|
||||
relative inline-flex items-center rounded-full transition-colors duration-200 ease-in-out
|
||||
${value ? 'bg-gray-900 dark:bg-white' : 'bg-gray-200 dark:bg-gray-700'}
|
||||
${disabled ? 'opacity-25' : ''}
|
||||
${size === 'xxs' ? 'h-3 w-5' : ''}
|
||||
${size === 'xs' ? 'h-4 w-7' : ''}
|
||||
${size === 'sm' ? 'h-5 w-9' : ''}
|
||||
${size === 'sm+' ? 'h-5 w-10' : ''}
|
||||
|
|
@ -244,10 +405,12 @@ export const VoidSwitch = ({
|
|||
<span
|
||||
className={`
|
||||
inline-block transform rounded-full bg-white dark:bg-gray-900 shadow transition-transform duration-200 ease-in-out
|
||||
${size === 'xxs' ? 'h-2 w-2' : ''}
|
||||
${size === 'xs' ? 'h-2.5 w-2.5' : ''}
|
||||
${size === 'sm' ? 'h-3 w-3' : ''}
|
||||
${size === 'sm+' ? 'h-3.5 w-3.5' : ''}
|
||||
${size === 'md' ? 'h-4 w-4' : ''}
|
||||
${size === 'xxs' ? (value ? 'translate-x-2.5' : 'translate-x-0.5') : ''}
|
||||
${size === 'xs' ? (value ? 'translate-x-3.5' : 'translate-x-0.5') : ''}
|
||||
${size === 'sm' ? (value ? 'translate-x-5' : 'translate-x-1') : ''}
|
||||
${size === 'sm+' ? (value ? 'translate-x-6' : 'translate-x-1') : ''}
|
||||
|
|
@ -255,14 +418,6 @@ export const VoidSwitch = ({
|
|||
`}
|
||||
/>
|
||||
</div>
|
||||
{label && (
|
||||
<span className={`
|
||||
ml-3 font-medium text-gray-900 dark:text-gray-100
|
||||
${size === 'xs' ? 'text-xs' : 'text-sm'}
|
||||
`}>
|
||||
{label}
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import React, { useState, useEffect, useCallback } from 'react'
|
||||
import { ThreadStreamState,IChatThreadService, ThreadsState } from '../../../../common/chatThreadService.js'
|
||||
import { RefreshableProviderName, SettingsOfProvider } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'
|
||||
import { IDisposable } from '../../../../../../../base/common/lifecycle.js'
|
||||
import { VoidSidebarState } from '../../../sidebarStateService.js'
|
||||
|
|
@ -25,7 +24,8 @@ import { IThemeService } from '../../../../../../../platform/theme/common/themeS
|
|||
import { ILLMMessageService } from '../../../../../../../workbench/contrib/void/common/llmMessageService.js';
|
||||
import { IRefreshModelService } from '../../../../../../../workbench/contrib/void/common/refreshModelService.js';
|
||||
import { IVoidSettingsService } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js';
|
||||
import { IEditCodeService, URIStreamState } from '../../../editCodeService.js';
|
||||
import { IEditCodeService, URIStreamState } from '../../../editCodeServiceInterface.js'
|
||||
|
||||
import { IVoidUriStateService } from '../../../voidUriStateService.js';
|
||||
import { IQuickEditStateService } from '../../../quickEditStateService.js';
|
||||
import { ISidebarStateService } from '../../../sidebarStateService.js';
|
||||
|
|
@ -44,6 +44,7 @@ import { IConfigurationService } from '../../../../../../../platform/configurati
|
|||
import { IPathService } from '../../../../../../../workbench/services/path/common/pathService.js'
|
||||
import { IMetricsService } from '../../../../../../../workbench/contrib/void/common/metricsService.js'
|
||||
import { URI } from '../../../../../../../base/common/uri.js'
|
||||
import { IChatThreadService, ThreadsState, ThreadStreamState } from '../../../chatThreadService.js'
|
||||
|
||||
|
||||
|
||||
|
|
@ -170,7 +171,7 @@ export const _registerServices = (accessor: ServicesAccessor) => {
|
|||
|
||||
colorThemeState = themeService.getColorTheme().type
|
||||
disposables.push(
|
||||
themeService.onDidColorThemeChange(theme => {
|
||||
themeService.onDidColorThemeChange(({ theme }) => {
|
||||
colorThemeState = theme.type
|
||||
colorThemeStateListeners.forEach(l => l(colorThemeState))
|
||||
})
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { _VoidSelectBox, VoidCustomDropdownBox } from '../util/inputs.js'
|
|||
import { SelectBox } from '../../../../../../../base/browser/ui/selectBox/selectBox.js'
|
||||
import { IconWarning } from '../sidebar-tsx/SidebarChat.js'
|
||||
import { VOID_OPEN_SETTINGS_ACTION_ID, VOID_TOGGLE_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js'
|
||||
import { ModelOption } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js'
|
||||
import { modelFilterOfFeatureName, ModelOption } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js'
|
||||
import { WarningBox } from './WarningBox.js'
|
||||
|
||||
const optionsEqual = (m1: ModelOption[], m2: ModelOption[]) => {
|
||||
|
|
@ -38,9 +38,9 @@ const ModelSelectBox = ({ options, featureName }: { options: ModelOption[], feat
|
|||
onChangeOption={onChangeOption}
|
||||
getOptionDisplayName={(option) => option.selection.modelName}
|
||||
getOptionDropdownName={(option) => option.selection.modelName}
|
||||
getOptionDropdownDetail={(option) => option.selection.providerName }
|
||||
getOptionDropdownDetail={(option) => option.selection.providerName}
|
||||
getOptionsEqual={(a, b) => optionsEqual([a], [b])}
|
||||
className='text-xs text-void-fg-3 px-1'
|
||||
className='text-xs text-void-fg-3'
|
||||
matchInputWidth={false}
|
||||
/>
|
||||
}
|
||||
|
|
@ -82,14 +82,21 @@ const MemoizedModelDropdown = ({ featureName }: { featureName: FeatureName }) =>
|
|||
const oldOptionsRef = useRef<ModelOption[]>([])
|
||||
const [memoizedOptions, setMemoizedOptions] = useState(oldOptionsRef.current)
|
||||
|
||||
const { filter, emptyMessage } = modelFilterOfFeatureName[featureName]
|
||||
|
||||
useEffect(() => {
|
||||
const oldOptions = oldOptionsRef.current
|
||||
const newOptions = settingsState._modelOptions
|
||||
const newOptions = settingsState._modelOptions.filter((o) => filter(o.selection))
|
||||
|
||||
if (!optionsEqual(oldOptions, newOptions)) {
|
||||
setMemoizedOptions(newOptions)
|
||||
}
|
||||
oldOptionsRef.current = newOptions
|
||||
}, [settingsState._modelOptions])
|
||||
}, [settingsState._modelOptions, filter])
|
||||
|
||||
if (memoizedOptions.length === 0) { // Pretty sure this will never be reached unless filter is enabled
|
||||
return <WarningBox text={emptyMessage || 'No models available'} />
|
||||
}
|
||||
|
||||
return <ModelSelectBox featureName={featureName} options={memoizedOptions} />
|
||||
|
||||
|
|
@ -103,13 +110,17 @@ export const ModelDropdown = ({ featureName }: { featureName: FeatureName }) =>
|
|||
|
||||
const openSettings = () => { commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID); };
|
||||
|
||||
|
||||
const { emptyMessage } = modelFilterOfFeatureName[featureName]
|
||||
|
||||
const isDisabled = isFeatureNameDisabled(featureName, settingsState)
|
||||
if (isDisabled)
|
||||
return <WarningBox onClick={openSettings} text={
|
||||
isDisabled === 'needToEnableModel' ? 'Enable a model'
|
||||
: isDisabled === 'addModel' ? 'Add a model'
|
||||
: (isDisabled === 'addProvider' || isDisabled === 'notFilledIn' || isDisabled === 'providerNotAutoDetected') ? 'Provider required'
|
||||
: 'Provider required'
|
||||
emptyMessage ? emptyMessage :
|
||||
isDisabled === 'needToEnableModel' ? 'Enable a model'
|
||||
: isDisabled === 'addModel' ? 'Add a model'
|
||||
: (isDisabled === 'addProvider' || isDisabled === 'notFilledIn' || isDisabled === 'providerNotAutoDetected') ? 'Provider required'
|
||||
: 'Provider required'
|
||||
} />
|
||||
|
||||
return <MemoizedModelDropdown featureName={featureName} />
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import { env } from '../../../../../../../base/common/process.js'
|
|||
import { ModelDropdown } from './ModelDropdown.js'
|
||||
import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js'
|
||||
import { WarningBox } from './WarningBox.js'
|
||||
import { os } from '../../../helpers/systemInfo.js'
|
||||
import { os } from '../../../../common/helpers/systemInfo.js'
|
||||
|
||||
const SubtleButton = ({ onClick, text, icon, disabled }: { onClick: () => void, text: string, icon: React.ReactNode, disabled: boolean }) => {
|
||||
|
||||
|
|
@ -230,9 +230,7 @@ export const ModelDump = () => {
|
|||
|
||||
<VoidSwitch
|
||||
value={disabled ? false : !isHidden}
|
||||
onChange={() => {
|
||||
settingsStateService.toggleModelHidden(providerName, modelName)
|
||||
}}
|
||||
onChange={() => { settingsStateService.toggleModelHidden(providerName, modelName) }}
|
||||
disabled={disabled}
|
||||
size='sm'
|
||||
/>
|
||||
|
|
@ -293,7 +291,7 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider
|
|||
isPasswordField={isPasswordField}
|
||||
/>
|
||||
{subTextMd === undefined ? null : <div className='py-1 px-3 opacity-50 text-sm'>
|
||||
<ChatMarkdownRender noSpace string={subTextMd} />
|
||||
<ChatMarkdownRender string={subTextMd} />
|
||||
</div>}
|
||||
|
||||
</div>
|
||||
|
|
@ -396,6 +394,11 @@ export const AIInstructionsBox = () => {
|
|||
}
|
||||
|
||||
export const FeaturesTab = () => {
|
||||
const voidSettingsState = useSettingsState()
|
||||
const accessor = useAccessor()
|
||||
const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
|
||||
|
||||
return <>
|
||||
<h2 className={`text-3xl mb-2`}>Models</h2>
|
||||
<ErrorBoundary>
|
||||
|
|
@ -412,12 +415,12 @@ export const FeaturesTab = () => {
|
|||
{/* <h3 className={`opacity-50 mb-2`}>{`Instructions:`}</h3> */}
|
||||
{/* <h3 className={`mb-2`}>{`Void can access any model that you host locally. We automatically detect your local models by default.`}</h3> */}
|
||||
<h3 className={`text-void-fg-3 mb-2`}>{`Void can access any model that you host locally. We automatically detect your local models by default.`}</h3>
|
||||
<div className='pl-4 opacity-50'>
|
||||
<span className={`text-sm mb-2`}><ChatMarkdownRender noSpace string={`1. Download [Ollama](https://ollama.com/download).`} /></span>
|
||||
<span className={`text-sm mb-2`}><ChatMarkdownRender noSpace string={`2. Open your terminal.`} /></span>
|
||||
<span className={`text-sm mb-2 select-text`}><ChatMarkdownRender noSpace string={`3. Run \`ollama run llama3.1:8b\`. This installs Meta's llama3.1 model which is best for chat and inline edits. Requires 5GB of memory.`} /></span>
|
||||
<span className={`text-sm mb-2 select-text`}><ChatMarkdownRender noSpace string={`4. Run \`ollama run qwen2.5-coder:1.5b\`. This installs a faster autocomplete model. Requires 1GB of memory.`} /></span>
|
||||
<span className={`text-sm mb-2`}><ChatMarkdownRender noSpace string={`Void automatically detects locally running models and enables them.`} /></span>
|
||||
<div className='pl-4 prose-ol:list-decimal opacity-80'>
|
||||
<span className={`text-sm mb-2`}><ChatMarkdownRender string={`1. Download [Ollama](https://ollama.com/download).`} /></span>
|
||||
<span className={`text-sm mb-2`}><ChatMarkdownRender string={`2. Open your terminal.`} /></span>
|
||||
<span className={`text-sm mb-2 select-text`}><ChatMarkdownRender string={`3. Run \`ollama run llama3.1:8b\`. This installs Meta's llama3.1 model which is best for chat and inline edits. Requires 5GB of memory.`} /></span>
|
||||
<span className={`text-sm mb-2 select-text`}><ChatMarkdownRender string={`4. Run \`ollama run qwen2.5-coder:1.5b\`. This installs a faster autocomplete model. Requires 1GB of memory.`} /></span>
|
||||
<span className={`text-sm mb-2`}><ChatMarkdownRender string={`Void automatically detects locally running models and enables them.`} /></span>
|
||||
{/* TODO we should create UI for downloading models without user going into terminal */}
|
||||
</div>
|
||||
|
||||
|
|
@ -434,17 +437,33 @@ export const FeaturesTab = () => {
|
|||
|
||||
|
||||
|
||||
<h2 className={`text-3xl mb-2 mt-12`}>Feature Options</h2>
|
||||
<h2 className={`text-3xl mt-12`}>Feature Options</h2>
|
||||
<ErrorBoundary>
|
||||
{featureNames.map(featureName =>
|
||||
(['Ctrl+L', 'Ctrl+K'] as FeatureName[]).includes(featureName) ? null :
|
||||
<div key={featureName}
|
||||
className='mb-2'
|
||||
>
|
||||
<h4 className={`text-void-fg-3`}>{displayInfoOfFeatureName(featureName)}</h4>
|
||||
<ModelDropdown featureName={featureName} />
|
||||
<div className='flex gap-x-4 items-start justify-around mt-4 mb-16'>
|
||||
<div className='w-full'>
|
||||
<h4 className={`text-base`}>{displayInfoOfFeatureName('Autocomplete')}</h4>
|
||||
<div className='text-sm italic text-void-fg-3 my-1'>Experimental. Only works with models that support FIM.</div>
|
||||
<div className='flex items-center gap-x-2'>
|
||||
<VoidSwitch
|
||||
size='xs'
|
||||
value={voidSettingsState.globalSettings.enableAutocomplete}
|
||||
onChange={(newVal) => voidSettingsService.setGlobalSetting('enableAutocomplete', newVal)}
|
||||
/>
|
||||
<span className='text-void-fg-3 text-xs pointer-events-none'>{voidSettingsState.globalSettings.enableAutocomplete ? 'Enabled' : 'Disabled'}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={!voidSettingsState.globalSettings.enableAutocomplete ? 'hidden' : ''}>
|
||||
<ModelDropdown featureName={'Autocomplete'} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='w-full'>
|
||||
<h4 className={`text-base`}>{displayInfoOfFeatureName('Apply')}</h4>
|
||||
<div className='text-sm italic text-void-fg-3 my-1'>We recommend the smartest model you{`'`}ve got, like Claude 3.7 or GPT 4o.</div>
|
||||
<ModelDropdown featureName={'Apply'} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ErrorBoundary>
|
||||
|
||||
</>
|
||||
|
|
@ -649,7 +668,7 @@ export const Settings = () => {
|
|||
|
||||
|
||||
{/* content */}
|
||||
<div className='w-full min-w-[600px] overflow-auto'>
|
||||
<div className='w-full min-w-[550px]'>
|
||||
|
||||
<div className={`${tab !== 'models' ? 'hidden' : ''}`}>
|
||||
<FeaturesTab />
|
||||
|
|
|
|||
|
|
@ -9,6 +9,29 @@ module.exports = {
|
|||
content: ['./src2/**/*.{jsx,tsx}'], // uses these files to decide how to transform the css file
|
||||
theme: {
|
||||
extend: {
|
||||
typography: {
|
||||
DEFAULT: {
|
||||
css: {
|
||||
'--tw-prose-body': 'var(--void-fg-1)',
|
||||
'--tw-prose-headings': 'var(--void-fg-1)',
|
||||
'--tw-prose-lead': 'var(--void-fg-2)',
|
||||
'--tw-prose-links': 'var(--void-link-color)',
|
||||
'--tw-prose-bold': 'var(--void-fg-1)',
|
||||
'--tw-prose-counters': 'var(--void-fg-3)',
|
||||
'--tw-prose-bullets': 'var(--void-fg-3)',
|
||||
'--tw-prose-hr': 'var(--void-border-4)',
|
||||
'--tw-prose-quotes': 'var(--void-fg-1)',
|
||||
'--tw-prose-quote-borders': 'var(--void-border-2)',
|
||||
'--tw-prose-captions': 'var(--void-fg-3)',
|
||||
'--tw-prose-code': 'var(--void-fg-0)',
|
||||
'--tw-prose-pre-code': 'var(--void-fg-0)',
|
||||
'--tw-prose-pre-bg': 'var(--void-bg-1)',
|
||||
'--tw-prose-th-borders': 'var(--void-border-4)',
|
||||
'--tw-prose-td-borders': 'var(--void-border-4)',
|
||||
},
|
||||
},
|
||||
|
||||
},
|
||||
fontSize: {
|
||||
xs: '10px',
|
||||
sm: '11px',
|
||||
|
|
@ -27,146 +50,150 @@ module.exports = {
|
|||
// common colors to use, ordered light to dark
|
||||
|
||||
colors: {
|
||||
"void-bg-1": "var(--vscode-input-background)",
|
||||
"void-bg-1-alt": "var(--vscode-badge-background)",
|
||||
"void-bg-2": "var(--vscode-sideBar-background)",
|
||||
"void-bg-2-alt": "color-mix(in srgb, var(--vscode-sideBar-background) 30%, var(--vscode-editor-background) 70%)",
|
||||
"void-bg-3": "var(--vscode-editor-background)",
|
||||
'void-bg-1': 'var(--void-bg-1)',
|
||||
'void-bg-1-alt': 'var(--void-bg-1-alt)',
|
||||
'void-bg-2': 'var(--void-bg-2)',
|
||||
'void-bg-2-alt': 'var(--void-bg-2-alt)',
|
||||
'void-bg-3': 'var(--void-bg-3)',
|
||||
|
||||
|
||||
"void-fg-1": "var(--vscode-editor-foreground)",
|
||||
"void-fg-2": "var(--vscode-input-foreground)",
|
||||
"void-fg-3": "var(--vscode-input-placeholderForeground)",
|
||||
// "void-fg-4": "var(--vscode-tab-inactiveForeground)",
|
||||
"void-fg-4": "var(--vscode-list-deemphasizedForeground)",
|
||||
'void-fg-0': 'var(--void-fg-0)',
|
||||
'void-fg-1': 'var(--void-fg-1)',
|
||||
'void-fg-2': 'var(--void-fg-2)',
|
||||
'void-fg-3': 'var(--void-fg-3)',
|
||||
// 'void-fg-4': 'var(--vscode-tab-inactiveForeground)',
|
||||
'void-fg-4': 'var(--void-fg-4)',
|
||||
|
||||
'void-warning': 'var(--void-warning)',
|
||||
|
||||
"void-warning": "var(--vscode-charts-yellow)",
|
||||
|
||||
"void-border-1": "var(--vscode-commandCenter-activeBorder)",
|
||||
"void-border-2": "var(--vscode-commandCenter-border)",
|
||||
"void-border-3": "var(--vscode-commandCenter-inactiveBorder)",
|
||||
"void-border-4": "var(--vscode-editorGroup-border)",
|
||||
'void-border-1': 'var(--void-border-1)',
|
||||
'void-border-2': 'var(--void-border-2)',
|
||||
'void-border-3': 'var(--void-border-3)',
|
||||
'void-border-4': 'var(--void-border-4)',
|
||||
|
||||
'void-ring-color': 'var(--void-ring-color)',
|
||||
'void-link-color': 'var(--void-link-color)',
|
||||
|
||||
vscode: {
|
||||
// see: https://code.visualstudio.com/api/extension-guides/webview#theming-webview-content
|
||||
|
||||
// base colors
|
||||
"fg": "var(--vscode-foreground)",
|
||||
"focus-border": "var(--vscode-focusBorder)",
|
||||
"disabled-fg": "var(--vscode-disabledForeground)",
|
||||
"widget-border": "var(--vscode-widget-border)",
|
||||
"widget-shadow": "var(--vscode-widget-shadow)",
|
||||
"selection-bg": "var(--vscode-selection-background)",
|
||||
"description-fg": "var(--vscode-descriptionForeground)",
|
||||
"error-fg": "var(--vscode-errorForeground)",
|
||||
"icon-fg": "var(--vscode-icon-foreground)",
|
||||
"sash-hover-border": "var(--vscode-sash-hoverBorder)",
|
||||
'fg': 'var(--vscode-foreground)',
|
||||
'focus-border': 'var(--vscode-focusBorder)',
|
||||
'disabled-fg': 'var(--vscode-disabledForeground)',
|
||||
'widget-border': 'var(--vscode-widget-border)',
|
||||
'widget-shadow': 'var(--vscode-widget-shadow)',
|
||||
'selection-bg': 'var(--vscode-selection-background)',
|
||||
'description-fg': 'var(--vscode-descriptionForeground)',
|
||||
'error-fg': 'var(--vscode-errorForeground)',
|
||||
'icon-fg': 'var(--vscode-icon-foreground)',
|
||||
'sash-hover-border': 'var(--vscode-sash-hoverBorder)',
|
||||
|
||||
// text colors
|
||||
"text-blockquote-bg": "var(--vscode-textBlockQuote-background)",
|
||||
"text-blockquote-border": "var(--vscode-textBlockQuote-border)",
|
||||
"text-codeblock-bg": "var(--vscode-textCodeBlock-background)",
|
||||
"text-link-active-fg": "var(--vscode-textLink-activeForeground)",
|
||||
"text-link-fg": "var(--vscode-textLink-foreground)",
|
||||
"text-preformat-fg": "var(--vscode-textPreformat-foreground)",
|
||||
"text-preformat-bg": "var(--vscode-textPreformat-background)",
|
||||
"text-separator-fg": "var(--vscode-textSeparator-foreground)",
|
||||
'text-blockquote-bg': 'var(--vscode-textBlockQuote-background)',
|
||||
'text-blockquote-border': 'var(--vscode-textBlockQuote-border)',
|
||||
'text-codeblock-bg': 'var(--vscode-textCodeBlock-background)',
|
||||
'text-link-active-fg': 'var(--vscode-textLink-activeForeground)',
|
||||
'text-link-fg': 'var(--vscode-textLink-foreground)',
|
||||
'text-preformat-fg': 'var(--vscode-textPreformat-foreground)',
|
||||
'text-preformat-bg': 'var(--vscode-textPreformat-background)',
|
||||
'text-separator-fg': 'var(--vscode-textSeparator-foreground)',
|
||||
|
||||
// input colors
|
||||
"input-bg": "var(--vscode-input-background)",
|
||||
"input-border": "var(--vscode-input-border)",
|
||||
"input-fg": "var(--vscode-input-foreground)",
|
||||
"input-placeholder-fg": "var(--vscode-input-placeholderForeground)",
|
||||
"input-active-bg": "var(--vscode-input-activeBackground)",
|
||||
"input-option-active-border": "var(--vscode-inputOption-activeBorder)",
|
||||
"input-option-active-fg": "var(--vscode-inputOption-activeForeground)",
|
||||
"input-option-hover-bg": "var(--vscode-inputOption-hoverBackground)",
|
||||
"input-validation-error-bg": "var(--vscode-inputValidation-errorBackground)",
|
||||
"input-validation-error-fg": "var(--vscode-inputValidation-errorForeground)",
|
||||
"input-validation-error-border": "var(--vscode-inputValidation-errorBorder)",
|
||||
"input-validation-info-bg": "var(--vscode-inputValidation-infoBackground)",
|
||||
"input-validation-info-fg": "var(--vscode-inputValidation-infoForeground)",
|
||||
"input-validation-info-border": "var(--vscode-inputValidation-infoBorder)",
|
||||
"input-validation-warning-bg": "var(--vscode-inputValidation-warningBackground)",
|
||||
"input-validation-warning-fg": "var(--vscode-inputValidation-warningForeground)",
|
||||
"input-validation-warning-border": "var(--vscode-inputValidation-warningBorder)",
|
||||
'input-bg': 'var(--vscode-input-background)',
|
||||
'input-border': 'var(--vscode-input-border)',
|
||||
'input-fg': 'var(--vscode-input-foreground)',
|
||||
'input-placeholder-fg': 'var(--vscode-input-placeholderForeground)',
|
||||
'input-active-bg': 'var(--vscode-input-activeBackground)',
|
||||
'input-option-active-border': 'var(--vscode-inputOption-activeBorder)',
|
||||
'input-option-active-fg': 'var(--vscode-inputOption-activeForeground)',
|
||||
'input-option-hover-bg': 'var(--vscode-inputOption-hoverBackground)',
|
||||
'input-validation-error-bg': 'var(--vscode-inputValidation-errorBackground)',
|
||||
'input-validation-error-fg': 'var(--vscode-inputValidation-errorForeground)',
|
||||
'input-validation-error-border': 'var(--vscode-inputValidation-errorBorder)',
|
||||
'input-validation-info-bg': 'var(--vscode-inputValidation-infoBackground)',
|
||||
'input-validation-info-fg': 'var(--vscode-inputValidation-infoForeground)',
|
||||
'input-validation-info-border': 'var(--vscode-inputValidation-infoBorder)',
|
||||
'input-validation-warning-bg': 'var(--vscode-inputValidation-warningBackground)',
|
||||
'input-validation-warning-fg': 'var(--vscode-inputValidation-warningForeground)',
|
||||
'input-validation-warning-border': 'var(--vscode-inputValidation-warningBorder)',
|
||||
|
||||
// command center colors (the top bar)
|
||||
"commandcenter-fg": "var(--vscode-commandCenter-foreground)",
|
||||
"commandcenter-active-fg": "var(--vscode-commandCenter-activeForeground)",
|
||||
"commandcenter-bg": "var(--vscode-commandCenter-background)",
|
||||
"commandcenter-active-bg": "var(--vscode-commandCenter-activeBackground)",
|
||||
"commandcenter-border": "var(--vscode-commandCenter-border)",
|
||||
"commandcenter-inactive-fg": "var(--vscode-commandCenter-inactiveForeground)",
|
||||
"commandcenter-inactive-border": "var(--vscode-commandCenter-inactiveBorder)",
|
||||
"commandcenter-active-border": "var(--vscode-commandCenter-activeBorder)",
|
||||
"commandcenter-debugging-bg": "var(--vscode-commandCenter-debuggingBackground)",
|
||||
'commandcenter-fg': 'var(--vscode-commandCenter-foreground)',
|
||||
'commandcenter-active-fg': 'var(--vscode-commandCenter-activeForeground)',
|
||||
'commandcenter-bg': 'var(--vscode-commandCenter-background)',
|
||||
'commandcenter-active-bg': 'var(--vscode-commandCenter-activeBackground)',
|
||||
'commandcenter-border': 'var(--vscode-commandCenter-border)',
|
||||
'commandcenter-inactive-fg': 'var(--vscode-commandCenter-inactiveForeground)',
|
||||
'commandcenter-inactive-border': 'var(--vscode-commandCenter-inactiveBorder)',
|
||||
'commandcenter-active-border': 'var(--vscode-commandCenter-activeBorder)',
|
||||
'commandcenter-debugging-bg': 'var(--vscode-commandCenter-debuggingBackground)',
|
||||
|
||||
// badge colors
|
||||
"badge-fg": "var(--vscode-badge-foreground)",
|
||||
"badge-bg": "var(--vscode-badge-background)",
|
||||
'badge-fg': 'var(--vscode-badge-foreground)',
|
||||
'badge-bg': 'var(--vscode-badge-background)',
|
||||
|
||||
// button colors
|
||||
"button-bg": "var(--vscode-button-background)",
|
||||
"button-fg": "var(--vscode-button-foreground)",
|
||||
"button-border": "var(--vscode-button-border)",
|
||||
"button-separator": "var(--vscode-button-separator)",
|
||||
"button-hover-bg": "var(--vscode-button-hoverBackground)",
|
||||
"button-secondary-fg": "var(--vscode-button-secondaryForeground)",
|
||||
"button-secondary-bg": "var(--vscode-button-secondaryBackground)",
|
||||
"button-secondary-hover-bg": "var(--vscode-button-secondaryHoverBackground)",
|
||||
'button-bg': 'var(--vscode-button-background)',
|
||||
'button-fg': 'var(--vscode-button-foreground)',
|
||||
'button-border': 'var(--vscode-button-border)',
|
||||
'button-separator': 'var(--vscode-button-separator)',
|
||||
'button-hover-bg': 'var(--vscode-button-hoverBackground)',
|
||||
'button-secondary-fg': 'var(--vscode-button-secondaryForeground)',
|
||||
'button-secondary-bg': 'var(--vscode-button-secondaryBackground)',
|
||||
'button-secondary-hover-bg': 'var(--vscode-button-secondaryHoverBackground)',
|
||||
|
||||
// checkbox colors
|
||||
"checkbox-bg": "var(--vscode-checkbox-background)",
|
||||
"checkbox-fg": "var(--vscode-checkbox-foreground)",
|
||||
"checkbox-border": "var(--vscode-checkbox-border)",
|
||||
"checkbox-select-bg": "var(--vscode-checkbox-selectBackground)",
|
||||
'checkbox-bg': 'var(--vscode-checkbox-background)',
|
||||
'checkbox-fg': 'var(--vscode-checkbox-foreground)',
|
||||
'checkbox-border': 'var(--vscode-checkbox-border)',
|
||||
'checkbox-select-bg': 'var(--vscode-checkbox-selectBackground)',
|
||||
|
||||
// sidebar colors
|
||||
"sidebar-bg": "var(--vscode-sideBar-background)",
|
||||
"sidebar-fg": "var(--vscode-sideBar-foreground)",
|
||||
"sidebar-border": "var(--vscode-sideBar-border)",
|
||||
"sidebar-drop-bg": "var(--vscode-sideBar-dropBackground)",
|
||||
"sidebar-title-fg": "var(--vscode-sideBarTitle-foreground)",
|
||||
"sidebar-header-bg": "var(--vscode-sideBarSectionHeader-background)",
|
||||
"sidebar-header-fg": "var(--vscode-sideBarSectionHeader-foreground)",
|
||||
"sidebar-header-border": "var(--vscode-sideBarSectionHeader-border)",
|
||||
"sidebar-activitybartop-border": "var(--vscode-sideBarActivityBarTop-border)",
|
||||
"sidebar-title-bg": "var(--vscode-sideBarTitle-background)",
|
||||
"sidebar-title-border": "var(--vscode-sideBarTitle-border)",
|
||||
"sidebar-stickyscroll-bg": "var(--vscode-sideBarStickyScroll-background)",
|
||||
"sidebar-stickyscroll-border": "var(--vscode-sideBarStickyScroll-border)",
|
||||
"sidebar-stickyscroll-shadow": "var(--vscode-sideBarStickyScroll-shadow)",
|
||||
'sidebar-bg': 'var(--vscode-sideBar-background)',
|
||||
'sidebar-fg': 'var(--vscode-sideBar-foreground)',
|
||||
'sidebar-border': 'var(--vscode-sideBar-border)',
|
||||
'sidebar-drop-bg': 'var(--vscode-sideBar-dropBackground)',
|
||||
'sidebar-title-fg': 'var(--vscode-sideBarTitle-foreground)',
|
||||
'sidebar-header-bg': 'var(--vscode-sideBarSectionHeader-background)',
|
||||
'sidebar-header-fg': 'var(--vscode-sideBarSectionHeader-foreground)',
|
||||
'sidebar-header-border': 'var(--vscode-sideBarSectionHeader-border)',
|
||||
'sidebar-activitybartop-border': 'var(--vscode-sideBarActivityBarTop-border)',
|
||||
'sidebar-title-bg': 'var(--vscode-sideBarTitle-background)',
|
||||
'sidebar-title-border': 'var(--vscode-sideBarTitle-border)',
|
||||
'sidebar-stickyscroll-bg': 'var(--vscode-sideBarStickyScroll-background)',
|
||||
'sidebar-stickyscroll-border': 'var(--vscode-sideBarStickyScroll-border)',
|
||||
'sidebar-stickyscroll-shadow': 'var(--vscode-sideBarStickyScroll-shadow)',
|
||||
|
||||
// other colors (these are partially complete)
|
||||
|
||||
// text formatting
|
||||
"text-preformat-bg": "var(--vscode-textPreformat-background)",
|
||||
"text-preformat-fg": "var(--vscode-textPreformat-foreground)",
|
||||
'text-preformat-bg': 'var(--vscode-textPreformat-background)',
|
||||
'text-preformat-fg': 'var(--vscode-textPreformat-foreground)',
|
||||
|
||||
// editor colors
|
||||
"editor-bg": "var(--vscode-editor-background)",
|
||||
"editor-fg": "var(--vscode-editor-foreground)",
|
||||
'editor-bg': 'var(--vscode-editor-background)',
|
||||
'editor-fg': 'var(--vscode-editor-foreground)',
|
||||
|
||||
|
||||
|
||||
// other
|
||||
"editorwidget-bg": "var(--vscode-editorWidget-background)",
|
||||
"toolbar-hover-bg": "var(--vscode-toolbar-hoverBackground)",
|
||||
"toolbar-foreground": "var(--vscode-editorActionList-foreground)",
|
||||
'editorwidget-bg': 'var(--vscode-editorWidget-background)',
|
||||
'toolbar-hover-bg': 'var(--vscode-toolbar-hoverBackground)',
|
||||
'toolbar-foreground': 'var(--vscode-editorActionList-foreground)',
|
||||
|
||||
"editorwidget-fg": "var(--vscode-editorWidget-foreground)",
|
||||
"editorwidget-border": "var(--vscode-editorWidget-border)",
|
||||
'editorwidget-fg': 'var(--vscode-editorWidget-foreground)',
|
||||
'editorwidget-border': 'var(--vscode-editorWidget-border)',
|
||||
|
||||
"charts-orange": "var(--vscode-charts-orange)",
|
||||
"charts-yellow": "var(--vscode-charts-yellow)",
|
||||
'charts-orange': 'var(--vscode-charts-orange)',
|
||||
'charts-yellow': 'var(--vscode-charts-yellow)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
plugins: [
|
||||
require('@tailwindcss/typography')
|
||||
],
|
||||
prefix: 'void-'
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ 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 '../common/chatThreadService.js';
|
||||
import { StagingSelectionItem, IChatThreadService } from './chatThreadService.js';
|
||||
|
||||
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
|
||||
import { IRange } from '../../../../editor/common/core/range.js';
|
||||
|
|
|
|||
71
src/vs/workbench/contrib/void/browser/terminalToolService.ts
Normal file
71
src/vs/workbench/contrib/void/browser/terminalToolService.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
/*--------------------------------------------------------------------------------------
|
||||
* 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 { 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';
|
||||
|
||||
export interface ITerminalToolService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
createNewTerminal(terminalId: string): Promise<string>;
|
||||
runCommand(command: string, terminalId?: string): Promise<void>;
|
||||
focus(terminalId: string): Promise<void>;
|
||||
}
|
||||
|
||||
export const ITerminalToolService = createDecorator<ITerminalToolService>('TerminalToolService');
|
||||
|
||||
export class TerminalToolService extends Disposable implements ITerminalToolService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
private terminalInstances: Record<string, ITerminalInstance> = {}
|
||||
|
||||
constructor(
|
||||
@ITerminalService private readonly terminalService: ITerminalService
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async createNewTerminal() {
|
||||
const terminalId = generateUuid();
|
||||
|
||||
this.terminalService.createTerminal({});
|
||||
const terminal = await this.terminalService.createTerminal({
|
||||
location: TerminalLocation.Editor,
|
||||
config: { name: `Void Agent (${terminalId})`, }
|
||||
});
|
||||
|
||||
this.terminalInstances[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;
|
||||
}
|
||||
|
||||
async focus(terminalId: string) {
|
||||
const terminal = this.terminalInstances[terminalId];
|
||||
if (!terminal) throw new Error(`That terminal was closed.`);
|
||||
|
||||
|
||||
terminal.focus(true);
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(ITerminalToolService, TerminalToolService, InstantiationType.Eager);
|
||||
509
src/vs/workbench/contrib/void/browser/toolsService.ts
Normal file
509
src/vs/workbench/contrib/void/browser/toolsService.ts
Normal file
|
|
@ -0,0 +1,509 @@
|
|||
import { CancellationToken } from '../../../../base/common/cancellation.js'
|
||||
import { URI } from '../../../../base/common/uri.js'
|
||||
import { IFileService } from '../../../../platform/files/common/files.js'
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js'
|
||||
import { createDecorator, IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'
|
||||
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'
|
||||
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'
|
||||
|
||||
|
||||
// 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<string>(toolNames)
|
||||
export const isAToolName = (toolName: string): toolName is ToolName => {
|
||||
const isAToolName = toolNamesSet.has(toolName)
|
||||
return isAToolName
|
||||
}
|
||||
|
||||
|
||||
export const toolNamesThatRequireApproval = new Set<ToolName>(['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<ToolCallParams[T]> }
|
||||
export type CallTool = { [T in ToolName]: (p: ToolCallParams[T]) => Promise<ToolResultType[T]> }
|
||||
export type ToolResultToString = { [T in ToolName]: (p: ToolCallParams[T], result: ToolResultType[T]) => string }
|
||||
|
||||
|
||||
|
||||
|
||||
// pagination info
|
||||
const MAX_FILE_CHARS_PAGE = 50_000
|
||||
const MAX_CHILDREN_URIs_PAGE = 500
|
||||
|
||||
|
||||
|
||||
const computeDirectoryResult = async (
|
||||
fileService: IFileService,
|
||||
rootURI: URI,
|
||||
pageNumber: number = 1
|
||||
): Promise<ToolResultType['list_dir']> => {
|
||||
const stat = await fileService.resolve(rootURI, { resolveMetadata: false });
|
||||
if (!stat.isDirectory) {
|
||||
return { children: null, hasNextPage: false, hasPrevPage: false, itemsRemaining: 0 };
|
||||
}
|
||||
|
||||
const originalChildrenLength = stat.children?.length ?? 0;
|
||||
const fromChildIdx = MAX_CHILDREN_URIs_PAGE * (pageNumber - 1);
|
||||
const toChildIdx = MAX_CHILDREN_URIs_PAGE * pageNumber - 1; // INCLUSIVE
|
||||
const listChildren = stat.children?.slice(fromChildIdx, toChildIdx + 1) ?? [];
|
||||
|
||||
const children: DirectoryItem[] = listChildren.map(child => ({
|
||||
name: child.name,
|
||||
uri: child.resource,
|
||||
isDirectory: child.isDirectory,
|
||||
isSymbolicLink: child.isSymbolicLink
|
||||
}));
|
||||
|
||||
const hasNextPage = (originalChildrenLength - 1) > toChildIdx;
|
||||
const hasPrevPage = pageNumber > 1;
|
||||
const itemsRemaining = Math.max(0, originalChildrenLength - (toChildIdx + 1));
|
||||
|
||||
return {
|
||||
children,
|
||||
hasNextPage,
|
||||
hasPrevPage,
|
||||
itemsRemaining
|
||||
};
|
||||
};
|
||||
|
||||
const directoryResultToString = (params: ToolCallParams['list_dir'], result: ToolResultType['list_dir']): string => {
|
||||
if (!result.children) {
|
||||
return `Error: ${params.rootURI} is not a directory`;
|
||||
}
|
||||
|
||||
let output = '';
|
||||
const entries = result.children;
|
||||
|
||||
if (!result.hasPrevPage) {
|
||||
output += `${params.rootURI}\n`;
|
||||
}
|
||||
|
||||
for (let i = 0; i < entries.length; i++) {
|
||||
const entry = entries[i];
|
||||
const isLast = i === entries.length - 1 && !result.hasNextPage;
|
||||
const prefix = isLast ? '└── ' : '├── ';
|
||||
|
||||
output += `${prefix}${entry.name}${entry.isDirectory ? '/' : ''}${entry.isSymbolicLink ? ' (symbolic link)' : ''}\n`;
|
||||
}
|
||||
|
||||
if (result.hasNextPage) {
|
||||
output += `└── (${result.itemsRemaining} results remaining...)\n`;
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const validateJSON = (s: string): { [s: string]: unknown } => {
|
||||
try {
|
||||
const o = JSON.parse(s)
|
||||
return o
|
||||
}
|
||||
catch (e) {
|
||||
throw new Error(`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.`)
|
||||
return value
|
||||
}
|
||||
|
||||
|
||||
// TODO!!!! check to make sure in workspace
|
||||
const validateURI = (uriStr: unknown) => {
|
||||
if (typeof uriStr !== 'string') throw new Error('Error: provided uri must be a string.')
|
||||
|
||||
const uri = URI.file(uriStr)
|
||||
return uri
|
||||
}
|
||||
|
||||
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}".`)
|
||||
return parsedInt
|
||||
}
|
||||
|
||||
const validateRecursiveParamStr = (paramsUnknown: unknown) => {
|
||||
if (typeof paramsUnknown !== 'string') throw new Error('Error calling tool: provided params must be a string.')
|
||||
const params = paramsUnknown
|
||||
const isRecursive = params.includes('r')
|
||||
return isRecursive
|
||||
}
|
||||
|
||||
export interface IToolsService {
|
||||
readonly _serviceBrand: undefined;
|
||||
validateParams: ValidateParams;
|
||||
callTool: CallTool;
|
||||
stringOfResult: ToolResultToString;
|
||||
}
|
||||
|
||||
export const IToolsService = createDecorator<IToolsService>('ToolsService');
|
||||
|
||||
export class ToolsService implements IToolsService {
|
||||
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
public validateParams: ValidateParams;
|
||||
public callTool: CallTool;
|
||||
public stringOfResult: ToolResultToString;
|
||||
|
||||
|
||||
constructor(
|
||||
@IFileService fileService: IFileService,
|
||||
@IWorkspaceContextService workspaceContextService: IWorkspaceContextService,
|
||||
@ISearchService searchService: ISearchService,
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
@IVoidFileService voidFileService: IVoidFileService,
|
||||
@IEditCodeService editCodeService: IEditCodeService,
|
||||
// @ITerminalToolService private readonly terminalToolService: ITerminalToolService,
|
||||
) {
|
||||
|
||||
const queryBuilder = instantiationService.createInstance(QueryBuilder);
|
||||
|
||||
this.validateParams = {
|
||||
read_file: async (params: string) => {
|
||||
const o = validateJSON(params)
|
||||
const { uri: uriStr, pageNumber: pageNumberUnknown } = o
|
||||
|
||||
const uri = validateURI(uriStr)
|
||||
const pageNumber = validatePageNum(pageNumberUnknown)
|
||||
|
||||
return { uri, pageNumber }
|
||||
},
|
||||
list_dir: async (params: string) => {
|
||||
const o = validateJSON(params)
|
||||
const { uri: uriStr, pageNumber: pageNumberUnknown } = o
|
||||
|
||||
const uri = validateURI(uriStr)
|
||||
const pageNumber = validatePageNum(pageNumberUnknown)
|
||||
return { rootURI: uri, pageNumber }
|
||||
},
|
||||
pathname_search: async (params: string) => {
|
||||
const o = validateJSON(params)
|
||||
const { query: queryUnknown, pageNumber: pageNumberUnknown } = o
|
||||
|
||||
const queryStr = validateStr('query', queryUnknown)
|
||||
const pageNumber = validatePageNum(pageNumberUnknown)
|
||||
|
||||
return { queryStr, pageNumber }
|
||||
|
||||
},
|
||||
search: async (params: string) => {
|
||||
const o = validateJSON(params)
|
||||
const { query: queryUnknown, pageNumber: pageNumberUnknown } = o
|
||||
|
||||
const queryStr = validateStr('query', queryUnknown)
|
||||
const pageNumber = validatePageNum(pageNumberUnknown)
|
||||
|
||||
return { queryStr, pageNumber }
|
||||
},
|
||||
|
||||
// ---
|
||||
|
||||
create_uri: async (params: string) => {
|
||||
const o = validateJSON(params)
|
||||
const { uri: uriStr } = o
|
||||
const uri = validateURI(uriStr)
|
||||
return { uri }
|
||||
},
|
||||
|
||||
delete_uri: async (params: string) => {
|
||||
const o = validateJSON(params)
|
||||
const { uri: uriStr, params: paramsStr } = o
|
||||
const uri = validateURI(uriStr)
|
||||
const isRecursive = validateRecursiveParamStr(paramsStr)
|
||||
return { uri, isRecursive }
|
||||
},
|
||||
|
||||
edit: async (params: string) => {
|
||||
console.log('validating edit!!!')
|
||||
const o = validateJSON(params)
|
||||
const { uri: uriStr, changeDescription: changeDescriptionUnknown } = o
|
||||
const uri = validateURI(uriStr)
|
||||
const changeDescription = validateStr('changeDescription', changeDescriptionUnknown)
|
||||
|
||||
return { uri, changeDescription }
|
||||
},
|
||||
|
||||
terminal_command: async (s: string) => {
|
||||
const o = validateJSON(s)
|
||||
const { command: commandUnknown } = o
|
||||
const command = validateStr('command', commandUnknown)
|
||||
return { command }
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
|
||||
this.callTool = {
|
||||
read_file: async ({ uri, pageNumber }) => {
|
||||
const readFileContents = await voidFileService.readFile(uri)
|
||||
|
||||
const fromIdx = MAX_FILE_CHARS_PAGE * (pageNumber - 1)
|
||||
const toIdx = MAX_FILE_CHARS_PAGE * pageNumber - 1
|
||||
const fileContents = readFileContents.slice(fromIdx, toIdx + 1) || '(empty)' // paginate
|
||||
const hasNextPage = (readFileContents.length - 1) - toIdx >= 1
|
||||
return { fileContents, hasNextPage }
|
||||
},
|
||||
|
||||
list_dir: async ({ rootURI, pageNumber }) => {
|
||||
const dirResult = await computeDirectoryResult(fileService, rootURI, pageNumber)
|
||||
return dirResult
|
||||
},
|
||||
|
||||
pathname_search: async ({ queryStr, pageNumber }) => {
|
||||
const query = queryBuilder.file(workspaceContextService.getWorkspace().folders.map(f => f.uri), { filePattern: queryStr, })
|
||||
const data = await searchService.fileSearch(query, CancellationToken.None)
|
||||
|
||||
const fromIdx = MAX_CHILDREN_URIs_PAGE * (pageNumber - 1)
|
||||
const toIdx = MAX_CHILDREN_URIs_PAGE * pageNumber - 1
|
||||
const uris = data.results
|
||||
.slice(fromIdx, toIdx + 1) // paginate
|
||||
.map(({ resource, results }) => resource)
|
||||
|
||||
const hasNextPage = (data.results.length - 1) - toIdx >= 1
|
||||
return { uris, hasNextPage }
|
||||
},
|
||||
|
||||
search: async ({ queryStr, pageNumber }) => {
|
||||
const query = queryBuilder.text({ pattern: queryStr, }, workspaceContextService.getWorkspace().folders.map(f => f.uri))
|
||||
const data = await searchService.textSearch(query, CancellationToken.None)
|
||||
|
||||
const fromIdx = MAX_CHILDREN_URIs_PAGE * (pageNumber - 1)
|
||||
const toIdx = MAX_CHILDREN_URIs_PAGE * pageNumber - 1
|
||||
const uris = data.results
|
||||
.slice(fromIdx, toIdx + 1) // paginate
|
||||
.map(({ resource, results }) => resource)
|
||||
|
||||
const hasNextPage = (data.results.length - 1) - toIdx >= 1
|
||||
return { queryStr, uris, hasNextPage }
|
||||
},
|
||||
|
||||
// ---
|
||||
|
||||
create_uri: async ({ uri }) => {
|
||||
await fileService.createFile(uri)
|
||||
return {}
|
||||
},
|
||||
|
||||
delete_uri: async ({ uri, isRecursive }) => {
|
||||
await fileService.del(uri, { recursive: isRecursive })
|
||||
return {}
|
||||
},
|
||||
|
||||
edit: async ({ uri, changeDescription }) => {
|
||||
console.log('editing!!!!')
|
||||
const [_, p] = editCodeService.startApplying({
|
||||
uri,
|
||||
applyStr: changeDescription,
|
||||
from: 'ClickApply',
|
||||
type: 'searchReplace',
|
||||
}) ?? []
|
||||
console.log('B')
|
||||
|
||||
await p
|
||||
return {}
|
||||
},
|
||||
terminal_command: async ({ command }) => {
|
||||
// TODO!!!!
|
||||
// await // Await user confirmation and then command execution before resolving
|
||||
return {}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
const nextPageStr = (hasNextPage: boolean) => hasNextPage ? '\n\n(more on next page...)' : ''
|
||||
|
||||
// given to the LLM after the call
|
||||
this.stringOfResult = {
|
||||
read_file: (params, result) => {
|
||||
return result.fileContents + nextPageStr(result.hasNextPage)
|
||||
},
|
||||
list_dir: (params, result) => {
|
||||
const dirTreeStr = directoryResultToString(params, result)
|
||||
return dirTreeStr + nextPageStr(result.hasNextPage)
|
||||
},
|
||||
pathname_search: (params, result) => {
|
||||
return result.uris.map(uri => uri.fsPath).join('\n') + nextPageStr(result.hasNextPage)
|
||||
},
|
||||
search: (params, result) => {
|
||||
return result.uris.map(uri => uri.fsPath).join('\n') + nextPageStr(result.hasNextPage)
|
||||
},
|
||||
// ---
|
||||
create_uri: (params, result) => {
|
||||
return `URI ${params.uri.fsPath} successfully created.`
|
||||
},
|
||||
delete_uri: (params, result) => {
|
||||
return `URI ${params.uri.fsPath} successfully deleted.`
|
||||
},
|
||||
edit: (params, result) => {
|
||||
return `Change successfully made ${params.uri.fsPath} successfully deleted.`
|
||||
},
|
||||
terminal_command: (params, result) => {
|
||||
return `Terminal command "${params.command}" successfully executed.`
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(IToolsService, ToolsService, InstantiationType.Eager);
|
||||
|
|
@ -53,8 +53,8 @@ import '../common/metricsService.js'
|
|||
import '../common/voidUpdateService.js'
|
||||
|
||||
// tools
|
||||
import '../common/toolsService.js'
|
||||
import './toolsService.js'
|
||||
|
||||
// register Thread History
|
||||
import '../common/chatThreadService.js'
|
||||
import './chatThreadService.js'
|
||||
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
|
|||
this.llmMessageHooks.onError[requestId] = onError
|
||||
|
||||
const { aiInstructions } = this.voidSettingsService.state.globalSettings
|
||||
const { settingsOfProvider } = this.voidSettingsService.state
|
||||
const { settingsOfProvider, optionsOfModelSelection, } = this.voidSettingsService.state
|
||||
|
||||
// params will be stripped of all its functions over the IPC channel
|
||||
this.channel.call('sendLLMMessage', {
|
||||
|
|
@ -126,6 +126,7 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
|
|||
providerName,
|
||||
modelName,
|
||||
settingsOfProvider,
|
||||
optionsOfModelSelection,
|
||||
} satisfies MainSendLLMMessageParams);
|
||||
|
||||
return requestId
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@
|
|||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ChatMessage } from './chatThreadService.js'
|
||||
import { InternalToolInfo, ToolName } from './toolsService.js'
|
||||
import { FeatureName, ProviderName, SettingsOfProvider } from './voidSettingsTypes.js'
|
||||
import type { ChatMessage } from '../browser/chatThreadService.js'
|
||||
import type { InternalToolInfo, ToolName } from '../browser/toolsService.js'
|
||||
import { FeatureName, OptionsOfModelSelection, ProviderName, SettingsOfProvider } from './voidSettingsTypes.js'
|
||||
|
||||
|
||||
export const errorDetails = (fullError: Error | null): string | null => {
|
||||
|
|
@ -22,13 +22,19 @@ export const errorDetails = (fullError: Error | null): string | null => {
|
|||
return null
|
||||
}
|
||||
|
||||
export const getErrorMessage: (error: unknown) => string = (error) => {
|
||||
if (error instanceof Error) return `${error.name}: ${error.message}`
|
||||
return error + ''
|
||||
}
|
||||
|
||||
|
||||
export type LLMChatMessage = {
|
||||
role: 'system' | 'user';
|
||||
content: string;
|
||||
} | {
|
||||
role: 'assistant',
|
||||
content: string;
|
||||
content: string; // text content
|
||||
rawAnthropicAssistantContent?: RawAnthropicAssistantContent[]; // used for anthropic signing
|
||||
} | {
|
||||
role: 'tool';
|
||||
content: string; // result
|
||||
|
|
@ -40,27 +46,31 @@ export type LLMChatMessage = {
|
|||
|
||||
export type ToolCallType = {
|
||||
name: ToolName;
|
||||
params: string;
|
||||
paramsStr: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export type RawAnthropicAssistantContent = { type: 'thinking'; thinking: string; signature: string; } | { type: 'redacted_thinking'; data: string } | { type: 'text', text: string }
|
||||
|
||||
|
||||
export type OnText = (p: { fullText: string; fullReasoning: string }) => void
|
||||
export type OnFinalMessage = (p: { fullText: string, toolCalls?: ToolCallType[], fullReasoning?: string }) => void // id is tool_use_id
|
||||
export type OnFinalMessage = (p: { fullText: string, toolCalls?: ToolCallType[], fullReasoning?: string, rawAnthropicAssistantContent?: RawAnthropicAssistantContent[] }) => void // id is tool_use_id
|
||||
export type OnError = (p: { message: string, fullError: Error | null }) => void
|
||||
export type AbortRef = { current: (() => void) | null }
|
||||
|
||||
|
||||
export const toLLMChatMessage = (c: ChatMessage): LLMChatMessage => {
|
||||
export const toLLMChatMessage = (c: ChatMessage): LLMChatMessage | null => {
|
||||
if (c.role === 'user') {
|
||||
return { role: c.role, content: c.content || '(empty message)' }
|
||||
}
|
||||
else if (c.role === 'assistant')
|
||||
return { role: c.role, content: c.content || '(empty message)' }
|
||||
else if (c.role === 'tool')
|
||||
return { role: c.role, id: c.id, name: c.name, params: c.params, content: c.content || '(empty output)' }
|
||||
return { role: c.role, id: c.id, name: c.name, params: c.paramsStr, content: c.content || '(empty output)' }
|
||||
else if (c.role === 'tool_request')
|
||||
return null
|
||||
else {
|
||||
throw 1
|
||||
throw new Error(`Role ${(c as any).role} not recognized.`)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -103,6 +113,7 @@ export type SendLLMMessageParams = {
|
|||
providerName: ProviderName;
|
||||
modelName: string;
|
||||
settingsOfProvider: SettingsOfProvider;
|
||||
optionsOfModelSelection: OptionsOfModelSelection;
|
||||
} & SendLLMType
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -3,12 +3,11 @@
|
|||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { createDecorator, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { ProxyChannel } from '../../../../base/parts/ipc/common/ipc.js';
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { IMainProcessService } from '../../../../platform/ipc/common/mainProcessService.js';
|
||||
import { localize2 } from '../../../../nls.js';
|
||||
import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';
|
||||
import { registerAction2, Action2 } from '../../../../platform/actions/common/actions.js';
|
||||
import { INotificationService } from '../../../../platform/notification/common/notification.js';
|
||||
|
||||
|
|
|
|||
643
src/vs/workbench/contrib/void/common/modelCapabilities.ts
Normal file
643
src/vs/workbench/contrib/void/common/modelCapabilities.ts
Normal file
|
|
@ -0,0 +1,643 @@
|
|||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { OptionsOfModelSelection, ProviderName } from './voidSettingsTypes.js';
|
||||
|
||||
|
||||
export const defaultModelsOfProvider = {
|
||||
openAI: [ // https://platform.openai.com/docs/models/gp
|
||||
'o3-mini',
|
||||
'o1',
|
||||
'o1-mini',
|
||||
'gpt-4o',
|
||||
'gpt-4o-mini',
|
||||
],
|
||||
anthropic: [ // https://docs.anthropic.com/en/docs/about-claude/models
|
||||
'claude-3-7-sonnet-latest',
|
||||
'claude-3-5-sonnet-latest',
|
||||
'claude-3-5-haiku-latest',
|
||||
'claude-3-opus-latest',
|
||||
],
|
||||
xAI: [ // https://docs.x.ai/docs/models?cluster=us-east-1
|
||||
'grok-2-latest',
|
||||
'grok-3-latest',
|
||||
],
|
||||
gemini: [ // https://ai.google.dev/gemini-api/docs/models/gemini
|
||||
'gemini-2.0-flash',
|
||||
'gemini-1.5-flash',
|
||||
'gemini-1.5-pro',
|
||||
'gemini-1.5-flash-8b',
|
||||
'gemini-2.0-flash-thinking-exp',
|
||||
],
|
||||
deepseek: [ // https://api-docs.deepseek.com/quick_start/pricing
|
||||
'deepseek-chat',
|
||||
'deepseek-reasoner',
|
||||
],
|
||||
ollama: [ // autodetected
|
||||
],
|
||||
vLLM: [ // autodetected
|
||||
],
|
||||
openRouter: [ // https://openrouter.ai/models
|
||||
'anthropic/claude-3.5-sonnet',
|
||||
'deepseek/deepseek-r1',
|
||||
'mistralai/codestral-2501',
|
||||
'qwen/qwen-2.5-coder-32b-instruct',
|
||||
],
|
||||
groq: [ // https://console.groq.com/docs/models
|
||||
'qwen-qwq-32b',
|
||||
'llama-3.3-70b-versatile',
|
||||
'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',
|
||||
// ],
|
||||
openAICompatible: [], // fallback
|
||||
} as const satisfies Record<ProviderName, string[]>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
type ModelOptions = {
|
||||
contextWindow: number; // input tokens // <-- UNUSED
|
||||
maxOutputTokens: number | null; // output tokens // <-- UNUSED
|
||||
cost: { // <-- UNUSED
|
||||
input: number;
|
||||
output: number;
|
||||
cache_read?: number;
|
||||
cache_write?: number;
|
||||
}
|
||||
supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated';
|
||||
supportsTools: false | 'anthropic-style' | 'openai-style';
|
||||
supportsFIM: boolean;
|
||||
|
||||
supportsReasoning: false | {
|
||||
// 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 reasoningMaxOutputTokens?: number; // overrides normal maxOutputTokens // <-- UNUSED (except anthropic)
|
||||
readonly reasoningBudgetSlider?: { type: 'slider'; min: number; max: number; default: number };
|
||||
|
||||
// options related specifically to model output
|
||||
// you are allowed to not include openSourceThinkTags if it's not open source (no such cases as of writing)
|
||||
// if it's open source, put the think tags here so we parse them out in e.g. ollama
|
||||
readonly openSourceThinkTags?: [string, string];
|
||||
};
|
||||
}
|
||||
|
||||
type ProviderReasoningIOSettings = {
|
||||
// include this in payload to get reasoning
|
||||
input?: { includeInPayload?: { [key: string]: any }, };
|
||||
// nameOfFieldInDelta: reasoning output is in response.choices[0].delta[deltaReasoningField]
|
||||
// needsManualParse: whether we must manually parse out the <think> tags
|
||||
output?:
|
||||
| { nameOfFieldInDelta?: string, needsManualParse?: undefined, }
|
||||
| { nameOfFieldInDelta?: undefined, needsManualParse?: true, };
|
||||
}
|
||||
|
||||
type ProviderSettings = {
|
||||
providerReasoningIOSettings?: ProviderReasoningIOSettings; // input/output settings around thinking (allowed to be empty) - only applied if the model supports reasoning output
|
||||
modelOptions: { [key: string]: ModelOptions };
|
||||
modelOptionsFallback: (modelName: string) => (ModelOptions & { modelName: string }) | null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
const modelOptionsDefaults: ModelOptions = {
|
||||
contextWindow: 32_000, // unused
|
||||
maxOutputTokens: null, // unused
|
||||
cost: { input: 0, output: 0 }, // unused
|
||||
supportsSystemMessage: false,
|
||||
supportsTools: false,
|
||||
supportsFIM: false,
|
||||
supportsReasoning: false,
|
||||
}
|
||||
|
||||
|
||||
const openSourceModelOptions_assumingOAICompat = {
|
||||
'deepseekR1': {
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: false,
|
||||
supportsTools: false,
|
||||
supportsReasoning: { canToggleReasoning: false, canIOReasoning: true, openSourceThinkTags: ['<think>', '</think>'] },
|
||||
},
|
||||
'deepseekCoderV2': {
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: false, // unstable
|
||||
supportsTools: false,
|
||||
supportsReasoning: false,
|
||||
},
|
||||
'codestral': {
|
||||
supportsFIM: true,
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: 'openai-style',
|
||||
supportsReasoning: false,
|
||||
},
|
||||
// llama
|
||||
'llama3': {
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: 'openai-style',
|
||||
supportsReasoning: false,
|
||||
},
|
||||
'llama3.1': {
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: 'openai-style',
|
||||
supportsReasoning: false,
|
||||
},
|
||||
'llama3.2': {
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: 'openai-style',
|
||||
supportsReasoning: false,
|
||||
},
|
||||
'llama3.3': {
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: 'openai-style',
|
||||
supportsReasoning: false,
|
||||
},
|
||||
// qwen
|
||||
'qwen2.5coder': {
|
||||
supportsFIM: true,
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: 'openai-style',
|
||||
supportsReasoning: false,
|
||||
},
|
||||
'qwq': {
|
||||
supportsFIM: false, // no FIM, yes reasoning
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: 'openai-style',
|
||||
supportsReasoning: { canToggleReasoning: false, canIOReasoning: true, openSourceThinkTags: ['<think>', '</think>'] },
|
||||
},
|
||||
// FIM only
|
||||
'starcoder2': {
|
||||
supportsFIM: true,
|
||||
supportsSystemMessage: false,
|
||||
supportsTools: false,
|
||||
supportsReasoning: false,
|
||||
},
|
||||
'codegemma:2b': {
|
||||
supportsFIM: true,
|
||||
supportsSystemMessage: false,
|
||||
supportsTools: false,
|
||||
supportsReasoning: false,
|
||||
},
|
||||
} as const satisfies { [s: string]: Partial<ModelOptions> }
|
||||
|
||||
|
||||
|
||||
|
||||
const extensiveModelFallback: ProviderSettings['modelOptionsFallback'] = (modelName) => {
|
||||
const toFallback = (opts: Omit<ModelOptions, 'cost'>): ModelOptions & { modelName: string } => {
|
||||
return {
|
||||
modelName,
|
||||
...opts,
|
||||
supportsSystemMessage: opts.supportsSystemMessage ? 'system-role' : false,
|
||||
cost: { input: 0, output: 0 },
|
||||
}
|
||||
}
|
||||
if (modelName.includes('gpt-4o')) return toFallback(openAIModelOptions['gpt-4o'])
|
||||
if (modelName.includes('claude-3-5') || modelName.includes('claude-3.5')) return toFallback(anthropicModelOptions['claude-3-5-sonnet-20241022'])
|
||||
if (modelName.includes('claude')) return toFallback(anthropicModelOptions['claude-3-7-sonnet-20250219'])
|
||||
if (modelName.includes('grok')) return toFallback(xAIModelOptions['grok-2'])
|
||||
if (modelName.includes('deepseek-r1') || modelName.includes('deepseek-reasoner')) return toFallback({ ...openSourceModelOptions_assumingOAICompat.deepseekR1, contextWindow: 32_000, maxOutputTokens: 4_096, })
|
||||
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 (/\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
|
||||
contextWindow: 200_000,
|
||||
maxOutputTokens: 8_192,
|
||||
cost: { input: 3.00, cache_read: 0.30, cache_write: 3.75, output: 15.00 },
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'separated',
|
||||
supportsTools: 'anthropic-style',
|
||||
supportsReasoning: {
|
||||
canToggleReasoning: 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
|
||||
},
|
||||
},
|
||||
'claude-3-5-sonnet-20241022': {
|
||||
contextWindow: 200_000,
|
||||
maxOutputTokens: 8_192,
|
||||
cost: { input: 3.00, cache_read: 0.30, cache_write: 3.75, output: 15.00 },
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'separated',
|
||||
supportsTools: 'anthropic-style',
|
||||
supportsReasoning: false,
|
||||
},
|
||||
'claude-3-5-haiku-20241022': {
|
||||
contextWindow: 200_000,
|
||||
maxOutputTokens: 8_192,
|
||||
cost: { input: 0.80, cache_read: 0.08, cache_write: 1.00, output: 4.00 },
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'separated',
|
||||
supportsTools: 'anthropic-style',
|
||||
supportsReasoning: false,
|
||||
},
|
||||
'claude-3-opus-20240229': {
|
||||
contextWindow: 200_000,
|
||||
maxOutputTokens: 4_096,
|
||||
cost: { input: 15.00, cache_read: 1.50, cache_write: 18.75, output: 75.00 },
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'separated',
|
||||
supportsTools: 'anthropic-style',
|
||||
supportsReasoning: 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 },
|
||||
maxOutputTokens: 4_096,
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'separated',
|
||||
supportsTools: 'anthropic-style',
|
||||
supportsReasoning: false,
|
||||
}
|
||||
} as const satisfies { [s: string]: ModelOptions }
|
||||
|
||||
const anthropicSettings: ProviderSettings = {
|
||||
modelOptions: anthropicModelOptions,
|
||||
modelOptionsFallback: (modelName) => {
|
||||
let fallbackName: keyof typeof anthropicModelOptions | null = null
|
||||
if (modelName.includes('claude-3-7-sonnet')) fallbackName = 'claude-3-7-sonnet-20250219'
|
||||
if (modelName.includes('claude-3-5-sonnet')) fallbackName = 'claude-3-5-sonnet-20241022'
|
||||
if (modelName.includes('claude-3-5-haiku')) fallbackName = 'claude-3-5-haiku-20241022'
|
||||
if (modelName.includes('claude-3-opus')) fallbackName = 'claude-3-opus-20240229'
|
||||
if (modelName.includes('claude-3-sonnet')) fallbackName = 'claude-3-sonnet-20240229'
|
||||
if (fallbackName) return { modelName: fallbackName, ...anthropicModelOptions[fallbackName] }
|
||||
return { modelName, ...modelOptionsDefaults, maxOutputTokens: 4_096 }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ---------------- OPENAI ----------------
|
||||
const openAIModelOptions = { // https://platform.openai.com/docs/pricing
|
||||
'o1': {
|
||||
contextWindow: 128_000,
|
||||
maxOutputTokens: 100_000,
|
||||
cost: { input: 15.00, cache_read: 7.50, output: 60.00, },
|
||||
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
|
||||
},
|
||||
'o3-mini': {
|
||||
contextWindow: 200_000,
|
||||
maxOutputTokens: 100_000,
|
||||
cost: { input: 1.10, cache_read: 0.55, output: 4.40, },
|
||||
supportsFIM: false,
|
||||
supportsTools: false,
|
||||
supportsSystemMessage: 'developer-role',
|
||||
supportsReasoning: { canIOReasoning: false, canToggleReasoning: false },
|
||||
},
|
||||
'gpt-4o': {
|
||||
contextWindow: 128_000,
|
||||
maxOutputTokens: 16_384,
|
||||
cost: { input: 2.50, cache_read: 1.25, output: 10.00, },
|
||||
supportsFIM: false,
|
||||
supportsTools: 'openai-style',
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsReasoning: false,
|
||||
},
|
||||
'o1-mini': {
|
||||
contextWindow: 128_000,
|
||||
maxOutputTokens: 65_536,
|
||||
cost: { input: 1.10, cache_read: 0.55, output: 4.40, },
|
||||
supportsFIM: false,
|
||||
supportsTools: false,
|
||||
supportsSystemMessage: false, // does not support any system
|
||||
supportsReasoning: { canIOReasoning: false, canToggleReasoning: false },
|
||||
},
|
||||
'gpt-4o-mini': {
|
||||
contextWindow: 128_000,
|
||||
maxOutputTokens: 16_384,
|
||||
cost: { input: 0.15, cache_read: 0.075, output: 0.60, },
|
||||
supportsFIM: false,
|
||||
supportsTools: 'openai-style',
|
||||
supportsSystemMessage: 'system-role', // ??
|
||||
supportsReasoning: false,
|
||||
},
|
||||
} as const satisfies { [s: string]: ModelOptions }
|
||||
|
||||
|
||||
const openAISettings: ProviderSettings = {
|
||||
modelOptions: openAIModelOptions,
|
||||
modelOptionsFallback: (modelName) => {
|
||||
let fallbackName: keyof typeof openAIModelOptions | null = null
|
||||
if (modelName.includes('o1')) { fallbackName = 'o1' }
|
||||
if (modelName.includes('o3-mini')) { fallbackName = 'o3-mini' }
|
||||
if (modelName.includes('gpt-4o')) { fallbackName = 'gpt-4o' }
|
||||
if (fallbackName) return { modelName: fallbackName, ...openAIModelOptions[fallbackName] }
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------- XAI ----------------
|
||||
const xAIModelOptions = {
|
||||
'grok-2': {
|
||||
contextWindow: 131_072,
|
||||
maxOutputTokens: null, // 131_072,
|
||||
cost: { input: 2.00, output: 10.00 },
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: 'openai-style',
|
||||
supportsReasoning: false,
|
||||
},
|
||||
} as const satisfies { [s: string]: ModelOptions }
|
||||
|
||||
const xAISettings: ProviderSettings = {
|
||||
modelOptions: xAIModelOptions,
|
||||
modelOptionsFallback: (modelName) => {
|
||||
let fallbackName: keyof typeof xAIModelOptions | null = null
|
||||
if (modelName.includes('grok-2')) fallbackName = 'grok-2'
|
||||
if (fallbackName) return { modelName: fallbackName, ...xAIModelOptions[fallbackName] }
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ---------------- GEMINI ----------------
|
||||
const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing
|
||||
'gemini-2.0-flash': {
|
||||
contextWindow: 1_048_576,
|
||||
maxOutputTokens: null, // 8_192,
|
||||
cost: { input: 0.10, output: 0.40 },
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: 'openai-style', // we are assuming OpenAI SDK when calling gemini
|
||||
supportsReasoning: false,
|
||||
},
|
||||
'gemini-2.0-flash-lite-preview-02-05': {
|
||||
contextWindow: 1_048_576,
|
||||
maxOutputTokens: null, // 8_192,
|
||||
cost: { input: 0.075, output: 0.30 },
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: 'openai-style',
|
||||
supportsReasoning: false,
|
||||
},
|
||||
'gemini-1.5-flash': {
|
||||
contextWindow: 1_048_576,
|
||||
maxOutputTokens: null, // 8_192,
|
||||
cost: { input: 0.075, output: 0.30 }, // TODO!!! price doubles after 128K tokens, we are NOT encoding that info right now
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: 'openai-style',
|
||||
supportsReasoning: false,
|
||||
},
|
||||
'gemini-1.5-pro': {
|
||||
contextWindow: 2_097_152,
|
||||
maxOutputTokens: null, // 8_192,
|
||||
cost: { input: 1.25, output: 5.00 }, // TODO!!! price doubles after 128K tokens, we are NOT encoding that info right now
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: 'openai-style',
|
||||
supportsReasoning: false,
|
||||
},
|
||||
'gemini-1.5-flash-8b': {
|
||||
contextWindow: 1_048_576,
|
||||
maxOutputTokens: null, // 8_192,
|
||||
cost: { input: 0.0375, output: 0.15 }, // TODO!!! price doubles after 128K tokens, we are NOT encoding that info right now
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: 'openai-style',
|
||||
supportsReasoning: false,
|
||||
},
|
||||
} as const satisfies { [s: string]: ModelOptions }
|
||||
|
||||
const geminiSettings: ProviderSettings = {
|
||||
modelOptions: geminiModelOptions,
|
||||
modelOptionsFallback: (modelName) => { return null }
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ---------------- DEEPSEEK API ----------------
|
||||
const deepseekModelOptions = {
|
||||
'deepseek-chat': {
|
||||
...openSourceModelOptions_assumingOAICompat.deepseekR1,
|
||||
contextWindow: 64_000, // https://api-docs.deepseek.com/quick_start/pricing
|
||||
maxOutputTokens: null, // 8_000,
|
||||
cost: { cache_read: .07, input: .27, output: 1.10, },
|
||||
},
|
||||
'deepseek-reasoner': {
|
||||
...openSourceModelOptions_assumingOAICompat.deepseekCoderV2,
|
||||
contextWindow: 64_000,
|
||||
maxOutputTokens: null, // 8_000,
|
||||
cost: { cache_read: .14, input: .55, output: 2.19, },
|
||||
},
|
||||
} as const satisfies { [s: string]: ModelOptions }
|
||||
|
||||
|
||||
const deepseekSettings: ProviderSettings = {
|
||||
modelOptions: deepseekModelOptions,
|
||||
providerReasoningIOSettings: {
|
||||
// reasoning: OAICompat + response.choices[0].delta.reasoning_content // https://api-docs.deepseek.com/guides/reasoning_model
|
||||
output: { nameOfFieldInDelta: 'reasoning_content' },
|
||||
},
|
||||
modelOptionsFallback: (modelName) => { return null }
|
||||
}
|
||||
|
||||
// ---------------- GROQ ----------------
|
||||
const groqModelOptions = { // https://console.groq.com/docs/models, https://groq.com/pricing/
|
||||
'llama-3.3-70b-versatile': {
|
||||
contextWindow: 128_000,
|
||||
maxOutputTokens: null, // 32_768,
|
||||
cost: { input: 0.59, output: 0.79 },
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: 'openai-style',
|
||||
supportsReasoning: false,
|
||||
},
|
||||
'llama-3.1-8b-instant': {
|
||||
contextWindow: 128_000,
|
||||
maxOutputTokens: null, // 8_192,
|
||||
cost: { input: 0.05, output: 0.08 },
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: 'openai-style',
|
||||
supportsReasoning: false,
|
||||
},
|
||||
'qwen-2.5-coder-32b': {
|
||||
contextWindow: 128_000,
|
||||
maxOutputTokens: null, // not specified?
|
||||
cost: { input: 0.79, output: 0.79 },
|
||||
supportsFIM: false, // unfortunately looks like no FIM support on groq
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: 'openai-style',
|
||||
supportsReasoning: false,
|
||||
},
|
||||
'qwen-qwq-32b': { // https://huggingface.co/Qwen/QwQ-32B
|
||||
contextWindow: 128_000,
|
||||
maxOutputTokens: null, // not specified?
|
||||
cost: { input: 0.29, output: 0.39 },
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: 'openai-style',
|
||||
supportsReasoning: { canIOReasoning: true, canToggleReasoning: false, openSourceThinkTags: ['<think>', '</think>'] }, // 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
|
||||
modelOptions: groqModelOptions,
|
||||
modelOptionsFallback: (modelName) => { return null }
|
||||
}
|
||||
|
||||
|
||||
// ---------------- VLLM, OLLAMA, OPENAICOMPAT (self-hosted / local) ----------------
|
||||
const vLLMSettings: ProviderSettings = {
|
||||
// reasoning: OAICompat + response.choices[0].delta.reasoning_content // https://docs.vllm.ai/en/stable/features/reasoning_outputs.html#streaming-chat-completions
|
||||
providerReasoningIOSettings: { output: { nameOfFieldInDelta: 'reasoning_content' }, },
|
||||
modelOptionsFallback: (modelName) => extensiveModelFallback(modelName),
|
||||
modelOptions: {},
|
||||
}
|
||||
|
||||
const ollamaSettings: ProviderSettings = {
|
||||
// reasoning: we need to filter out reasoning <think> tags manually
|
||||
providerReasoningIOSettings: { output: { needsManualParse: true }, },
|
||||
modelOptionsFallback: (modelName) => extensiveModelFallback(modelName),
|
||||
modelOptions: {},
|
||||
}
|
||||
|
||||
const openaiCompatible: ProviderSettings = {
|
||||
// reasoning: we have no idea what endpoint they used, so we can't consistently parse out reasoning
|
||||
modelOptionsFallback: (modelName) => extensiveModelFallback(modelName),
|
||||
modelOptions: {},
|
||||
}
|
||||
|
||||
|
||||
// ---------------- OPENROUTER ----------------
|
||||
const openRouterModelOptions_assumingOpenAICompat = {
|
||||
'deepseek/deepseek-r1': {
|
||||
...openSourceModelOptions_assumingOAICompat.deepseekR1,
|
||||
contextWindow: 128_000,
|
||||
maxOutputTokens: null,
|
||||
cost: { input: 0.8, output: 2.4 },
|
||||
},
|
||||
'anthropic/claude-3.7-sonnet': {
|
||||
contextWindow: 200_000,
|
||||
maxOutputTokens: null,
|
||||
cost: { input: 3.00, output: 15.00 },
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: 'openai-style',
|
||||
supportsReasoning: { canIOReasoning: true, canToggleReasoning: false }, // TODO!!! false for now
|
||||
},
|
||||
'anthropic/claude-3.5-sonnet': {
|
||||
contextWindow: 200_000,
|
||||
maxOutputTokens: null,
|
||||
cost: { input: 3.00, output: 15.00 },
|
||||
supportsFIM: false,
|
||||
supportsSystemMessage: 'system-role',
|
||||
supportsTools: 'openai-style',
|
||||
supportsReasoning: false,
|
||||
},
|
||||
'mistralai/codestral-2501': {
|
||||
...openSourceModelOptions_assumingOAICompat.codestral,
|
||||
contextWindow: 256_000,
|
||||
maxOutputTokens: null,
|
||||
cost: { input: 0.3, output: 0.9 },
|
||||
supportsTools: 'openai-style',
|
||||
supportsReasoning: false,
|
||||
},
|
||||
'qwen/qwen-2.5-coder-32b-instruct': {
|
||||
...openSourceModelOptions_assumingOAICompat['qwen2.5coder'],
|
||||
contextWindow: 33_000,
|
||||
maxOutputTokens: null,
|
||||
supportsTools: false, // openrouter qwen doesn't seem to support tools...?
|
||||
cost: { input: 0.07, output: 0.16 },
|
||||
},
|
||||
'qwen/qwq-32b': {
|
||||
...openSourceModelOptions_assumingOAICompat['qwq'],
|
||||
contextWindow: 33_000,
|
||||
maxOutputTokens: null,
|
||||
supportsTools: false, // openrouter qwen doesn't seem to support tools...?
|
||||
cost: { input: 0.07, output: 0.16 },
|
||||
}
|
||||
} as const satisfies { [s: string]: ModelOptions }
|
||||
|
||||
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 } },
|
||||
output: { nameOfFieldInDelta: 'reasoning' },
|
||||
},
|
||||
modelOptions: openRouterModelOptions_assumingOpenAICompat,
|
||||
// TODO!!! send a query to openrouter to get the price, etc.
|
||||
modelOptionsFallback: (modelName) => extensiveModelFallback(modelName),
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// ---------------- model settings of everything above ----------------
|
||||
|
||||
const modelSettingsOfProvider: { [providerName in ProviderName]: ProviderSettings } = {
|
||||
openAI: openAISettings,
|
||||
anthropic: anthropicSettings,
|
||||
xAI: xAISettings,
|
||||
gemini: geminiSettings,
|
||||
|
||||
// open source models
|
||||
deepseek: deepseekSettings,
|
||||
groq: groqSettings,
|
||||
|
||||
// open source models + providers (mixture of everything)
|
||||
openRouter: openRouterSettings,
|
||||
vLLM: vLLMSettings,
|
||||
ollama: ollamaSettings,
|
||||
openAICompatible: openaiCompatible,
|
||||
|
||||
// googleVertex: {},
|
||||
// microsoftAzure: {},
|
||||
} as const
|
||||
|
||||
|
||||
// ---------------- exports ----------------
|
||||
|
||||
// returns the capabilities and the adjusted modelName if it was a fallback
|
||||
export const getModelCapabilities = (providerName: ProviderName, modelName: string): ModelOptions & { modelName: string; isUnrecognizedModel: boolean } => {
|
||||
const { modelOptions, modelOptionsFallback } = modelSettingsOfProvider[providerName]
|
||||
if (modelName in modelOptions) return { modelName, ...modelOptions[modelName], isUnrecognizedModel: false }
|
||||
const result = modelOptionsFallback(modelName)
|
||||
if (result) return { ...result, isUnrecognizedModel: false }
|
||||
return { modelName, ...modelOptionsDefaults, isUnrecognizedModel: true }
|
||||
}
|
||||
|
||||
// non-model settings
|
||||
export const getProviderCapabilities = (providerName: ProviderName) => {
|
||||
const { providerReasoningIOSettings } = modelSettingsOfProvider[providerName]
|
||||
return { providerReasoningIOSettings }
|
||||
}
|
||||
|
||||
// state from optionsOfModelSelection
|
||||
export const getModelSelectionState = (providerName: ProviderName, modelName: string, optionsOfModelSelection: OptionsOfModelSelection): { isReasoningEnabled: boolean, reasoningBudget: number | undefined } => {
|
||||
const { canToggleReasoning } = getModelCapabilities(providerName, modelName).supportsReasoning || {}
|
||||
|
||||
const defaultEnabledVal = canToggleReasoning ? true : false
|
||||
const isReasoningEnabled = optionsOfModelSelection[providerName]?.[modelName]?.reasoningEnabled ?? defaultEnabledVal
|
||||
const reasoningBudget = optionsOfModelSelection[providerName]?.[modelName]?.reasoningBudget
|
||||
return { isReasoningEnabled, reasoningBudget }
|
||||
}
|
||||
|
|
@ -1,388 +0,0 @@
|
|||
import { CancellationToken } from '../../../../base/common/cancellation.js'
|
||||
import { URI } from '../../../../base/common/uri.js'
|
||||
import { IFileService } from '../../../../platform/files/common/files.js'
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js'
|
||||
import { createDecorator, IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'
|
||||
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'
|
||||
import { QueryBuilder } from '../../../../workbench/services/search/common/queryBuilder.js'
|
||||
import { ISearchService } from '../../../../workbench/services/search/common/search.js'
|
||||
import { IVoidFileService } from './voidFileService.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 = {
|
||||
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'],
|
||||
},
|
||||
|
||||
|
||||
// create_file: {
|
||||
// name: 'create_file',
|
||||
// description: `Creates a file at the given path. Fails gracefully if the file already exists by doing nothing.`,
|
||||
// params: {
|
||||
// uri: { type: 'string', description: undefined },
|
||||
// },
|
||||
// required: ['uri'],
|
||||
// },
|
||||
|
||||
// create_folder: {
|
||||
// name: 'create_folder',
|
||||
// description: `Creates a folder at the given path. Fails gracefully if the folder already exists by doing nothing.`,
|
||||
// params: {
|
||||
// uri: { type: 'string', description: undefined },
|
||||
// },
|
||||
// required: ['uri'],
|
||||
// },
|
||||
|
||||
|
||||
// go_to_definition: {
|
||||
|
||||
// },
|
||||
|
||||
// go_to_usages:
|
||||
|
||||
// create_file: {
|
||||
// name: 'create_file',
|
||||
// description: `Creates a file at the given path. Fails gracefully if the file already exists by doing nothing.`
|
||||
// params: {
|
||||
// uri: { type: 'string', description: undefined },
|
||||
// }
|
||||
// }
|
||||
|
||||
// semantic_search: {
|
||||
// description: 'Searches files semantically for the given string query.',
|
||||
// // RAG
|
||||
// },
|
||||
} satisfies { [name: string]: InternalToolInfo }
|
||||
|
||||
export type ToolName = keyof typeof voidTools
|
||||
export const toolNames = Object.keys(voidTools) as ToolName[]
|
||||
|
||||
const toolNamesSet = new Set<string>(toolNames)
|
||||
export const isAToolName = (toolName: string): toolName is ToolName => {
|
||||
const isAToolName = toolNamesSet.has(toolName)
|
||||
return isAToolName
|
||||
}
|
||||
|
||||
|
||||
export type ToolParamNames<T extends ToolName> = keyof typeof voidTools[T]['params']
|
||||
export type ToolParamsObj<T extends ToolName> = { [paramName in ToolParamNames<T>]: unknown }
|
||||
|
||||
export type ToolCallReturnType = {
|
||||
'read_file': { uri: URI, fileContents: string, hasNextPage: boolean },
|
||||
'list_dir': { rootURI: URI, children: DirectoryItem[] | null, hasNextPage: boolean, hasPrevPage: boolean, itemsRemaining: number },
|
||||
'pathname_search': { queryStr: string, uris: URI[] | string, hasNextPage: boolean },
|
||||
'search': { queryStr: string, uris: URI[] | string, hasNextPage: boolean }
|
||||
'create_file': {}
|
||||
}
|
||||
|
||||
type DirectoryItem = {
|
||||
uri: URI;
|
||||
name: string;
|
||||
isDirectory: boolean;
|
||||
isSymbolicLink: boolean;
|
||||
}
|
||||
|
||||
export type ToolFns = { [T in ToolName]: (p: string) => Promise<ToolCallReturnType[T]> }
|
||||
export type ToolResultToString = { [T in ToolName]: (result: ToolCallReturnType[T]) => string }
|
||||
|
||||
|
||||
// pagination info
|
||||
const MAX_FILE_CHARS_PAGE = 50_000
|
||||
const MAX_CHILDREN_URIs_PAGE = 500
|
||||
|
||||
|
||||
|
||||
const computeDirectoryResult = async (
|
||||
fileService: IFileService,
|
||||
rootURI: URI,
|
||||
pageNumber: number = 1
|
||||
): Promise<ToolCallReturnType['list_dir']> => {
|
||||
const stat = await fileService.resolve(rootURI, { resolveMetadata: false });
|
||||
if (!stat.isDirectory) {
|
||||
return { rootURI, children: null, hasNextPage: false, hasPrevPage: false, itemsRemaining: 0 };
|
||||
}
|
||||
|
||||
const originalChildrenLength = stat.children?.length ?? 0;
|
||||
const fromChildIdx = MAX_CHILDREN_URIs_PAGE * (pageNumber - 1);
|
||||
const toChildIdx = MAX_CHILDREN_URIs_PAGE * pageNumber - 1; // INCLUSIVE
|
||||
const listChildren = stat.children?.slice(fromChildIdx, toChildIdx + 1) ?? [];
|
||||
|
||||
const children: DirectoryItem[] = listChildren.map(child => ({
|
||||
name: child.name,
|
||||
uri: child.resource,
|
||||
isDirectory: child.isDirectory,
|
||||
isSymbolicLink: child.isSymbolicLink
|
||||
}));
|
||||
|
||||
const hasNextPage = (originalChildrenLength - 1) > toChildIdx;
|
||||
const hasPrevPage = pageNumber > 1;
|
||||
const itemsRemaining = Math.max(0, originalChildrenLength - (toChildIdx + 1));
|
||||
|
||||
return {
|
||||
rootURI,
|
||||
children,
|
||||
hasNextPage,
|
||||
hasPrevPage,
|
||||
itemsRemaining
|
||||
};
|
||||
};
|
||||
|
||||
const directoryResultToString = (result: ToolCallReturnType['list_dir']): string => {
|
||||
if (!result.children) {
|
||||
return `Error: ${result.rootURI} is not a directory`;
|
||||
}
|
||||
|
||||
let output = '';
|
||||
const entries = result.children;
|
||||
|
||||
if (!result.hasPrevPage) {
|
||||
output += `${result.rootURI}\n`;
|
||||
}
|
||||
|
||||
for (let i = 0; i < entries.length; i++) {
|
||||
const entry = entries[i];
|
||||
const isLast = i === entries.length - 1 && !result.hasNextPage;
|
||||
const prefix = isLast ? '└── ' : '├── ';
|
||||
|
||||
output += `${prefix}${entry.name}${entry.isDirectory ? '/' : ''}${entry.isSymbolicLink ? ' (symbolic link)' : ''}\n`;
|
||||
}
|
||||
|
||||
if (result.hasNextPage) {
|
||||
output += `└── (${result.itemsRemaining} results remaining...)\n`;
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const validateJSON = (s: string): { [s: string]: unknown } => {
|
||||
try {
|
||||
const o = JSON.parse(s)
|
||||
return o
|
||||
}
|
||||
catch (e) {
|
||||
throw new Error(`Tool parameter was not a valid JSON: "${s}".`)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
const validateQueryStr = (queryStr: unknown) => {
|
||||
if (typeof queryStr !== 'string') throw new Error('Error calling tool: provided query must be a string.')
|
||||
return queryStr
|
||||
}
|
||||
|
||||
|
||||
// TODO!!!! check to make sure in workspace
|
||||
const validateURI = (uriStr: unknown) => {
|
||||
if (typeof uriStr !== 'string') throw new Error('Error calling tool: provided uri must be a string.')
|
||||
|
||||
const uri = URI.file(uriStr)
|
||||
return uri
|
||||
}
|
||||
|
||||
const validatePageNum = (pageNumberUnknown: unknown) => {
|
||||
const proposedPageNum = Number.parseInt(pageNumberUnknown + '')
|
||||
const num = Number.isInteger(proposedPageNum) ? proposedPageNum : 1
|
||||
const pageNumber = num < 1 ? 1 : num
|
||||
return pageNumber
|
||||
}
|
||||
export interface IToolsService {
|
||||
readonly _serviceBrand: undefined;
|
||||
toolFns: ToolFns;
|
||||
toolResultToString: ToolResultToString;
|
||||
}
|
||||
|
||||
export const IToolsService = createDecorator<IToolsService>('ToolsService');
|
||||
|
||||
export class ToolsService implements IToolsService {
|
||||
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
public toolFns: ToolFns
|
||||
public toolResultToString: ToolResultToString
|
||||
|
||||
|
||||
constructor(
|
||||
@IFileService fileService: IFileService,
|
||||
@IWorkspaceContextService workspaceContextService: IWorkspaceContextService,
|
||||
@ISearchService searchService: ISearchService,
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
@IVoidFileService voidFileService: IVoidFileService,
|
||||
) {
|
||||
|
||||
const queryBuilder = instantiationService.createInstance(QueryBuilder);
|
||||
|
||||
this.toolFns = {
|
||||
read_file: async (s: string) => {
|
||||
console.log('read_file')
|
||||
|
||||
const o = validateJSON(s)
|
||||
const { uri: uriStr, pageNumber: pageNumberUnknown } = o
|
||||
|
||||
const uri = validateURI(uriStr)
|
||||
const pageNumber = validatePageNum(pageNumberUnknown)
|
||||
|
||||
const readFileContents = await voidFileService.readFile(uri)
|
||||
|
||||
const fromIdx = MAX_FILE_CHARS_PAGE * (pageNumber - 1)
|
||||
const toIdx = MAX_FILE_CHARS_PAGE * pageNumber - 1
|
||||
const fileContents = readFileContents.slice(fromIdx, toIdx + 1) || '(empty)' // paginate
|
||||
const hasNextPage = (readFileContents.length - 1) - toIdx >= 1
|
||||
|
||||
|
||||
console.log('read_file result:', fileContents)
|
||||
|
||||
|
||||
return { uri, fileContents, hasNextPage }
|
||||
},
|
||||
list_dir: async (s: string) => {
|
||||
console.log('list_dir')
|
||||
const o = validateJSON(s)
|
||||
const { uri: uriStr, pageNumber: pageNumberUnknown } = o
|
||||
|
||||
const uri = validateURI(uriStr)
|
||||
const pageNumber = validatePageNum(pageNumberUnknown)
|
||||
|
||||
const dirResult = await computeDirectoryResult(fileService, uri, pageNumber)
|
||||
console.log('list_dir result:', dirResult)
|
||||
|
||||
return dirResult
|
||||
},
|
||||
pathname_search: async (s: string) => {
|
||||
console.log('pathname_search')
|
||||
const o = validateJSON(s)
|
||||
const { query: queryUnknown, pageNumber: pageNumberUnknown } = o
|
||||
|
||||
const queryStr = validateQueryStr(queryUnknown)
|
||||
const pageNumber = validatePageNum(pageNumberUnknown)
|
||||
|
||||
const query = queryBuilder.file(workspaceContextService.getWorkspace().folders.map(f => f.uri), { filePattern: queryStr, })
|
||||
const data = await searchService.fileSearch(query, CancellationToken.None)
|
||||
|
||||
const fromIdx = MAX_CHILDREN_URIs_PAGE * (pageNumber - 1)
|
||||
const toIdx = MAX_CHILDREN_URIs_PAGE * pageNumber - 1
|
||||
const uris = data.results
|
||||
.slice(fromIdx, toIdx + 1) // paginate
|
||||
.map(({ resource, results }) => resource)
|
||||
|
||||
const hasNextPage = (data.results.length - 1) - toIdx >= 1
|
||||
console.log('pathname_search result:', uris)
|
||||
|
||||
return { queryStr, uris, hasNextPage }
|
||||
},
|
||||
search: async (s: string) => {
|
||||
|
||||
|
||||
console.log('search')
|
||||
|
||||
const o = validateJSON(s)
|
||||
const { query: queryUnknown, pageNumber: pageNumberUnknown } = o
|
||||
|
||||
const queryStr = validateQueryStr(queryUnknown)
|
||||
const pageNumber = validatePageNum(pageNumberUnknown)
|
||||
|
||||
const query = queryBuilder.text({ pattern: queryStr, }, workspaceContextService.getWorkspace().folders.map(f => f.uri))
|
||||
const data = await searchService.textSearch(query, CancellationToken.None)
|
||||
|
||||
const fromIdx = MAX_CHILDREN_URIs_PAGE * (pageNumber - 1)
|
||||
const toIdx = MAX_CHILDREN_URIs_PAGE * pageNumber - 1
|
||||
const uris = data.results
|
||||
.slice(fromIdx, toIdx + 1) // paginate
|
||||
.map(({ resource, results }) => resource)
|
||||
|
||||
const hasNextPage = (data.results.length - 1) - toIdx >= 1
|
||||
|
||||
console.log('search result:', uris)
|
||||
|
||||
return { queryStr, uris, hasNextPage }
|
||||
},
|
||||
|
||||
|
||||
}
|
||||
|
||||
const nextPageStr = (hasNextPage: boolean) => hasNextPage ? '\n\n(more on next page...)' : ''
|
||||
|
||||
this.toolResultToString = {
|
||||
read_file: (result) => {
|
||||
return nextPageStr(result.hasNextPage)
|
||||
},
|
||||
list_dir: (result) => {
|
||||
const dirTreeStr = directoryResultToString(result)
|
||||
return dirTreeStr + nextPageStr(result.hasNextPage)
|
||||
},
|
||||
pathname_search: (result) => {
|
||||
if (typeof result.uris === 'string') return result.uris
|
||||
return result.uris.map(uri => uri.fsPath).join('\n') + nextPageStr(result.hasNextPage)
|
||||
},
|
||||
search: (result) => {
|
||||
if (typeof result.uris === 'string') return result.uris
|
||||
return result.uris.map(uri => uri.fsPath).join('\n') + nextPageStr(result.hasNextPage)
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
registerSingleton(IToolsService, ToolsService, InstantiationType.Eager);
|
||||
|
|
@ -3,7 +3,6 @@
|
|||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { isWindows } from '../../../../base/common/platform.js';
|
||||
import { URI } from '../../../../base/common/uri.js';
|
||||
import { EndOfLinePreference } from '../../../../editor/common/model.js';
|
||||
import { IModelService } from '../../../../editor/common/services/model.js';
|
||||
|
|
@ -11,11 +10,6 @@ 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';
|
||||
|
||||
|
||||
// linebreak symbols
|
||||
export const allLinebreakSymbols = ['\r\n', '\n']
|
||||
export const _ln = isWindows ? allLinebreakSymbols[0] : allLinebreakSymbols[1]
|
||||
|
||||
export interface IVoidFileService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
|
|
@ -52,19 +46,10 @@ export class VoidFileService implements IVoidFileService {
|
|||
_readFileRaw = async (uri: URI, range?: { startLineNumber: number, endLineNumber: number }): Promise<string | null> => {
|
||||
|
||||
try { // this throws an error if no file exists (eg it was deleted)
|
||||
|
||||
const res = await this.fileService.readFile(uri);
|
||||
|
||||
if (range) {
|
||||
return res.value.toString()
|
||||
.split(_ln)
|
||||
.slice(range.startLineNumber - 1, range.endLineNumber)
|
||||
.join(_ln)
|
||||
}
|
||||
|
||||
return res.value.toString();
|
||||
|
||||
|
||||
const str = res.value.toString().replace(/\r\n/g, '\n'); // even if not on Windows, might read a file with \r\n
|
||||
if (range) return str.split('\n').slice(range.startLineNumber - 1, range.endLineNumber).join('\n')
|
||||
return str;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,11 +11,18 @@ import { registerSingleton, InstantiationType } from '../../../../platform/insta
|
|||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
|
||||
import { IMetricsService } from './metricsService.js';
|
||||
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, VoidModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, defaultProviderSettings } from './voidSettingsTypes.js';
|
||||
import { getModelCapabilities } from './modelCapabilities.js';
|
||||
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, VoidModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, defaultProviderSettings, ModelSelectionOptions, OptionsOfModelSelection } from './voidSettingsTypes.js';
|
||||
|
||||
|
||||
const STORAGE_KEY = 'void.settingsServiceStorage'
|
||||
|
||||
|
||||
// name is the name in the dropdown
|
||||
export type ModelOption = { name: string, selection: ModelSelection }
|
||||
|
||||
|
||||
|
||||
type SetSettingOfProviderFn = <S extends SettingName>(
|
||||
providerName: ProviderName,
|
||||
settingName: S,
|
||||
|
|
@ -25,16 +32,17 @@ type SetSettingOfProviderFn = <S extends SettingName>(
|
|||
type SetModelSelectionOfFeatureFn = <K extends FeatureName>(
|
||||
featureName: K,
|
||||
newVal: ModelSelectionOfFeature[K],
|
||||
options?: { doNotApplyEffects?: true }
|
||||
) => Promise<void>;
|
||||
|
||||
type SetGlobalSettingFn = <T extends GlobalSettingName>(settingName: T, newVal: GlobalSettings[T]) => void;
|
||||
|
||||
export type ModelOption = { name: string, selection: ModelSelection }
|
||||
type SetOptionsOfModelSelection = (providerName: ProviderName, modelName: string, newVal: Partial<ModelSelectionOptions>) => void
|
||||
|
||||
|
||||
export type VoidSettingsState = {
|
||||
readonly settingsOfProvider: SettingsOfProvider; // optionsOfProvider
|
||||
readonly modelSelectionOfFeature: ModelSelectionOfFeature; // stateOfFeature
|
||||
readonly optionsOfModelSelection: OptionsOfModelSelection;
|
||||
readonly globalSettings: GlobalSettings;
|
||||
|
||||
readonly _modelOptions: ModelOption[] // computed based on the two above items
|
||||
|
|
@ -49,12 +57,11 @@ export interface IVoidSettingsService {
|
|||
readonly state: VoidSettingsState; // in order to play nicely with react, you should immutably change state
|
||||
readonly waitForInitState: Promise<void>;
|
||||
|
||||
readAndInitializeState: (providedState?: VoidSettingsState) => Promise<void>;
|
||||
|
||||
onDidChangeState: Event<void>;
|
||||
|
||||
setSettingOfProvider: SetSettingOfProviderFn;
|
||||
setModelSelectionOfFeature: SetModelSelectionOfFeatureFn;
|
||||
setOptionsOfModelSelection: SetOptionsOfModelSelection;
|
||||
setGlobalSetting: SetGlobalSettingFn;
|
||||
|
||||
setAutodetectedModels(providerName: ProviderName, modelNames: string[], logging: object): void;
|
||||
|
|
@ -88,6 +95,14 @@ const _updatedModelsAfterDefaultModelsChange = (defaultModelNames: string[], opt
|
|||
}
|
||||
|
||||
|
||||
export const modelFilterOfFeatureName: { [featureName in FeatureName]: { filter: (o: ModelSelection) => boolean; emptyMessage: string | null } } = {
|
||||
'Autocomplete': { filter: o => getModelCapabilities(o.providerName, o.modelName).supportsFIM, emptyMessage: 'No models support FIM' },
|
||||
'Ctrl+L': { filter: o => true, emptyMessage: null },
|
||||
'Ctrl+K': { filter: o => true, emptyMessage: null },
|
||||
'Apply': { filter: o => true, emptyMessage: null },
|
||||
}
|
||||
|
||||
|
||||
const _validatedState = (state: Omit<VoidSettingsState, '_modelOptions'>) => {
|
||||
|
||||
let newSettingsOfProvider = state.settingsOfProvider
|
||||
|
|
@ -125,14 +140,17 @@ const _validatedState = (state: Omit<VoidSettingsState, '_modelOptions'>) => {
|
|||
let newModelSelectionOfFeature = state.modelSelectionOfFeature
|
||||
for (const featureName of featureNames) {
|
||||
|
||||
const modelSelectionAtFeature = newModelSelectionOfFeature[featureName]
|
||||
const selnIdx = modelSelectionAtFeature === null ? -1 : newModelOptions.findIndex(m => modelSelectionsEqual(m.selection, modelSelectionAtFeature))
|
||||
const { filter } = modelFilterOfFeatureName[featureName]
|
||||
const modelOptionsForThisFeature = newModelOptions.filter((o) => filter(o.selection))
|
||||
|
||||
if (selnIdx !== -1) continue
|
||||
const modelSelectionAtFeature = newModelSelectionOfFeature[featureName]
|
||||
const selnIdx = modelSelectionAtFeature === null ? -1 : modelOptionsForThisFeature.findIndex(m => modelSelectionsEqual(m.selection, modelSelectionAtFeature))
|
||||
|
||||
if (selnIdx !== -1) continue // no longer in list, so update to 1st in list or null
|
||||
|
||||
newModelSelectionOfFeature = {
|
||||
...newModelSelectionOfFeature,
|
||||
[featureName]: newModelOptions.length === 0 ? null : newModelOptions[0].selection
|
||||
[featureName]: modelOptionsForThisFeature.length === 0 ? null : modelOptionsForThisFeature[0].selection
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -156,6 +174,7 @@ const defaultState = () => {
|
|||
settingsOfProvider: deepClone(defaultSettingsOfProvider),
|
||||
modelSelectionOfFeature: { 'Ctrl+L': null, 'Ctrl+K': null, 'Autocomplete': null, 'Apply': null },
|
||||
globalSettings: deepClone(defaultGlobalSettings),
|
||||
optionsOfModelSelection: {},
|
||||
_modelOptions: [], // computed later
|
||||
}
|
||||
return d
|
||||
|
|
@ -192,48 +211,13 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
|||
this.readAndInitializeState()
|
||||
}
|
||||
|
||||
async readAndInitializeState(providedState?: VoidSettingsState) {
|
||||
// If providedState is given, use it instead of reading from storage
|
||||
const readS = providedState || await this._readState();
|
||||
async readAndInitializeState() {
|
||||
const readS = await this._readState();
|
||||
|
||||
// the stored data structure might be outdated, so we need to update it here
|
||||
const newSettingsOfProvider = {
|
||||
// A HACK BECAUSE WE ADDED DEEPSEEK (did not exist before, comes before readS)
|
||||
...{ deepseek: defaultSettingsOfProvider.deepseek },
|
||||
|
||||
// A HACK BECAUSE WE ADDED XAI (did not exist before, comes before readS)
|
||||
...{ xAI: defaultSettingsOfProvider.xAI },
|
||||
|
||||
// A HACK BECAUSE WE ADDED VLLM (did not exist before, comes before readS)
|
||||
...{ vLLM: defaultSettingsOfProvider.vLLM },
|
||||
|
||||
...readS.settingsOfProvider,
|
||||
|
||||
// A HACK BECAUSE WE ADDED NEW GEMINI MODELS (existed before, comes after readS)
|
||||
gemini: {
|
||||
...readS.settingsOfProvider.gemini,
|
||||
models: [
|
||||
...readS.settingsOfProvider.gemini.models,
|
||||
...defaultSettingsOfProvider.gemini.models.filter(m => /* if cant find the model in readS (yes this is O(n^2), very small) */ !readS.settingsOfProvider.gemini.models.find(m2 => m2.modelName === m.modelName))
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
const newModelSelectionOfFeature = {
|
||||
// A HACK BECAUSE WE ADDED FastApply
|
||||
...{ 'Apply': null },
|
||||
...readS.modelSelectionOfFeature,
|
||||
};
|
||||
|
||||
const finalState = {
|
||||
...readS,
|
||||
settingsOfProvider: newSettingsOfProvider,
|
||||
modelSelectionOfFeature: newModelSelectionOfFeature,
|
||||
};
|
||||
|
||||
const finalState = readS
|
||||
this.state = _validatedState(finalState);
|
||||
|
||||
|
||||
this._resolver();
|
||||
this._onDidChangeState.fire();
|
||||
}
|
||||
|
|
@ -260,6 +244,8 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
|||
|
||||
const newModelSelectionOfFeature = this.state.modelSelectionOfFeature
|
||||
|
||||
const newOptionsOfModelSelection = this.state.optionsOfModelSelection
|
||||
|
||||
const newSettingsOfProvider: SettingsOfProvider = {
|
||||
...this.state.settingsOfProvider,
|
||||
[providerName]: {
|
||||
|
|
@ -272,6 +258,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
|||
|
||||
const newState = {
|
||||
modelSelectionOfFeature: newModelSelectionOfFeature,
|
||||
optionsOfModelSelection: newOptionsOfModelSelection,
|
||||
settingsOfProvider: newSettingsOfProvider,
|
||||
globalSettings: newGlobalSettings,
|
||||
}
|
||||
|
|
@ -299,7 +286,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
|||
}
|
||||
|
||||
|
||||
setModelSelectionOfFeature: SetModelSelectionOfFeatureFn = async (featureName, newVal, options) => {
|
||||
setModelSelectionOfFeature: SetModelSelectionOfFeatureFn = async (featureName, newVal) => {
|
||||
const newState: VoidSettingsState = {
|
||||
...this.state,
|
||||
modelSelectionOfFeature: {
|
||||
|
|
@ -310,8 +297,26 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
|||
|
||||
this.state = newState
|
||||
|
||||
if (options?.doNotApplyEffects)
|
||||
return
|
||||
await this._storeState()
|
||||
this._onDidChangeState.fire()
|
||||
}
|
||||
|
||||
|
||||
setOptionsOfModelSelection = async (providerName: ProviderName, modelName: string, newVal: Partial<ModelSelectionOptions>) => {
|
||||
const newState: VoidSettingsState = {
|
||||
...this.state,
|
||||
optionsOfModelSelection: {
|
||||
...this.state.optionsOfModelSelection,
|
||||
[providerName]: {
|
||||
...this.state.optionsOfModelSelection[providerName],
|
||||
[modelName]: {
|
||||
...this.state.optionsOfModelSelection[providerName]?.[modelName],
|
||||
...newVal
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.state = newState
|
||||
|
||||
await this._storeState()
|
||||
this._onDidChangeState.fire()
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { defaultModelsOfProvider } from './modelCapabilities.js';
|
||||
import { VoidSettingsState } from './voidSettingsService.js'
|
||||
|
||||
|
||||
|
|
@ -47,63 +48,6 @@ export const defaultProviderSettings = {
|
|||
|
||||
|
||||
|
||||
export const defaultModelsOfProvider = {
|
||||
openAI: [ // https://platform.openai.com/docs/models/gp
|
||||
'o1',
|
||||
'o3-mini',
|
||||
'o1-mini',
|
||||
'gpt-4o',
|
||||
'gpt-4o-mini',
|
||||
],
|
||||
anthropic: [ // https://docs.anthropic.com/en/docs/about-claude/models
|
||||
'claude-3-7-sonnet-latest',
|
||||
// 'claude-3-5-sonnet-latest',
|
||||
'claude-3-5-haiku-latest',
|
||||
'claude-3-opus-latest',
|
||||
],
|
||||
xAI: [ // https://docs.x.ai/docs/models?cluster=us-east-1
|
||||
'grok-2-latest',
|
||||
'grok-3-latest',
|
||||
],
|
||||
gemini: [ // https://ai.google.dev/gemini-api/docs/models/gemini
|
||||
'gemini-2.0-flash',
|
||||
'gemini-1.5-flash',
|
||||
'gemini-1.5-pro',
|
||||
'gemini-1.5-flash-8b',
|
||||
'gemini-2.0-flash-thinking-exp',
|
||||
],
|
||||
deepseek: [ // https://api-docs.deepseek.com/quick_start/pricing
|
||||
'deepseek-chat',
|
||||
'deepseek-reasoner',
|
||||
],
|
||||
ollama: [ // autodetected
|
||||
],
|
||||
vLLM: [ // autodetected
|
||||
],
|
||||
openRouter: [ // https://openrouter.ai/models
|
||||
'anthropic/claude-3.5-sonnet',
|
||||
'deepseek/deepseek-r1',
|
||||
'mistralai/codestral-2501',
|
||||
'qwen/qwen-2.5-coder-32b-instruct',
|
||||
],
|
||||
groq: [ // https://console.groq.com/docs/models
|
||||
'llama-3.3-70b-versatile',
|
||||
'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',
|
||||
// ],
|
||||
openAICompatible: [], // fallback
|
||||
} as const satisfies Record<ProviderName, string[]>
|
||||
|
||||
|
||||
|
||||
|
||||
export type ProviderName = keyof typeof defaultProviderSettings
|
||||
export const providerNames = Object.keys(defaultProviderSettings) as ProviderName[]
|
||||
|
||||
|
|
@ -197,7 +141,7 @@ export const displayInfoOfProviderName = (providerName: ProviderName): DisplayIn
|
|||
}
|
||||
else if (providerName === 'xAI') {
|
||||
return {
|
||||
title: 'xAI API',
|
||||
title: 'Grok (xAI)',
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -380,7 +324,7 @@ export const displayInfoOfFeatureName = (featureName: FeatureName) => {
|
|||
else if (featureName === 'Ctrl+L')
|
||||
return 'Chat'
|
||||
else if (featureName === 'Apply')
|
||||
return 'Apply'
|
||||
return 'Fast Apply'
|
||||
else
|
||||
throw new Error(`Feature Name ${featureName} not allowed`)
|
||||
}
|
||||
|
|
@ -434,15 +378,21 @@ export const isFeatureNameDisabled = (featureName: FeatureName, settingsState: V
|
|||
|
||||
|
||||
|
||||
export type ChatMode = 'agent' | 'gather' | 'chat'
|
||||
|
||||
|
||||
export type GlobalSettings = {
|
||||
autoRefreshModels: boolean;
|
||||
aiInstructions: string;
|
||||
enableAutocomplete: boolean;
|
||||
chatMode: ChatMode;
|
||||
}
|
||||
|
||||
export const defaultGlobalSettings: GlobalSettings = {
|
||||
autoRefreshModels: true,
|
||||
aiInstructions: '',
|
||||
enableAutocomplete: false,
|
||||
chatMode: 'agent',
|
||||
}
|
||||
|
||||
export type GlobalSettingName = keyof GlobalSettings
|
||||
|
|
@ -459,4 +409,9 @@ export const globalSettingNames = Object.keys(defaultGlobalSettings) as GlobalSe
|
|||
|
||||
|
||||
|
||||
export type ModelSelectionOptions = {
|
||||
reasoningEnabled?: boolean;
|
||||
reasoningBudget?: number;
|
||||
}
|
||||
|
||||
export type OptionsOfModelSelection = Partial<{ [providerName in ProviderName]: { [modelName: string]: ModelSelectionOptions | undefined } }>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +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 { LLMChatMessage, LLMFIMMessage } from '../../common/llmMessageTypes.js';
|
||||
import { RawAnthropicAssistantContent, LLMChatMessage, LLMFIMMessage } from '../../common/llmMessageTypes.js';
|
||||
import { deepClone } from '../../../../../base/common/objects.js';
|
||||
|
||||
|
||||
|
|
@ -14,9 +17,28 @@ export const parseObject = (args: unknown) => {
|
|||
}
|
||||
|
||||
|
||||
type InternalLLMChatMessage = {
|
||||
role: 'system' | 'user';
|
||||
content: string;
|
||||
} | {
|
||||
role: 'assistant',
|
||||
content: string | (RawAnthropicAssistantContent | { type: 'text'; text: string })[];
|
||||
rawAnthropicAssistantContent?: RawAnthropicAssistantContent[] | undefined;
|
||||
} | {
|
||||
role: 'tool';
|
||||
content: string; // result
|
||||
name: string;
|
||||
params: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
|
||||
const prepareMessages_normalize = ({ messages: messages_ }: { messages: LLMChatMessage[] }) => {
|
||||
const messages = deepClone(messages_)
|
||||
const newMessages: LLMChatMessage[] = []
|
||||
if (messages.length >= 0) newMessages.push(messages[0])
|
||||
|
||||
// remove duplicate roles
|
||||
for (let i = 1; i < messages.length; i += 1) {
|
||||
const curr = messages[i]
|
||||
const prev = messages[i - 1]
|
||||
|
|
@ -32,13 +54,42 @@ const prepareMessages_normalize = ({ messages: messages_ }: { messages: LLMChatM
|
|||
return { messages: finalMessages }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// remove rawAnthropicAssistantContent, and make content equal to it if supportsAnthropicContent
|
||||
const prepareMessages_anthropicContent = ({ messages, supportsAnthropicContent }: { messages: LLMChatMessage[], supportsAnthropicContent: boolean }) => {
|
||||
const newMessages: InternalLLMChatMessage[] = []
|
||||
for (const m of messages) {
|
||||
if (m.role !== 'assistant') {
|
||||
newMessages.push(m)
|
||||
continue
|
||||
}
|
||||
let newMessage: InternalLLMChatMessage
|
||||
if (supportsAnthropicContent) {
|
||||
const newContent = m.rawAnthropicAssistantContent
|
||||
newMessage = { role: 'assistant', content: newContent ?? m.content }
|
||||
}
|
||||
else {
|
||||
newMessage = m
|
||||
}
|
||||
delete newMessage.rawAnthropicAssistantContent // important to delete this field
|
||||
newMessages.push(m)
|
||||
}
|
||||
return { messages: newMessages }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// no matter whether the model supports a system message or not (or what format it supports), add it in some way
|
||||
const prepareMessages_systemMessage = ({
|
||||
messages,
|
||||
aiInstructions,
|
||||
supportsSystemMessage,
|
||||
}: {
|
||||
messages: LLMChatMessage[],
|
||||
messages: InternalLLMChatMessage[],
|
||||
aiInstructions: string,
|
||||
supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated',
|
||||
})
|
||||
|
|
@ -56,7 +107,7 @@ const prepareMessages_systemMessage = ({
|
|||
let separateSystemMessageStr: string | undefined = undefined
|
||||
|
||||
// remove all system messages
|
||||
const newMessages: (LLMChatMessage | { role: 'developer', content: string })[] = messages.filter(msg => msg.role !== 'system')
|
||||
const newMessages: (InternalLLMChatMessage | { role: 'developer', content: string })[] = messages.filter(msg => msg.role !== 'system')
|
||||
|
||||
|
||||
// if (!supportsTools) {
|
||||
|
|
@ -65,6 +116,7 @@ const prepareMessages_systemMessage = ({
|
|||
// }
|
||||
|
||||
|
||||
// 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
|
||||
if (supportsSystemMessage) {
|
||||
|
|
@ -77,25 +129,18 @@ const prepareMessages_systemMessage = ({
|
|||
}
|
||||
// if does not support system message
|
||||
else {
|
||||
if (supportsSystemMessage) {
|
||||
if (newMessages.length === 0)
|
||||
newMessages.push({ role: 'user', content: systemMessageStr })
|
||||
// add system mesasges to first message (should be a user message)
|
||||
else {
|
||||
const newFirstMessage = {
|
||||
role: 'user',
|
||||
content: (''
|
||||
+ '<SYSTEM_MESSAGE>\n'
|
||||
+ systemMessageStr
|
||||
+ '\n'
|
||||
+ '</SYSTEM_MESSAGE>\n'
|
||||
+ newMessages[0].content
|
||||
)
|
||||
} as const
|
||||
newMessages.splice(0, 1) // delete first message
|
||||
newMessages.unshift(newFirstMessage) // add new first message
|
||||
}
|
||||
}
|
||||
const newFirstMessage = {
|
||||
role: 'user',
|
||||
content: (''
|
||||
+ '<SYSTEM_MESSAGE>\n'
|
||||
+ systemMessageStr
|
||||
+ '\n'
|
||||
+ '</SYSTEM_MESSAGE>\n'
|
||||
+ newMessages[0].content
|
||||
)
|
||||
} as const
|
||||
newMessages.splice(0, 1) // delete first message
|
||||
newMessages.unshift(newFirstMessage) // add new first message
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -128,12 +173,12 @@ openai on prompting - https://platform.openai.com/docs/guides/reasoning#advice-o
|
|||
openai on developer system message - https://cdn.openai.com/spec/model-spec-2024-05-08.html#follow-the-chain-of-command
|
||||
*/
|
||||
|
||||
const prepareMessages_tools_openai = ({ messages }: { messages: LLMChatMessage[], }) => {
|
||||
const prepareMessages_tools_openai = ({ messages }: { messages: InternalLLMChatMessage[], }) => {
|
||||
|
||||
const newMessages: (
|
||||
Exclude<LLMChatMessage, { role: 'assistant' | 'tool' }> | {
|
||||
Exclude<InternalLLMChatMessage, { role: 'assistant' | 'tool' }> | {
|
||||
role: 'assistant',
|
||||
content: string;
|
||||
content: string | object[];
|
||||
tool_calls?: {
|
||||
type: 'function';
|
||||
id: string;
|
||||
|
|
@ -205,19 +250,22 @@ anthropic RESPONSE (role=user):
|
|||
}]
|
||||
*/
|
||||
|
||||
const prepareMessages_tools_anthropic = ({ messages }: { messages: LLMChatMessage[], }) => {
|
||||
const prepareMessages_tools_anthropic = ({ messages }: { messages: InternalLLMChatMessage[], }) => {
|
||||
const newMessages: (
|
||||
Exclude<LLMChatMessage, { role: 'assistant' | 'user' }> | {
|
||||
Exclude<InternalLLMChatMessage, { role: 'assistant' | 'user' }> | {
|
||||
role: 'assistant',
|
||||
content: string | ({
|
||||
type: 'text';
|
||||
text: string;
|
||||
} | {
|
||||
type: 'tool_use';
|
||||
name: string;
|
||||
input: Record<string, any>;
|
||||
id: string;
|
||||
})[]
|
||||
content: string | (
|
||||
| RawAnthropicAssistantContent
|
||||
| {
|
||||
type: 'text';
|
||||
text: string;
|
||||
}
|
||||
| {
|
||||
type: 'tool_use';
|
||||
name: string;
|
||||
input: Record<string, any>;
|
||||
id: string;
|
||||
})[]
|
||||
} | {
|
||||
role: 'user',
|
||||
content: string | ({
|
||||
|
|
@ -260,7 +308,7 @@ const prepareMessages_tools_anthropic = ({ messages }: { messages: LLMChatMessag
|
|||
|
||||
|
||||
|
||||
const prepareMessages_tools = ({ messages, supportsTools }: { messages: LLMChatMessage[], supportsTools: false | 'anthropic-style' | 'openai-style' }) => {
|
||||
const prepareMessages_tools = ({ messages, supportsTools }: { messages: InternalLLMChatMessage[], supportsTools: false | 'anthropic-style' | 'openai-style' }) => {
|
||||
if (!supportsTools) {
|
||||
return { messages: messages }
|
||||
}
|
||||
|
|
@ -271,7 +319,7 @@ const prepareMessages_tools = ({ messages, supportsTools }: { messages: LLMChatM
|
|||
return prepareMessages_tools_openai({ messages })
|
||||
}
|
||||
else {
|
||||
throw 1
|
||||
throw new Error(`supportsTools type not recognized`)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -311,26 +359,28 @@ gemini response:
|
|||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// --- CHAT ---
|
||||
|
||||
export const prepareMessages = ({
|
||||
messages,
|
||||
aiInstructions,
|
||||
supportsSystemMessage,
|
||||
supportsTools,
|
||||
supportsAnthropicContent,
|
||||
}: {
|
||||
messages: LLMChatMessage[],
|
||||
aiInstructions: string,
|
||||
supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated',
|
||||
supportsTools: false | 'anthropic-style' | 'openai-style',
|
||||
supportsAnthropicContent: boolean,
|
||||
}) => {
|
||||
const { messages: messages1 } = prepareMessages_normalize({ messages })
|
||||
const { messages: messages2, separateSystemMessageStr } = prepareMessages_systemMessage({ messages: messages1, aiInstructions, supportsSystemMessage })
|
||||
const { messages: messages3 } = prepareMessages_tools({ messages: messages2, supportsTools })
|
||||
const { messages: messages2 } = prepareMessages_anthropicContent({ messages: messages1, supportsAnthropicContent })
|
||||
const { messages: messages3, separateSystemMessageStr } = prepareMessages_systemMessage({ messages: messages2, aiInstructions, supportsSystemMessage })
|
||||
const { messages: messages4 } = prepareMessages_tools({ messages: messages3, supportsTools })
|
||||
|
||||
return {
|
||||
messages: messages3 as any,
|
||||
messages: messages4 as any,
|
||||
separateSystemMessageStr
|
||||
} as const
|
||||
}
|
||||
|
|
@ -339,6 +389,10 @@ export const prepareMessages = ({
|
|||
|
||||
|
||||
|
||||
|
||||
|
||||
// --- FIM ---
|
||||
|
||||
export const prepareFIMMessage = ({
|
||||
messages,
|
||||
aiInstructions,
|
||||
|
|
@ -358,6 +412,5 @@ ${messages.prefix}`
|
|||
const suffix = messages.suffix
|
||||
const stopTokens = messages.stopTokens
|
||||
const ret = { prefix, suffix, stopTokens, maxTokens: 300 } as const
|
||||
console.log('ret', ret)
|
||||
return ret
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,553 @@
|
|||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
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 '../../browser/helpers/extractCodeFromResult.js';
|
||||
import { LLMChatMessage, LLMFIMMessage, ModelListParams, OllamaModelResponse, OnError, OnFinalMessage, OnText } from '../../common/llmMessageTypes.js';
|
||||
import { InternalToolInfo, isAToolName, ToolName } from '../../browser/toolsService.js';
|
||||
import { defaultProviderSettings, displayInfoOfProviderName, OptionsOfModelSelection, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js';
|
||||
import { prepareFIMMessage, prepareMessages } from './preprocessLLMMessages.js';
|
||||
import { getModelSelectionState, getModelCapabilities, getProviderCapabilities } from '../../common/modelCapabilities.js';
|
||||
|
||||
|
||||
type InternalCommonMessageParams = {
|
||||
aiInstructions: string;
|
||||
onText: OnText;
|
||||
onFinalMessage: OnFinalMessage;
|
||||
onError: OnError;
|
||||
providerName: ProviderName;
|
||||
settingsOfProvider: SettingsOfProvider;
|
||||
optionsOfModelSelection: OptionsOfModelSelection;
|
||||
modelName: string;
|
||||
_setAborter: (aborter: () => void) => void;
|
||||
}
|
||||
|
||||
type SendChatParams_Internal = InternalCommonMessageParams & { messages: LLMChatMessage[]; tools?: InternalToolInfo[] }
|
||||
type SendFIMParams_Internal = InternalCommonMessageParams & { messages: LLMFIMMessage; }
|
||||
export type ListParams_Internal<ModelResponse> = ModelListParams<ModelResponse>
|
||||
|
||||
|
||||
const invalidApiKeyMessage = (providerName: ProviderName) => `Invalid ${displayInfoOfProviderName(providerName).title} API key.`
|
||||
|
||||
// ------------ OPENAI-COMPATIBLE (HELPERS) ------------
|
||||
const toOpenAICompatibleTool = (toolInfo: InternalToolInfo) => {
|
||||
const { name, description, params, required } = toolInfo
|
||||
return {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: name,
|
||||
description: description,
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: params,
|
||||
required: required,
|
||||
}
|
||||
}
|
||||
} satisfies OpenAI.Chat.Completions.ChatCompletionTool
|
||||
}
|
||||
|
||||
type ToolCallOfIndex = { [index: string]: { name: string, paramsStr: string, id: string } } // type used to stream tool calls as they come in
|
||||
type ToolCallsFrom_ReturnType = { name: ToolName, id: string, paramsStr: string }[] // return type of toolCallsFrom_<PROVIDER>
|
||||
|
||||
const toolCallsFrom_OpenAICompat = (toolCallOfIndex: ToolCallOfIndex): ToolCallsFrom_ReturnType => {
|
||||
return Object.keys(toolCallOfIndex).map(index => {
|
||||
const tool = toolCallOfIndex[index]
|
||||
return isAToolName(tool.name) ? { name: tool.name, id: tool.id, paramsStr: tool.paramsStr } : null
|
||||
}).filter(t => !!t)
|
||||
}
|
||||
|
||||
|
||||
const newOpenAICompatibleSDK = ({ settingsOfProvider, providerName, includeInPayload }: { settingsOfProvider: SettingsOfProvider, providerName: ProviderName, includeInPayload?: { [s: string]: any } }) => {
|
||||
const commonPayloadOpts: ClientOptions = {
|
||||
dangerouslyAllowBrowser: true,
|
||||
...includeInPayload,
|
||||
}
|
||||
if (providerName === 'openAI') {
|
||||
const thisConfig = settingsOfProvider[providerName]
|
||||
return new OpenAI({ apiKey: thisConfig.apiKey, ...commonPayloadOpts })
|
||||
}
|
||||
else if (providerName === 'ollama') {
|
||||
const thisConfig = settingsOfProvider[providerName]
|
||||
return new OpenAI({ baseURL: `${thisConfig.endpoint}/v1`, apiKey: 'noop', ...commonPayloadOpts })
|
||||
}
|
||||
else if (providerName === 'vLLM') {
|
||||
const thisConfig = settingsOfProvider[providerName]
|
||||
return new OpenAI({ baseURL: `${thisConfig.endpoint}/v1`, apiKey: 'noop', ...commonPayloadOpts })
|
||||
}
|
||||
else if (providerName === 'openRouter') {
|
||||
const thisConfig = settingsOfProvider[providerName]
|
||||
return new OpenAI({
|
||||
baseURL: 'https://openrouter.ai/api/v1',
|
||||
apiKey: thisConfig.apiKey,
|
||||
defaultHeaders: {
|
||||
'HTTP-Referer': 'https://voideditor.com', // Optional, for including your app on openrouter.ai rankings.
|
||||
'X-Title': 'Void', // Optional. Shows in rankings on openrouter.ai.
|
||||
},
|
||||
...commonPayloadOpts,
|
||||
})
|
||||
}
|
||||
else if (providerName === 'gemini') {
|
||||
const thisConfig = settingsOfProvider[providerName]
|
||||
return new OpenAI({ baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai', apiKey: thisConfig.apiKey, ...commonPayloadOpts })
|
||||
}
|
||||
else if (providerName === 'deepseek') {
|
||||
const thisConfig = settingsOfProvider[providerName]
|
||||
return new OpenAI({ baseURL: 'https://api.deepseek.com/v1', apiKey: thisConfig.apiKey, ...commonPayloadOpts })
|
||||
}
|
||||
else if (providerName === 'openAICompatible') {
|
||||
const thisConfig = settingsOfProvider[providerName]
|
||||
return new OpenAI({ baseURL: thisConfig.endpoint, apiKey: thisConfig.apiKey, ...commonPayloadOpts })
|
||||
}
|
||||
else if (providerName === 'groq') {
|
||||
const thisConfig = settingsOfProvider[providerName]
|
||||
return new OpenAI({ baseURL: 'https://api.groq.com/openai/v1', apiKey: thisConfig.apiKey, ...commonPayloadOpts })
|
||||
}
|
||||
else if (providerName === 'xAI') {
|
||||
const thisConfig = settingsOfProvider[providerName]
|
||||
return new OpenAI({ baseURL: 'https://api.x.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, }: 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 openai = newOpenAICompatibleSDK({ providerName, settingsOfProvider })
|
||||
openai.completions
|
||||
.create({
|
||||
model: modelName,
|
||||
prompt: messages.prefix,
|
||||
suffix: messages.suffix,
|
||||
stop: messages.stopTokens,
|
||||
max_tokens: messages.maxTokens,
|
||||
})
|
||||
.then(async response => {
|
||||
const fullText = response.choices[0]?.text
|
||||
onFinalMessage({ fullText, });
|
||||
})
|
||||
.catch(error => {
|
||||
if (error instanceof OpenAI.APIError && error.status === 401) { onError({ message: invalidApiKeyMessage(providerName), fullError: error }); }
|
||||
else { onError({ message: error + '', fullError: error }); }
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
const _sendOpenAICompatibleChat = ({ 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, supportsAnthropicContent: false }) // can change supportsAnthropicContent if e.g. OpenRouter starts supporting anthropic extended thinking
|
||||
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, }
|
||||
|
||||
const { needsManualParse: needsManualReasoningParse, nameOfFieldInDelta: nameOfReasoningFieldInDelta } = providerReasoningIOSettings?.output ?? {}
|
||||
const manuallyParseReasoning = needsManualReasoningParse && canIOReasoning && openSourceThinkTags
|
||||
if (manuallyParseReasoning) {
|
||||
onText = extractReasoningOnTextWrapper(onText, openSourceThinkTags)
|
||||
}
|
||||
|
||||
let fullReasoningSoFar = ''
|
||||
let fullTextSoFar = ''
|
||||
const toolCallOfIndex: ToolCallOfIndex = {}
|
||||
openai.chat.completions
|
||||
.create(options)
|
||||
.then(async response => {
|
||||
_setAborter(() => response.controller.abort())
|
||||
// when receive text
|
||||
for await (const chunk of response) {
|
||||
// tool call
|
||||
for (const tool of chunk.choices[0]?.delta?.tool_calls ?? []) {
|
||||
const index = tool.index
|
||||
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 ?? ''
|
||||
}
|
||||
// message
|
||||
const newText = chunk.choices[0]?.delta?.content ?? ''
|
||||
fullTextSoFar += newText
|
||||
|
||||
// reasoning
|
||||
let newReasoning = ''
|
||||
if (nameOfReasoningFieldInDelta) {
|
||||
// @ts-ignore
|
||||
newReasoning = (chunk.choices[0]?.delta?.[nameOfReasoningFieldInDelta] || '') + ''
|
||||
fullReasoningSoFar += newReasoning
|
||||
}
|
||||
|
||||
onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
|
||||
}
|
||||
// on final
|
||||
const toolCalls = toolCallsFrom_OpenAICompat(toolCallOfIndex)
|
||||
if (!fullTextSoFar && !fullReasoningSoFar && toolCalls.length === 0) {
|
||||
onError({ message: 'Void: Response from model was empty.', fullError: null })
|
||||
}
|
||||
else {
|
||||
if (manuallyParseReasoning) {
|
||||
const { fullText, fullReasoning } = extractReasoningOnFinalMessage(fullTextSoFar, openSourceThinkTags)
|
||||
onFinalMessage({ fullText, fullReasoning, toolCalls });
|
||||
} else {
|
||||
onFinalMessage({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, toolCalls });
|
||||
}
|
||||
}
|
||||
})
|
||||
// when error/fail - this catches errors of both .create() and .then(for await)
|
||||
.catch(error => {
|
||||
if (error instanceof OpenAI.APIError && error.status === 401) { onError({ message: invalidApiKeyMessage(providerName), fullError: error }); }
|
||||
else { onError({ message: error + '', fullError: error }); }
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const _openaiCompatibleList = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider, providerName }: ListParams_Internal<OpenAIModel>) => {
|
||||
const onSuccess = ({ models }: { models: OpenAIModel[] }) => {
|
||||
onSuccess_({ models })
|
||||
}
|
||||
const onError = ({ error }: { error: string }) => {
|
||||
onError_({ error })
|
||||
}
|
||||
try {
|
||||
const openai = newOpenAICompatibleSDK({ providerName, settingsOfProvider })
|
||||
openai.models.list()
|
||||
.then(async (response) => {
|
||||
const models: OpenAIModel[] = []
|
||||
models.push(...response.data)
|
||||
while (response.hasNextPage()) {
|
||||
models.push(...(await response.getNextPage()).data)
|
||||
}
|
||||
onSuccess({ models })
|
||||
})
|
||||
.catch((error) => {
|
||||
onError({ error: error + '' })
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
onError({ error: error + '' })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// ------------ ANTHROPIC ------------
|
||||
const toAnthropicTool = (toolInfo: InternalToolInfo) => {
|
||||
const { name, description, params, required } = toolInfo
|
||||
return {
|
||||
name: name,
|
||||
description: description,
|
||||
input_schema: {
|
||||
type: 'object',
|
||||
properties: params,
|
||||
required: required,
|
||||
}
|
||||
} satisfies Anthropic.Messages.Tool
|
||||
}
|
||||
|
||||
const toolCallsFrom_AnthropicContent = (content: Anthropic.Messages.ContentBlock[]): ToolCallsFrom_ReturnType => {
|
||||
return content.map(c => {
|
||||
if (c.type !== 'tool_use') return null
|
||||
if (!isAToolName(c.name)) return null
|
||||
return c.type === 'tool_use' ? { name: c.name, paramsStr: JSON.stringify(c.input), id: c.id } : null
|
||||
}).filter(t => !!t)
|
||||
}
|
||||
|
||||
const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalMessage, onError, settingsOfProvider, optionsOfModelSelection, modelName: modelName_, _setAborter, aiInstructions, tools: tools_ }: SendChatParams_Internal) => {
|
||||
const {
|
||||
modelName,
|
||||
supportsSystemMessage,
|
||||
supportsTools,
|
||||
maxOutputTokens,
|
||||
supportsReasoning,
|
||||
} = getModelCapabilities(providerName, modelName_)
|
||||
const {
|
||||
isReasoningEnabled,
|
||||
reasoningBudget,
|
||||
} = getModelSelectionState(providerName, modelName_, optionsOfModelSelection) // user's modelName_ here
|
||||
|
||||
const { messages, separateSystemMessageStr } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsTools, supportsAnthropicContent: true })
|
||||
|
||||
const thisConfig = settingsOfProvider.anthropic
|
||||
const anthropic = new Anthropic({ apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true });
|
||||
const tools = ((tools_?.length ?? 0) !== 0) ? tools_?.map(tool => toAnthropicTool(tool)) : undefined
|
||||
|
||||
|
||||
const toolsObj: Partial<Anthropic.Messages.MessageStreamParams> = tools ? {
|
||||
tools: tools,
|
||||
tool_choice: { type: 'auto', disable_parallel_tool_use: true } // one tool at a time
|
||||
} : {}
|
||||
|
||||
|
||||
const enableThinking = supportsReasoning && isReasoningEnabled && reasoningBudget
|
||||
const maxTokens = enableThinking ? supportsReasoning.reasoningMaxOutputTokens : maxOutputTokens
|
||||
const thinkingObj: Partial<Anthropic.Messages.MessageStreamParams> = enableThinking ? {
|
||||
thinking: { type: 'enabled', budget_tokens: reasoningBudget } // thinking enabled
|
||||
} : {}
|
||||
|
||||
const stream = anthropic.messages.stream({
|
||||
system: separateSystemMessageStr,
|
||||
messages: messages,
|
||||
model: modelName,
|
||||
max_tokens: maxTokens ?? 4_096, // anthropic requires this
|
||||
...toolsObj,
|
||||
...thinkingObj,
|
||||
})
|
||||
|
||||
// when receive text
|
||||
let fullText = ''
|
||||
let fullReasoning = ''
|
||||
|
||||
// there are no events for tool_use, it comes in at the end
|
||||
stream.on('streamEvent', e => {
|
||||
// start block
|
||||
if (e.type === 'content_block_start') {
|
||||
if (e.content_block.type === 'text') {
|
||||
if (fullText) fullText += '\n\n' // starting a 2nd text block
|
||||
fullText += e.content_block.text
|
||||
onText({ fullText, fullReasoning })
|
||||
}
|
||||
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 })
|
||||
}
|
||||
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 })
|
||||
}
|
||||
}
|
||||
|
||||
// delta
|
||||
else if (e.type === 'content_block_delta') {
|
||||
if (e.delta.type === 'text_delta') {
|
||||
fullText += e.delta.text
|
||||
onText({ fullText, fullReasoning })
|
||||
}
|
||||
else if (e.delta.type === 'thinking_delta') {
|
||||
fullReasoning += e.delta.thinking
|
||||
onText({ fullText, fullReasoning })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 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 })
|
||||
})
|
||||
// on error
|
||||
stream.on('error', (error) => {
|
||||
if (error instanceof Anthropic.APIError && error.status === 401) { onError({ message: invalidApiKeyMessage(providerName), fullError: error }) }
|
||||
else { onError({ message: error + '', fullError: error }) }
|
||||
})
|
||||
_setAborter(() => stream.controller.abort())
|
||||
}
|
||||
|
||||
// // in future, can do tool_use streaming in anthropic, but it's pretty fast even without streaming...
|
||||
// const toolCallOfIndex: { [index: string]: { name: string, args: string } } = {}
|
||||
// stream.on('streamEvent', e => {
|
||||
// if (e.type === 'content_block_start') {
|
||||
// if (e.content_block.type !== 'tool_use') return
|
||||
// const index = e.index
|
||||
// if (!toolCallOfIndex[index]) toolCallOfIndex[index] = { name: '', args: '' }
|
||||
// toolCallOfIndex[index].name += e.content_block.name ?? ''
|
||||
// toolCallOfIndex[index].args += e.content_block.input ?? ''
|
||||
// }
|
||||
// else if (e.type === 'content_block_delta') {
|
||||
// if (e.delta.type !== 'input_json_delta') return
|
||||
// toolCallOfIndex[e.index].args += e.delta.partial_json
|
||||
// }
|
||||
// })
|
||||
|
||||
|
||||
// ------------ OLLAMA ------------
|
||||
const newOllamaSDK = ({ endpoint }: { endpoint: string }) => {
|
||||
// if endpoint is empty, normally ollama will send to 11434, but we want it to fail - the user should type it in
|
||||
if (!endpoint) throw new Error(`Ollama Endpoint was empty (please enter ${defaultProviderSettings.ollama.endpoint} in Void if you want the default url).`)
|
||||
const ollama = new Ollama({ host: endpoint })
|
||||
return ollama
|
||||
}
|
||||
|
||||
const ollamaList = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider }: ListParams_Internal<OllamaModelResponse>) => {
|
||||
const onSuccess = ({ models }: { models: OllamaModelResponse[] }) => {
|
||||
onSuccess_({ models })
|
||||
}
|
||||
const onError = ({ error }: { error: string }) => {
|
||||
onError_({ error })
|
||||
}
|
||||
try {
|
||||
const thisConfig = settingsOfProvider.ollama
|
||||
const ollama = newOllamaSDK({ endpoint: thisConfig.endpoint })
|
||||
ollama.list()
|
||||
.then((response) => {
|
||||
const { models } = response
|
||||
onSuccess({ models })
|
||||
})
|
||||
.catch((error) => {
|
||||
onError({ error: error + '' })
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
onError({ error: error + '' })
|
||||
}
|
||||
}
|
||||
|
||||
const sendOllamaFIM = ({ messages: messages_, onFinalMessage, onError, settingsOfProvider, modelName, aiInstructions, _setAborter }: SendFIMParams_Internal) => {
|
||||
const thisConfig = settingsOfProvider.ollama
|
||||
const ollama = newOllamaSDK({ endpoint: thisConfig.endpoint })
|
||||
|
||||
const messages = prepareFIMMessage({ messages: messages_, aiInstructions, })
|
||||
|
||||
let fullText = ''
|
||||
ollama.generate({
|
||||
model: modelName,
|
||||
prompt: messages.prefix,
|
||||
suffix: messages.suffix,
|
||||
options: {
|
||||
stop: messages.stopTokens,
|
||||
num_predict: messages.maxTokens, // max tokens
|
||||
// repeat_penalty: 1,
|
||||
},
|
||||
raw: true,
|
||||
stream: true, // stream is not necessary but lets us expose the
|
||||
})
|
||||
.then(async stream => {
|
||||
_setAborter(() => stream.abort())
|
||||
for await (const chunk of stream) {
|
||||
const newText = chunk.response
|
||||
fullText += newText
|
||||
}
|
||||
onFinalMessage({ fullText })
|
||||
})
|
||||
// when error/fail
|
||||
.catch((error) => {
|
||||
onError({ message: error + '', fullError: error })
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
type CallFnOfProvider = {
|
||||
[providerName in ProviderName]: {
|
||||
sendChat: (params: SendChatParams_Internal) => void;
|
||||
sendFIM: ((params: SendFIMParams_Internal) => void) | null;
|
||||
list: ((params: ListParams_Internal<any>) => void) | null;
|
||||
}
|
||||
}
|
||||
|
||||
export const sendLLMMessageToProviderImplementation = {
|
||||
anthropic: {
|
||||
sendChat: sendAnthropicChat,
|
||||
sendFIM: null,
|
||||
list: null,
|
||||
},
|
||||
openAI: {
|
||||
sendChat: (params) => _sendOpenAICompatibleChat(params),
|
||||
sendFIM: null,
|
||||
list: null,
|
||||
},
|
||||
xAI: {
|
||||
sendChat: (params) => _sendOpenAICompatibleChat(params),
|
||||
sendFIM: null,
|
||||
list: null,
|
||||
},
|
||||
gemini: {
|
||||
sendChat: (params) => _sendOpenAICompatibleChat(params),
|
||||
sendFIM: null,
|
||||
list: null,
|
||||
},
|
||||
ollama: {
|
||||
sendChat: (params) => _sendOpenAICompatibleChat(params),
|
||||
sendFIM: sendOllamaFIM,
|
||||
list: ollamaList,
|
||||
},
|
||||
openAICompatible: {
|
||||
sendChat: (params) => _sendOpenAICompatibleChat(params), // using openai's SDK is not ideal (your implementation might not do tools, reasoning, FIM etc correctly), talk to us for a custom integration
|
||||
sendFIM: (params) => _sendOpenAICompatibleFIM(params),
|
||||
list: null,
|
||||
},
|
||||
openRouter: {
|
||||
sendChat: (params) => _sendOpenAICompatibleChat(params),
|
||||
sendFIM: (params) => _sendOpenAICompatibleFIM(params),
|
||||
list: null,
|
||||
},
|
||||
vLLM: {
|
||||
sendChat: (params) => _sendOpenAICompatibleChat(params),
|
||||
sendFIM: (params) => _sendOpenAICompatibleFIM(params),
|
||||
list: (params) => _openaiCompatibleList(params),
|
||||
},
|
||||
deepseek: {
|
||||
sendChat: (params) => _sendOpenAICompatibleChat(params),
|
||||
sendFIM: null,
|
||||
list: null,
|
||||
},
|
||||
groq: {
|
||||
sendChat: (params) => _sendOpenAICompatibleChat(params),
|
||||
sendFIM: null,
|
||||
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
|
||||
<file_sep>
|
||||
<fim_prefix>
|
||||
{{ .Prompt }}<fim_suffix>{{ .Suffix }}<fim_middle>
|
||||
<|end_of_text|>
|
||||
|
||||
codegemma https://ollama.com/library/codegemma:2b/blobs/48d9a8140749
|
||||
<|fim_prefix|>{{ .Prompt }}<|fim_suffix|>{{ .Suffix }}<|fim_middle|>
|
||||
|
||||
*/
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
import { SendLLMMessageParams, OnText, OnFinalMessage, OnError } from '../../common/llmMessageTypes.js';
|
||||
import { IMetricsService } from '../../common/metricsService.js';
|
||||
import { displayInfoOfProviderName } from '../../common/voidSettingsTypes.js';
|
||||
import { sendLLMMessageToProviderImplementation } from './MODELS.js';
|
||||
import { sendLLMMessageToProviderImplementation } from './sendLLMMessage.impl.js';
|
||||
|
||||
|
||||
export const sendLLMMessage = ({
|
||||
|
|
@ -19,6 +19,7 @@ export const sendLLMMessage = ({
|
|||
abortRef: abortRef_,
|
||||
logging: { loggingName },
|
||||
settingsOfProvider,
|
||||
optionsOfModelSelection,
|
||||
providerName,
|
||||
modelName,
|
||||
tools,
|
||||
|
|
@ -104,12 +105,12 @@ export const sendLLMMessage = ({
|
|||
}
|
||||
const { sendFIM, sendChat } = implementation
|
||||
if (messagesType === 'chatMessages') {
|
||||
sendChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions, tools })
|
||||
sendChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, optionsOfModelSelection, modelName, _setAborter, providerName, aiInstructions, tools })
|
||||
return
|
||||
}
|
||||
if (messagesType === 'FIMMessage') {
|
||||
if (sendFIM) {
|
||||
sendFIM({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions })
|
||||
sendFIM({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, optionsOfModelSelection, modelName, _setAborter, providerName, aiInstructions })
|
||||
return
|
||||
}
|
||||
onError({ message: `Error: This provider does not support Autocomplete yet.`, fullError: null })
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { Emitter, Event } from '../../../../base/common/event.js';
|
|||
import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, MainSendLLMMessageParams, AbortRef, SendLLMMessageParams, MainLLMMessageAbortParams, ModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, OllamaModelResponse, VLLMModelResponse, MainModelListParams, } from '../common/llmMessageTypes.js';
|
||||
import { sendLLMMessage } from './llmMessage/sendLLMMessage.js'
|
||||
import { IMetricsService } from '../common/metricsService.js';
|
||||
import { sendLLMMessageToProviderImplementation } from './llmMessage/MODELS.js';
|
||||
import { sendLLMMessageToProviderImplementation } from './llmMessage/sendLLMMessage.impl.js';
|
||||
|
||||
// NODE IMPLEMENTATION - calls actual sendLLMMessage() and returns listeners to it
|
||||
|
||||
|
|
|
|||
|
|
@ -192,8 +192,9 @@ import './contrib/notebook/browser/notebook.contribution.js';
|
|||
import './contrib/speech/browser/speech.contribution.js';
|
||||
|
||||
// Chat
|
||||
// import './contrib/chat/browser/chat.contribution.js'; // Void - remove vscode built-in chat
|
||||
// import './contrib/inlineChat/browser/inlineChat.contribution.js';
|
||||
// Void - this is still registered to avoid console errors, we just commented it out in chatParticipant.contribution.ts
|
||||
import './contrib/chat/browser/chat.contribution.js';
|
||||
import './contrib/inlineChat/browser/inlineChat.contribution.js';
|
||||
|
||||
// Interactive
|
||||
import './contrib/interactive/browser/interactive.contribution.js';
|
||||
|
|
|
|||
Loading…
Reference in a new issue