From 858b6f6a91429d8c88b89cc905bcd39626e2ac4b Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Wed, 19 Feb 2025 00:10:44 -0800 Subject: [PATCH 01/41] file service read --- .../void/browser/MarkerCheckService.ts | 16 +++ .../void/browser/autocompleteService.ts | 5 +- .../contrib/void/browser/chatThreadService.ts | 8 +- .../contrib/void/browser/editCodeService.ts | 7 +- .../contrib/void/browser/prompt/prompts.ts | 15 ++- .../contrib/void/common/toolsService.ts | 5 +- .../contrib/void/common/voidFileService.ts | 109 ++++++++++++++++++ .../llmMessage/postprocessToolCalls.ts | 2 - 8 files changed, 142 insertions(+), 25 deletions(-) create mode 100644 src/vs/workbench/contrib/void/common/voidFileService.ts diff --git a/src/vs/workbench/contrib/void/browser/MarkerCheckService.ts b/src/vs/workbench/contrib/void/browser/MarkerCheckService.ts index f41c0513..7cdcb1e2 100644 --- a/src/vs/workbench/contrib/void/browser/MarkerCheckService.ts +++ b/src/vs/workbench/contrib/void/browser/MarkerCheckService.ts @@ -12,6 +12,7 @@ import { ITextModelService } from '../../../../editor/common/services/resolverSe import { Range } from '../../../../editor/common/core/range.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { CodeActionContext, CodeActionTriggerType } from '../../../../editor/common/languages.js'; +import { URI } from '../../../../base/common/uri.js'; export interface IMarkerCheckService { readonly _serviceBrand: undefined; @@ -99,6 +100,21 @@ class MarkerCheckService extends Disposable implements IMarkerCheckService { } + + + fixErrorsInFiles(uris: URI[], contextSoFar: []) { + const allMarkers = this._markerService.read(); + + + // check errors in files + + + // give LLM errors in files + + + + } + // private _onMarkersChanged = (changedResources: readonly URI[]): void => { // for (const resource of changedResources) { // const markers = this._markerService.read({ resource }); diff --git a/src/vs/workbench/contrib/void/browser/autocompleteService.ts b/src/vs/workbench/contrib/void/browser/autocompleteService.ts index aa8902f3..5fc8ac76 100644 --- a/src/vs/workbench/contrib/void/browser/autocompleteService.ts +++ b/src/vs/workbench/contrib/void/browser/autocompleteService.ts @@ -16,9 +16,9 @@ import { isCodeEditor } from '../../../../editor/browser/editorBrowser.js'; import { EditorResourceAccessor } from '../../../common/editor.js'; import { IModelService } from '../../../../editor/common/services/model.js'; import { extractCodeFromRegular } from './helpers/extractCodeFromResult.js'; -import { isWindows } from '../../../../base/common/platform.js'; import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; import { ILLMMessageService } from '../common/llmMessageService.js'; +import { _ln, allLinebreakSymbols } from '../common/voidFileService.js'; // import { IContextGatheringService } from './contextGatheringService.js'; // The extension this was called from is here - https://github.com/voideditor/void/blob/autocomplete/extensions/void/src/extension/extension.ts @@ -415,9 +415,6 @@ const toInlineCompletions = ({ autocompletionMatchup, autocompletion, prefixAndS // } -const allLinebreakSymbols = ['\r\n', '\n'] -const _ln = isWindows ? allLinebreakSymbols[0] : allLinebreakSymbols[1] - type PrefixAndSuffixInfo = { prefix: string, suffix: string, prefixLines: string[], suffixLines: string[], prefixToTheLeftOfCursor: string, suffixToTheRightOfCursor: string } const getPrefixAndSuffixInfo = (model: ITextModel, position: Position): PrefixAndSuffixInfo => { diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index cc875a79..8bbab4ad 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -12,12 +12,11 @@ import { URI } from '../../../../base/common/uri.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { IRange } from '../../../../editor/common/core/range.js'; import { ILLMMessageService } from '../common/llmMessageService.js'; -import { IModelService } from '../../../../editor/common/services/model.js'; import { chat_userMessageContent, chat_systemMessage, chat_userMessageContentWithAllFilesToo as chat_userMessageContentWithAllFiles, chat_selectionsString } from './prompt/prompts.js'; -import { IFileService } from '../../../../platform/files/common/files.js'; import { InternalToolInfo, IToolsService, ToolCallReturnType, ToolFns, ToolName, voidTools } from '../common/toolsService.js'; import { toLLMChatMessage } from '../common/llmMessageTypes.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; +import { IVoidFileService } from '../common/voidFileService.js'; const findLastIndex = (arr: T[], condition: (t: T) => boolean): number => { @@ -189,8 +188,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { constructor( @IStorageService private readonly _storageService: IStorageService, - @IModelService private readonly _modelService: IModelService, - @IFileService private readonly _fileService: IFileService, + @IVoidFileService private readonly _voidFileService: IVoidFileService, @ILLMMessageService private readonly _llmMessageService: ILLMMessageService, @IToolsService private readonly _toolsService: IToolsService, @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, @@ -358,7 +356,7 @@ 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._modelService, this._fileService) + const selectionsStr = await chat_selectionsString(prevSelns, currSelns, this._voidFileService) const userMessageFullContent = chat_userMessageContentWithAllFiles(userMessageContent, selectionsStr) const userHistoryElt: ChatMessage = { role: 'user', content: userMessageContent, displayContent: instructions, selections: currSelns, state: defaultMessageState } diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index e3a5d998..02e3fc23 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -41,8 +41,7 @@ import { ICommandService } from '../../../../platform/commands/common/commands.j import { ILLMMessageService } from '../common/llmMessageService.js'; import { LLMChatMessage, errorDetails } from '../common/llmMessageTypes.js'; import { IMetricsService } from '../common/metricsService.js'; -import { VSReadFile } from './helpers/readFile.js'; -import { IFileService } from '../../../../platform/files/common/files.js'; +import { IVoidFileService } from '../common/voidFileService.js'; const configOfBG = (color: Color) => { return { dark: color, light: color, hcDark: color, hcLight: color, } @@ -255,7 +254,7 @@ class EditCodeService extends Disposable implements IEditCodeService { @IMetricsService private readonly _metricsService: IMetricsService, @INotificationService private readonly _notificationService: INotificationService, @ICommandService private readonly _commandService: ICommandService, - @IFileService private readonly _fileService: IFileService, + @IVoidFileService private readonly _voidFileService: IVoidFileService, ) { super(); @@ -1184,7 +1183,7 @@ class EditCodeService extends Disposable implements IEditCodeService { const uri = uri_ // generate search/replace block text - const origFileContents = await VSReadFile(uri, this._modelService, this._fileService) + const origFileContents = await this._voidFileService.readFile(uri) if (origFileContents === null) return diff --git a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts index f04fcb5c..90e01d50 100644 --- a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts @@ -7,10 +7,9 @@ import { URI } from '../../../../../base/common/uri.js'; import { filenameToVscodeLanguage } from '../helpers/detectLanguage.js'; import { CodeSelection, StagingSelectionItem, FileSelection } from '../chatThreadService.js'; -import { VSReadFile } from '../helpers/readFile.js'; import { IModelService } from '../../../../../editor/common/services/model.js'; import { os } from '../helpers/systemInfo.js'; -import { IFileService } from '../../../../../platform/files/common/files.js'; +import { IVoidFileService } from '../../common/voidFileService.js'; // this is just for ease of readability @@ -169,10 +168,10 @@ ${tripleTick[1]} } const failToReadStr = 'Could not read content. This file may have been deleted. If you expected content here, you can tell the user about this as they might not know.' -const stringifyFileSelections = async (fileSelections: FileSelection[], modelService: IModelService, fileService: IFileService) => { +const stringifyFileSelections = async (fileSelections: FileSelection[], voidFileService: IVoidFileService) => { if (fileSelections.length === 0) return null const fileSlns: FileSelnLocal[] = await Promise.all(fileSelections.map(async (sel) => { - const content = await VSReadFile(sel.fileURI, modelService, fileService) ?? failToReadStr + const content = await voidFileService.readFile(sel.fileURI) ?? failToReadStr return { ...sel, content } })) return fileSlns.map(sel => stringifyFileSelection(sel)).join('\n') @@ -195,7 +194,7 @@ export const chat_userMessageContent = async (instructions: string, currSelns: S return str; }; -export const chat_selectionsString = async (prevSelns: StagingSelectionItem[] | null, currSelns: StagingSelectionItem[] | null, modelService: IModelService, fileService: IFileService) => { +export const chat_selectionsString = async (prevSelns: StagingSelectionItem[] | null, currSelns: StagingSelectionItem[] | null, voidFileService: IVoidFileService) => { // ADD IN FILES AT TOP const allSelections = [...currSelns || [], ...prevSelns || []] @@ -220,7 +219,7 @@ export const chat_selectionsString = async (prevSelns: StagingSelectionItem[] | } } - const filesStr = await stringifyFileSelections(fileSelections, modelService, fileService) + const filesStr = await stringifyFileSelections(fileSelections, voidFileService) const selnsStr = stringifyCodeSelections(codeSelections) @@ -297,12 +296,12 @@ For example, if the user is asking you to "make this variable a better name", ma - Make sure you give enough context in the code block to apply the changes to the correct location in the code` -export const aiRegex_computeReplacementsForFile_userMessage = async ({ searchClause, replaceClause, fileURI, modelService, fileService }: { searchClause: string, replaceClause: string, fileURI: URI, modelService: IModelService, fileService: IFileService }) => { +export const aiRegex_computeReplacementsForFile_userMessage = async ({ searchClause, replaceClause, fileURI, voidFileService }: { searchClause: string, replaceClause: string, fileURI: URI, modelService: IModelService, voidFileService: IVoidFileService }) => { // we may want to do this in batches const fileSelection: FileSelection = { type: 'File', fileURI, selectionStr: null, range: null } - const file = await stringifyFileSelections([fileSelection], modelService, fileService) + const file = await stringifyFileSelections([fileSelection], voidFileService) return `\ ## FILE diff --git a/src/vs/workbench/contrib/void/common/toolsService.ts b/src/vs/workbench/contrib/void/common/toolsService.ts index 3b609f60..fadbf333 100644 --- a/src/vs/workbench/contrib/void/common/toolsService.ts +++ b/src/vs/workbench/contrib/void/common/toolsService.ts @@ -5,9 +5,9 @@ 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 { VSReadFile } from '../../../../workbench/contrib/void/browser/helpers/readFile.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 @@ -196,6 +196,7 @@ export class ToolsService implements IToolsService { @IWorkspaceContextService workspaceContextService: IWorkspaceContextService, @ISearchService searchService: ISearchService, @IInstantiationService instantiationService: IInstantiationService, + @IVoidFileService voidFileService: IVoidFileService, ) { const queryBuilder = instantiationService.createInstance(QueryBuilder); @@ -208,7 +209,7 @@ export class ToolsService implements IToolsService { const uri = validateURI(uriStr) const pageNumber = validatePageNum(pageNumberUnknown) - const readFileContents = await VSReadFile(uri, modelService, fileService) + const readFileContents = await voidFileService.readFile(uri) const fromIdx = MAX_FILE_CHARS_PAGE * (pageNumber - 1) const toIdx = MAX_FILE_CHARS_PAGE * pageNumber - 1 diff --git a/src/vs/workbench/contrib/void/common/voidFileService.ts b/src/vs/workbench/contrib/void/common/voidFileService.ts new file mode 100644 index 00000000..668f1869 --- /dev/null +++ b/src/vs/workbench/contrib/void/common/voidFileService.ts @@ -0,0 +1,109 @@ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * 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'; +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; + + readFile(uri: URI, range?: { startLineNumber: number, endLineNumber: number }): Promise; + +} + +export const IVoidFileService = createDecorator('VoidFileService'); + +// implemented by calling channel +export class VoidFileService implements IVoidFileService { + readonly _serviceBrand: undefined; + + constructor( + @IModelService private readonly modelService: IModelService, + @IFileService private readonly fileService: IFileService, + ) { + + } + + readFile = async (uri: URI, range?: { startLineNumber: number, endLineNumber: number }): Promise => { + + // attempt to read the model + const modelResult = await this._readModel(uri, range); + if (modelResult) return modelResult; + + // if no model, read the raw file + const fileResult = await this._readFileRaw(uri, range); + if (fileResult) return fileResult; + + return ''; + } + + _readFileRaw = async (uri: URI, range?: { startLineNumber: number, endLineNumber: number }): Promise => { + + 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(); + + + } catch (e) { + return null; + } + } + + + _readModel = async (uri: URI, range?: { startLineNumber: number, endLineNumber: number }): Promise => { + + // read saved model (sometimes null if the user reloads application) + let model = this.modelService.getModel(uri); + + // check all opened models for the same `fsPath` + if (!model) { + const models = this.modelService.getModels(); + for (const m of models) { + if (m.uri.fsPath === uri.fsPath) { + model = m + break; + } + } + } + + // if still not found, return + if (!model) { return null } + + // if range, read it + if (range) { + return model.getValueInRange({ + startLineNumber: range.startLineNumber, + endLineNumber: range.endLineNumber, + startColumn: 1, + endColumn: Number.MAX_VALUE + }, EndOfLinePreference.LF); + } else { + return model.getValue(EndOfLinePreference.LF) + } + + } + +} + +registerSingleton(IVoidFileService, VoidFileService, InstantiationType.Eager); diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/postprocessToolCalls.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/postprocessToolCalls.ts index 2feeeb80..aee52dcb 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/postprocessToolCalls.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/postprocessToolCalls.ts @@ -1,7 +1,5 @@ import { ToolName, toolNames } from '../../common/toolsService.js'; - - const toolNamesSet = new Set(toolNames) export const isAToolName = (toolName: string): toolName is ToolName => { From 6a44d668db6222febf074a587c01f462e92d0010 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Wed, 19 Feb 2025 00:16:23 -0800 Subject: [PATCH 02/41] + --- src/vs/workbench/contrib/void/common/toolsService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/void/common/toolsService.ts b/src/vs/workbench/contrib/void/common/toolsService.ts index fadbf333..9d759199 100644 --- a/src/vs/workbench/contrib/void/common/toolsService.ts +++ b/src/vs/workbench/contrib/void/common/toolsService.ts @@ -162,6 +162,7 @@ const validateQueryStr = (queryStr: unknown) => { } +// 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) @@ -225,7 +226,6 @@ export class ToolsService implements IToolsService { const uri = validateURI(uriStr) const pageNumber = validatePageNum(pageNumberUnknown) - // TODO!!!! check to make sure in workspace const [treeStr, hasNextPage] = await generateDirectoryTreeMd(fileService, uri, pageNumber) return [treeStr, hasNextPage] }, From 0f26ee2288481a92348febc39d3f6d1de6d42ff4 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Wed, 19 Feb 2025 00:47:03 -0800 Subject: [PATCH 03/41] misc fixes --- src/vs/workbench/contrib/void/browser/MarkerCheckService.ts | 2 +- src/vs/workbench/contrib/void/common/toolsService.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/MarkerCheckService.ts b/src/vs/workbench/contrib/void/browser/MarkerCheckService.ts index 7cdcb1e2..4ea36c4c 100644 --- a/src/vs/workbench/contrib/void/browser/MarkerCheckService.ts +++ b/src/vs/workbench/contrib/void/browser/MarkerCheckService.ts @@ -103,7 +103,7 @@ class MarkerCheckService extends Disposable implements IMarkerCheckService { fixErrorsInFiles(uris: URI[], contextSoFar: []) { - const allMarkers = this._markerService.read(); + // const allMarkers = this._markerService.read(); // check errors in files diff --git a/src/vs/workbench/contrib/void/common/toolsService.ts b/src/vs/workbench/contrib/void/common/toolsService.ts index 9d759199..41ab059b 100644 --- a/src/vs/workbench/contrib/void/common/toolsService.ts +++ b/src/vs/workbench/contrib/void/common/toolsService.ts @@ -1,6 +1,5 @@ import { CancellationToken } from '../../../../base/common/cancellation.js' import { URI } from '../../../../base/common/uri.js' -import { IModelService } from '../../../../editor/common/services/model.js' import { IFileService } from '../../../../platform/files/common/files.js' import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js' import { createDecorator, IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js' @@ -113,7 +112,9 @@ async function generateDirectoryTreeMd(fileService: IFileService, rootURI: URI, const stat = await fileService.resolve(uri, { resolveMetadata: false }); // we might want to say where symlink links to - if ((depth === 0 && pageNumber === 1) || depth !== 0) + if (depth === 0 && pageNumber !== 1) + output += '' + else output += `${indentation(depth, isLast)}${stat.name}${stat.isDirectory ? '/' : ''}${stat.isSymbolicLink ? ` (symbolic link)` : ''}\n`; // list children @@ -193,7 +194,6 @@ export class ToolsService implements IToolsService { constructor( @IFileService fileService: IFileService, - @IModelService modelService: IModelService, @IWorkspaceContextService workspaceContextService: IWorkspaceContextService, @ISearchService searchService: ISearchService, @IInstantiationService instantiationService: IInstantiationService, From 62c2622ced39d4839017056c1ce02233bf5613fa Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Wed, 19 Feb 2025 17:57:40 -0800 Subject: [PATCH 04/41] Void's settings --- .../contrib/void/browser/editCodeService.ts | 2 +- .../react/src/markdown/ChatMarkdownRender.tsx | 18 ++++++++++++++++++ .../react/src/void-settings-tsx/Settings.tsx | 2 +- .../contrib/void/browser/sidebarActions.ts | 2 +- .../contrib/void/browser/voidSettingsPane.ts | 6 +++--- .../contrib/void/common/llmMessageService.ts | 6 +++--- .../contrib/void/common/toolsService.ts | 3 ++- .../electron-main/llmMessage/sendLLMMessage.ts | 2 +- 8 files changed, 30 insertions(+), 11 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 02e3fc23..c7629f89 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -1590,7 +1590,7 @@ class EditCodeService extends Disposable implements IEditCodeService { secondary: [{ id: 'void.onerror.opensettings', enabled: true, - label: 'Open Void settings', + label: `Open Void's settings`, tooltip: '', class: undefined, run: () => { this._commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID) } diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index 3e77a6df..7a9953b7 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -201,6 +201,24 @@ const RenderToken = ({ token, nested = false, noSpace = false, chatMessageLocati ))} ) + // attempt at indentation + // return ( + // + // {t.items.map((item, index) => ( + //
  • + // {item.task && ( + // + // )} + // + // + // + //
  • + // ))} + //
    + // ) } if (t.type === "paragraph") { diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx index 0cfdef04..fd612c43 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx @@ -624,7 +624,7 @@ export const Settings = () => {
    -

    Void Settings

    +

    {`Void's Settings`}

    {/* separator */}
    diff --git a/src/vs/workbench/contrib/void/browser/sidebarActions.ts b/src/vs/workbench/contrib/void/browser/sidebarActions.ts index 2e64c53f..39eb8381 100644 --- a/src/vs/workbench/contrib/void/browser/sidebarActions.ts +++ b/src/vs/workbench/contrib/void/browser/sidebarActions.ts @@ -241,7 +241,7 @@ registerAction2(class extends Action2 { constructor() { super({ id: 'void.settingsAction', - title: 'Void Settings', + title: `Void's Settings`, icon: { id: 'settings-gear' }, menu: [{ id: MenuId.ViewTitle, group: 'navigation', when: ContextKeyExpr.equals('view', VOID_VIEW_ID), }] }); diff --git a/src/vs/workbench/contrib/void/browser/voidSettingsPane.ts b/src/vs/workbench/contrib/void/browser/voidSettingsPane.ts index 0fd8ce2e..d5e99d57 100644 --- a/src/vs/workbench/contrib/void/browser/voidSettingsPane.ts +++ b/src/vs/workbench/contrib/void/browser/voidSettingsPane.ts @@ -49,7 +49,7 @@ class VoidSettingsInput extends EditorInput { } override getName(): string { - return nls.localize('voidSettingsInputsName', 'Void Settings'); + return nls.localize('voidSettingsInputsName', 'Void\'s Settings'); } override getIcon() { @@ -112,7 +112,7 @@ class VoidSettingsPane extends EditorPane { // register Settings pane Registry.as(EditorExtensions.EditorPane).registerEditorPane( - EditorPaneDescriptor.create(VoidSettingsPane, VoidSettingsPane.ID, nls.localize('VoidSettingsPane', "Void Settings Pane")), + EditorPaneDescriptor.create(VoidSettingsPane, VoidSettingsPane.ID, nls.localize('VoidSettingsPane', "Void\'s Settings Pane")), [new SyncDescriptor(VoidSettingsInput)] ); @@ -202,7 +202,7 @@ MenuRegistry.appendMenuItem(MenuId.GlobalActivity, { group: '0_command', command: { id: VOID_TOGGLE_SETTINGS_ACTION_ID, - title: nls.localize('voidSettings', "Void Settings") + title: nls.localize('voidSettings', "Void\'s Settings") }, order: 1 }); diff --git a/src/vs/workbench/contrib/void/common/llmMessageService.ts b/src/vs/workbench/contrib/void/common/llmMessageService.ts index 314031d4..bb6cf09c 100644 --- a/src/vs/workbench/contrib/void/common/llmMessageService.ts +++ b/src/vs/workbench/contrib/void/common/llmMessageService.ts @@ -99,15 +99,15 @@ export class LLMMessageService extends Disposable implements ILLMMessageService let message: string if (isDisabled === 'addProvider' || isDisabled === 'providerNotAutoDetected') - message = `Please add a provider in Void Settings.` + message = `Please add a provider in Void's Settings.` else if (isDisabled === 'addModel') message = `Please add a model.` else if (isDisabled === 'needToEnableModel') message = `Please enable a model.` else if (isDisabled === 'notFilledIn') - message = `Please fill in Void Settings${modelSelection !== null ? ` for ${displayInfoOfProviderName(modelSelection.providerName).title}` : ''}.` + message = `Please fill in Void's Settings${modelSelection !== null ? ` for ${displayInfoOfProviderName(modelSelection.providerName).title}` : ''}.` else - message = 'Please add a provider in Void Settings.' + message = `Please add a provider in Void's Settings.` onError({ message, fullError: null }) return null diff --git a/src/vs/workbench/contrib/void/common/toolsService.ts b/src/vs/workbench/contrib/void/common/toolsService.ts index 41ab059b..dbdd0e15 100644 --- a/src/vs/workbench/contrib/void/common/toolsService.ts +++ b/src/vs/workbench/contrib/void/common/toolsService.ts @@ -23,7 +23,6 @@ export type InternalToolInfo = { required: string[], // required paramNames } -// helper 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).' }, } @@ -166,6 +165,7 @@ const validateQueryStr = (queryStr: unknown) => { // 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 } @@ -270,6 +270,7 @@ export class ToolsService implements IToolsService { return [URIs, hasNextPage] }, + } diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts index 8e29bff4..0a182aec 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts @@ -74,7 +74,7 @@ export const sendLLMMessage = ({ // handle failed to fetch errors, which give 0 information by design if (error === 'TypeError: fetch failed') - error = `Failed to fetch from ${displayInfoOfProviderName(providerName).title}. This likely means you specified the wrong endpoint in Void Settings, or your local model provider like Ollama is powered off.` + error = `Failed to fetch from ${displayInfoOfProviderName(providerName).title}. This likely means you specified the wrong endpoint in Void's Settings, or your local model provider like Ollama is powered off.` captureLLMEvent(`${loggingName} - Error`, { error }) onError_({ message: error, fullError }) From e14aab632d282fe0ea7338b72b66f57e0d4cf654 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 20 Feb 2025 01:21:28 -0800 Subject: [PATCH 05/41] multiple find/replace blocks almost work. only 1 diffarea per find/replace application --- .../parts/editor/editorGroupWatermark.ts | 2 +- .../contrib/void/browser/chatThreadService.ts | 5 +- .../contrib/void/browser/editCodeService.ts | 339 ++++++++++-------- 3 files changed, 188 insertions(+), 158 deletions(-) diff --git a/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts b/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts index 42f4a7a7..8c0ebe8b 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts @@ -270,7 +270,7 @@ export class EditorGroupWatermark extends Disposable { const keys3 = this.keybindingService.lookupKeybinding('workbench.action.openGlobalKeybindings'); const button3 = append(recentsBox, $('button')); - button3.textContent = 'Void Settings' + button3.textContent = `Void's Settings` button3.style.display = 'block' button3.style.marginLeft = 'auto' button3.style.marginRight = 'auto' diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 8bbab4ad..05ef405b 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -72,8 +72,7 @@ export type ChatMessage = stagingSelections: StagingSelectionItem[]; isBeingEdited: boolean; } - } - | { + } | { role: 'assistant'; content: string | null; // content received from LLM - allowed to be '', will be replaced with (empty) displayContent: string | null; // content displayed to user (this is the same as content for now) - allowed to be '', will be ignored @@ -85,7 +84,7 @@ type UserMessageState = UserMessageType['state'] export const defaultMessageState: UserMessageState = { stagingSelections: [], - isBeingEdited: false + isBeingEdited: false, } // a 'thread' means a chat message history diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index c7629f89..e142e519 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -39,7 +39,7 @@ 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, errorDetails } from '../common/llmMessageTypes.js'; +import { LLMChatMessage, OnError, errorDetails } from '../common/llmMessageTypes.js'; import { IMetricsService } from '../common/metricsService.js'; import { IVoidFileService } from '../common/voidFileService.js'; @@ -65,6 +65,8 @@ registerColor('void.sweepIdxBG', configOfBG(sweepIdxBG), '', true); + + const getLeadingWhitespacePx = (editor: ICodeEditor, startLine: number): number => { const model = editor.getModel(); @@ -102,6 +104,25 @@ const getLeadingWhitespacePx = (editor: ICodeEditor, startLine: number): number +// finds block.orig in fileContents and return its range in file +const findTextInCode = (text: string, fileContents: string) => { + console.log('TEXTTTT', JSON.stringify(text)) + const idx = fileContents.indexOf(text) + if (idx === -1) return 'Not found' as const + const lastIdx = fileContents.lastIndexOf(text) + if (lastIdx !== idx) return 'Not unique' as const + console.log('TEXTTTT22222', JSON.stringify(fileContents.substring(0, idx))) + const startLine = fileContents.substring(0, idx).split('\n').length + const numLines = text.split('\n').length + const endLine = startLine + numLines - 1 + console.log('startline', startLine) + console.log('endline', endLine) + return [startLine, endLine] +} + + + + export type StartApplyingOpts = { from: 'QuickEdit'; type: 'rewrite'; @@ -328,6 +349,29 @@ class EditCodeService extends Disposable implements IEditCodeService { } + + + private _notifyError = (e: Parameters[0]) => { + const details = errorDetails(e.fullError) + this._notificationService.notify({ + severity: Severity.Warning, + message: `Void Error: ${e.message}`, + actions: { + secondary: [{ + id: 'void.onerror.opensettings', + enabled: true, + label: `Open Void's settings`, + tooltip: '', + class: undefined, + run: () => { this._commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID) } + }] + }, + source: details ? `(Hold ${isMacintosh ? 'Option' : 'Alt'} to hover) - ${details}\n\nIf this persists, feel free to [report](https://github.com/voideditor/void/issues/new) it.` : undefined + }) + } + + + // highlight the region private _addLineDecoration = (model: ITextModel | null, startLine: number, endLine: number, className: string, options?: Partial) => { if (model === null) return @@ -1000,18 +1044,11 @@ class EditCodeService extends Disposable implements IEditCodeService { // @throttle(100) - private _writeStreamedDiffZoneLLMText(diffZone: DiffZone, llmText: string, deltaText: string, latestMutable: StreamLocationMutable) { + private _writeStreamedDiffZoneLLMText(uri: URI, originalCode: string, llmTextSoFar: string, deltaText: string, latestMutable: StreamLocationMutable) { // ----------- 1. Write the new code to the document ----------- // figure out where to highlight based on where the AI is in the stream right now, use the last diff to figure that out - const uri = diffZone._URI - const computedDiffs = findDiffs(diffZone.originalCode, llmText) - - // should always be in streaming state here - if (!diffZone._streamState.isStreaming) { - console.error('DiffZone was not in streaming state on _writeDiffZoneLLMText') - return - } + const computedDiffs = findDiffs(originalCode, llmTextSoFar) // if streaming, use diffs to figure out where to write new code // these are two different coordinate systems - new and old line number @@ -1036,8 +1073,6 @@ class EditCodeService extends Disposable implements IEditCodeService { throw new Error(`Void: diff.type not recognized on: ${lastDiff}`) } - - // at the start, add a newline between the stream and originalCode to make reasoning easier if (!latestMutable.addedSplitYet) { this._writeText(uri, '\n', @@ -1066,18 +1101,14 @@ class EditCodeService extends Disposable implements IEditCodeService { ) } else if (latestMutable.originalCodeStartLine > originalCodeStartLine) { - this._writeText(uri, '\n' + diffZone.originalCode.split('\n').slice((originalCodeStartLine - 1), (latestMutable.originalCodeStartLine - 1) - 1 + 1).join('\n'), + this._writeText(uri, '\n' + originalCode.split('\n').slice((originalCodeStartLine - 1), (latestMutable.originalCodeStartLine - 1) - 1 + 1).join('\n'), { startLineNumber: latestMutable.line, startColumn: latestMutable.col, endLineNumber: latestMutable.line, endColumn: latestMutable.col }, { shouldRealignDiffAreas: true } ) } latestMutable.originalCodeStartLine = originalCodeStartLine - // add diffZone.startLine to convert to right coordinate system (line in file, not in diffarea) - diffZone._streamState.line = (diffZone.startLine - 1) + newCodeEndLine - - return computedDiffs - + return newCodeEndLine } @@ -1145,7 +1176,7 @@ class EditCodeService extends Disposable implements IEditCodeService { public startApplying(opts: StartApplyingOpts) { if (opts.type === 'rewrite') { - const addedDiffZone = this._initializeRewriteStream(opts) + const addedDiffZone = this._initializeWriteoverStream(opts) return addedDiffZone?.diffareaid } @@ -1183,41 +1214,55 @@ class EditCodeService extends Disposable implements IEditCodeService { const uri = uri_ // generate search/replace block text - const origFileContents = await this._voidFileService.readFile(uri) - if (origFileContents === null) return + const originalFileCode = await this._voidFileService.readFile(uri) + if (originalFileCode === null) return + const numLines = this._getNumLines(uri) + if (numLines === null) return - // // reject all diffZones on this URI, adding to history (there can't possibly be overlap after this) - // this.removeDiffAreas({ uri, behavior: 'reject', removeCtrlKs: true }) + // reject all diffZones on this URI, adding to history (there can't possibly be overlap after this) + this.removeDiffAreas({ uri, behavior: 'reject', removeCtrlKs: true }) - const userMessageContent = searchReplace_userMessage({ originalCode: origFileContents, applyStr: applyStr }) + const startLine = 1 + const endLine = numLines + + const userMessageContent = searchReplace_userMessage({ originalCode: originalFileCode, applyStr: applyStr }) const messages: LLMChatMessage[] = [ { role: 'system', content: searchReplace_systemMessage }, { role: 'user', content: userMessageContent }, ] + let streamRequestIdRef: { current: string | null } = { current: null } - const diffareaidOfBlockNum: number[] = [] - const diffAreaOriginalLines: [number, number][] = [] + let { onFinishEdit } = this._addToHistory(uri) + + // TODO replace these with whatever block we're on initially if already started + const infoOfBlockNum: { + originalLines: [number, number], // 1-indexed + finalStartLine: number, // 1-indexed + originalCode: string, + }[] = [] - // TODO replace all these with whatever block we're on initially if already started - let latestStreamLocationMutable: StreamLocationMutable | null = null - let currStreamingBlockNum = 0 let oldBlocks: ExtractedSearchReplaceBlock[] = [] - // find block.orig in fileContents and return its range in file - const findTextInCode = (text: string, fileContents: string) => { - const idx = fileContents.indexOf(text) - if (idx === -1) return 'Not found' as const - const lastIdx = fileContents.lastIndexOf(text) - if (lastIdx !== idx) return 'Not unique' as const - const startLine = fileContents.substring(0, idx).split('\n').length - const numLines = text.split('\n').length - const endLine = startLine + numLines - 1 - return [startLine, endLine] + const adding: Omit = { + type: 'DiffZone', + originalCode: originalFileCode, + startLine, + endLine, + _URI: uri, + _streamState: { + isStreaming: true, + streamRequestIdRef, + line: startLine, + }, + _diffOfId: {}, // added later + _removeStylesFns: new Set(), } + const diffZone = this._addDiffArea(adding) + this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) + this._onDidAddOrDeleteDiffZones.fire({ uri }) - let { onFinishEdit } = this._addToHistory(uri) const revertAndContinueHistory = () => { this._undoHistory(uri) @@ -1225,90 +1270,55 @@ class EditCodeService extends Disposable implements IEditCodeService { onFinishEdit = onFinishEdit_ } - const onDone = (errorMessage: false | string) => { - for (const blockNum in diffareaidOfBlockNum) { - const diffareaid = diffareaidOfBlockNum[blockNum] - const diffZone = this.diffAreaOfId[diffareaid] - if (diffZone?.type !== 'DiffZone') continue - diffZone._streamState = { isStreaming: false, } - this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) - } - this._refreshStylesAndDiffsInURI(uri) - if (errorMessage) { - this._notificationService.info(`Void had an error when running Apply: ${errorMessage}.\nIf this persists, feel free to [report](https://github.com/voideditor/void/issues/new) this error.`) - this._metricsService.capture('Error - Apply', { errorMessage }) - this._undoHistory(uri) - } - onFinishEdit() - } - - const onNewBlockStart = (blockNum: number, block: ExtractedSearchReplaceBlock): { errorStartingBlock?: undefined } | { errorStartingBlock: string } => { - console.log('STARTING BLOCK', JSON.stringify(block, null, 2)) - - - const foundInCode = findTextInCode(block.orig, origFileContents) + const findTextInCodeWithAdjustedOffset = (blockNum: number, block: ExtractedSearchReplaceBlock): string | { originalLines: [number, number], currentLines: [number, number] } => { + const foundInCode = findTextInCode(block.orig, originalFileCode) if (typeof foundInCode === 'string') { console.log('Apply error:', foundInCode, '; trying again.') - return { errorStartingBlock: foundInCode } + return foundInCode } const [originalStart, originalEnd] = foundInCode + // compute line offset if there were changes in the past let lineOffset = 0 - // compute line offset given multiple changes for (let i = 0; i < blockNum; i += 1) { - const [diffAreaOriginalStart, diffAreaOriginalEnd] = diffAreaOriginalLines[i] - console.log('ROIGGINAL!!!', diffAreaOriginalStart, diffAreaOriginalEnd) - if (diffAreaOriginalStart > originalEnd) continue + const { originalLines: [otherBlockOriginalStart, otherBlockOriginalEnd], } = infoOfBlockNum[i] + const finalCode = block.final - const diffareaid = diffareaidOfBlockNum[i] - const diffArea = this.diffAreaOfId[diffareaid] + if (otherBlockOriginalStart > originalEnd) continue + if (finalCode === null) continue - - const numNewLines = diffArea.endLine - diffArea.startLine - const numOldLines = diffAreaOriginalEnd - diffAreaOriginalStart - console.log('NUM NEW', numNewLines, numOldLines) + const numNewLines = finalCode.split('\n').length + const numOldLines = otherBlockOriginalEnd - otherBlockOriginalStart + 1 lineOffset += numNewLines - numOldLines } - const startLine = originalStart + lineOffset - const endLine = originalEnd + lineOffset - console.log('adding to', startLine, endLine) - - const adding: Omit = { - type: 'DiffZone', - originalCode: block.orig, - startLine, - endLine, - _URI: uri, - _streamState: { - isStreaming: true, - streamRequestIdRef, - line: startLine, - }, - _diffOfId: {}, // added later - _removeStylesFns: new Set(), + return { + originalLines: [originalStart, originalEnd], + currentLines: [originalStart + lineOffset, originalEnd + lineOffset], } - const diffZone = this._addDiffArea(adding) - this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) - this._onDidAddOrDeleteDiffZones.fire({ uri }) - - diffareaidOfBlockNum.push(diffZone.diffareaid) - diffAreaOriginalLines.push([originalStart, originalEnd]) - - latestStreamLocationMutable = { line: diffZone.startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 } - return { errorStartingBlock: undefined } } + let latestStreamLocationMutable: StreamLocationMutable | null = null + const onDone = () => { + diffZone._streamState = { isStreaming: false, } + this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) + this._refreshStylesAndDiffsInURI(uri) + onFinishEdit() + } + + // refresh now in case onText takes a while to get 1st message + this._refreshStylesAndDiffsInURI(uri) let shouldSendAnotherMessage = true let nMessagesSent = 0 // this generates >>>>>>> ORIGINAL <<<<<<< REPLACE blocks and and simultaneously applies it + let currStreamingBlockNum = 0 while (shouldSendAnotherMessage) { shouldSendAnotherMessage = false nMessagesSent += 1 @@ -1324,49 +1334,73 @@ class EditCodeService extends Disposable implements IEditCodeService { for (let blockNum = currStreamingBlockNum; blockNum < blocks.length; blockNum += 1) { const block = blocks[blockNum] - if (block.state === 'done') - currStreamingBlockNum = blockNum + // if a block is done, finish it + if (block.state === 'done') { + console.log('FINISHING BLOCK') - if (block.state === 'writingOriginal') // must be done writing original + const { finalStartLine } = infoOfBlockNum[blockNum] + const numLines = block.final.split('\n').length + + this._writeText(uri, block.final, + { startLineNumber: finalStartLine, startColumn: 1, endLineNumber: finalStartLine + numLines, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed + { shouldRealignDiffAreas: true } + ) + currStreamingBlockNum = blockNum + 1 + } + + // must be done writing original to stream code + if (block.state === 'writingOriginal') continue - // if this is the first time we're seeing this block, add it as a diffarea - if (!(blockNum in diffareaidOfBlockNum)) { - console.log('FULLTEXT!!!!!\n', fullText) - const { errorStartingBlock } = onNewBlockStart(blockNum, block) + // if this is the first time we're seeing this block, add it as a blocknum + if (!(blockNum in infoOfBlockNum)) { + console.log('----FULLTEXT!!!!!----\n', blockNum, fullText) - if (errorStartingBlock) { + const pos = findTextInCodeWithAdjustedOffset(blockNum, block) + console.log('OFFSET', pos) + + if (typeof pos === 'string') { + const errorStartingBlock = pos console.log('ERROR STARTING BLOCK SPOT!!!!!', errorStartingBlock) - const errMsgForLLM = errorStartingBlock === '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.' : errorStartingBlock === '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.' : '' - messages.push( { role: 'assistant', content: fullText }, // latest output { role: 'user', content: errMsgForLLM } // user explanation of what's wrong ) if (streamRequestIdRef.current) this._llmMessageService.abort(streamRequestIdRef.current) - shouldSendAnotherMessage = true revertAndContinueHistory() - return + continue } + infoOfBlockNum.push({ + originalLines: pos.originalLines, + finalStartLine: pos.currentLines[0], + originalCode: block.orig, + }) + + latestStreamLocationMutable = { line: diffZone.startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 } + } const deltaFinalText = block.final.substring((oldBlocks[blockNum]?.final ?? '').length, Infinity) oldBlocks = blocks - // write new text to diffarea - const diffareaid = diffareaidOfBlockNum[blockNum] - const diffZone = this.diffAreaOfId[diffareaid] - if (diffZone?.type !== 'DiffZone') continue - if (!latestStreamLocationMutable) continue - this._writeStreamedDiffZoneLLMText(diffZone, block.final, deltaFinalText, latestStreamLocationMutable) + + // should always be in streaming state here + if (!diffZone._streamState.isStreaming) { + console.error('DiffZone was not in streaming state in _initializeSearchAndReplaceStream') + continue + } + + const latestEndLine = this._writeStreamedDiffZoneLLMText(uri, diffZone.originalCode, block.final, deltaFinalText, latestStreamLocationMutable) + diffZone._streamState.line = (diffZone.startLine - 1) + latestEndLine // change coordinate systems from originalCode to full file + } // end for this._refreshStylesAndDiffsInURI(uri) @@ -1379,25 +1413,39 @@ class EditCodeService extends Disposable implements IEditCodeService { const blocks = extractSearchReplaceBlocks(fullText) if (blocks.length === 0) { - this._notificationService.info(`Void: When running Apply, your model didn't output any changes we recognized. You might need to use a smarter model for Apply.`) + this._notificationService.info(`Void: When running Apply, your model didn't output any changes that Void recognized. You might need to use a smarter model for Apply.`) } - for (let blockNum = 0; blockNum < blocks.length; blockNum += 1) { - const block = blocks[blockNum] - const diffareaid = diffareaidOfBlockNum[blockNum] - const diffZone = this.diffAreaOfId[diffareaid] - if (diffZone?.type !== 'DiffZone') continue + // writeover the whole file + let newCode = originalFileCode + for (let blockNum = infoOfBlockNum.length - 1; blockNum >= 0; blockNum -= 1) { + const { originalLines } = infoOfBlockNum[blockNum] + const finalCode = blocks[blockNum].final - this._writeText(uri, block.final, - { startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed + if (finalCode === null) continue + + const [originalStart, originalEnd] = originalLines + 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(false) + + onDone() }, onError: (e) => { - console.log('ERROR in SearchReplace:', e.message) - onDone(e.message) + this._notifyError(e) + onDone() + this._undoHistory(uri) }, }) @@ -1409,7 +1457,7 @@ class EditCodeService extends Disposable implements IEditCodeService { - private _initializeRewriteStream(opts: StartApplyingOpts): DiffZone | undefined { + private _initializeWriteoverStream(opts: StartApplyingOpts): DiffZone | undefined { const { from } = opts @@ -1516,7 +1564,7 @@ class EditCodeService extends Disposable implements IEditCodeService { else { throw new Error(`featureName ${from} is invalid`) } - const onDone = (hadError: boolean) => { + const onDone = () => { diffZone._streamState = { isStreaming: false, } this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) @@ -1528,11 +1576,6 @@ class EditCodeService extends Disposable implements IEditCodeService { } this._refreshStylesAndDiffsInURI(uri) onFinishEdit() - - // if had error, revert! - if (hadError) { - this._undoHistory(diffZone._URI) - } } // refresh now in case onText takes a while to get 1st message @@ -1566,7 +1609,9 @@ class EditCodeService extends Disposable implements IEditCodeService { fullText += prevIgnoredSuffix + newText // full text, including ```, etc const [croppedText, deltaCroppedText, croppedSuffix] = extractText(fullText, newText.length) - this._writeStreamedDiffZoneLLMText(diffZone, croppedText, deltaCroppedText, latestStreamInfoMutable) + const latestEndLine = this._writeStreamedDiffZoneLLMText(uri, originalCode, croppedText, deltaCroppedText, latestStreamInfoMutable) + diffZone._streamState.line = (diffZone.startLine - 1) + latestEndLine // change coordinate systems from originalCode to full file + this._refreshStylesAndDiffsInURI(uri) prevIgnoredSuffix = croppedSuffix @@ -1579,26 +1624,12 @@ class EditCodeService extends Disposable implements IEditCodeService { { startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed { shouldRealignDiffAreas: true } ) - onDone(false) + onDone() }, onError: (e) => { - const details = errorDetails(e.fullError) - this._notificationService.notify({ - severity: Severity.Warning, - message: `Void Error: ${e.message}`, - actions: { - secondary: [{ - id: 'void.onerror.opensettings', - enabled: true, - label: `Open Void's settings`, - tooltip: '', - class: undefined, - run: () => { this._commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID) } - }] - }, - source: details ? `(Hold ${isMacintosh ? 'Option' : 'Alt'} to hover) - ${details}` : undefined - }) - onDone(true) + this._notifyError(e) + onDone() + this._undoHistory(uri) }, }) From 7711c74ef016017704805e457bd03fdb6dbc2288 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 20 Feb 2025 01:21:56 -0800 Subject: [PATCH 06/41] reorder --- .../contrib/void/browser/editCodeService.ts | 373 +++++++++--------- 1 file changed, 188 insertions(+), 185 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index e142e519..e16ef078 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -1207,6 +1207,194 @@ class EditCodeService extends Disposable implements IEditCodeService { } + + + + + private _initializeWriteoverStream(opts: StartApplyingOpts): DiffZone | undefined { + + const { from } = opts + + let startLine: number + let endLine: number + let uri: URI + + if (from === 'ClickApply') { + + const uri_ = this._getActiveEditorURI() + if (!uri_) return + uri = uri_ + + // reject all diffZones on this URI, adding to history (there can't possibly be overlap after this) + this.removeDiffAreas({ uri, behavior: 'reject', removeCtrlKs: true }) + + // in ctrl+L the start and end lines are the full document + const numLines = this._getNumLines(uri) + if (numLines === null) return + startLine = 1 + endLine = numLines + + } + else if (from === 'QuickEdit') { + const { diffareaid } = opts + const ctrlKZone = this.diffAreaOfId[diffareaid] + if (ctrlKZone.type !== 'CtrlKZone') return + + const { startLine: startLine_, endLine: endLine_, _URI } = ctrlKZone + uri = _URI + startLine = startLine_ + endLine = endLine_ + } + else { + throw new Error(`Void: diff.type not recognized on: ${from}`) + } + + const currentFileStr = this._readURI(uri) + if (currentFileStr === null) return + const originalCode = currentFileStr.split('\n').slice((startLine - 1), (endLine - 1) + 1).join('\n') + + + let streamRequestIdRef: { current: string | null } = { current: null } + + + // add to history + const { onFinishEdit } = this._addToHistory(uri) + + // __TODO__ let users customize modelFimTags + const quickEditFIMTags = defaultQuickEditFimTags + + const adding: Omit = { + type: 'DiffZone', + originalCode, + startLine, + endLine, + _URI: uri, + _streamState: { + isStreaming: true, + streamRequestIdRef, + line: startLine, + }, + _diffOfId: {}, // added later + _removeStylesFns: new Set(), + } + const diffZone = this._addDiffArea(adding) + this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) + this._onDidAddOrDeleteDiffZones.fire({ uri }) + + if (from === 'QuickEdit') { + const { diffareaid } = opts + const ctrlKZone = this.diffAreaOfId[diffareaid] + if (ctrlKZone.type !== 'CtrlKZone') return + + ctrlKZone._linkedStreamingDiffZone = diffZone.diffareaid + } + + // now handle messages + let messages: LLMChatMessage[] + + if (from === 'ClickApply') { + const userContent = rewriteCode_userMessage({ originalCode, applyStr: opts.applyStr, uri }) + messages = [ + { role: 'system', content: rewriteCode_systemMessage, }, + { role: 'user', content: userContent, } + ] + } + else if (from === 'QuickEdit') { + const { diffareaid } = opts + const ctrlKZone = this.diffAreaOfId[diffareaid] + if (ctrlKZone.type !== 'CtrlKZone') return + const { _mountInfo } = ctrlKZone + const instructions = _mountInfo?.textAreaRef.current?.value ?? '' + + const { prefix, suffix } = voidPrefixAndSuffix({ fullFileStr: currentFileStr, startLine, endLine }) + const language = filenameToVscodeLanguage(uri.fsPath) ?? '' + const userContent = ctrlKStream_userMessage({ selection: originalCode, instructions: instructions, prefix, suffix, isOllamaFIM: false, fimTags: quickEditFIMTags, language }) + // type: 'messages', + messages = [ + { role: 'system', content: ctrlKStream_systemMessage({ quickEditFIMTags: quickEditFIMTags }), }, + { role: 'user', content: userContent, } + ] + } + else { throw new Error(`featureName ${from} is invalid`) } + + + const onDone = () => { + diffZone._streamState = { isStreaming: false, } + this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) + + if (from === 'QuickEdit') { + const ctrlKZone = this.diffAreaOfId[opts.diffareaid] as CtrlKZone + + ctrlKZone._linkedStreamingDiffZone = null + this._deleteCtrlKZone(ctrlKZone) + } + this._refreshStylesAndDiffsInURI(uri) + onFinishEdit() + } + + // refresh now in case onText takes a while to get 1st message + this._refreshStylesAndDiffsInURI(uri) + + + const extractText = (fullText: string, recentlyAddedTextLen: number) => { + if (from === 'QuickEdit') { + return extractCodeFromFIM({ text: fullText, recentlyAddedTextLen, midTag: quickEditFIMTags.midTag }) + } + else if (from === 'ClickApply') { + return extractCodeFromRegular({ text: fullText, recentlyAddedTextLen }) + } + throw 1 + } + + const latestStreamInfoMutable: StreamLocationMutable = { line: diffZone.startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 } + + // state used in onText: + let fullText = '' + let prevIgnoredSuffix = '' + + streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({ + messagesType: 'chatMessages', + useProviderFor: opts.from === 'ClickApply' ? 'Apply' : 'Ctrl+K', + logging: { loggingName: `startApplying - ${from}` }, + messages, + onText: ({ newText: newText_ }) => { + + const newText = prevIgnoredSuffix + newText_ // add the previously ignored suffix because it's no longer the suffix! + fullText += prevIgnoredSuffix + newText // full text, including ```, etc + + const [croppedText, deltaCroppedText, croppedSuffix] = extractText(fullText, newText.length) + const latestEndLine = this._writeStreamedDiffZoneLLMText(uri, originalCode, croppedText, deltaCroppedText, latestStreamInfoMutable) + diffZone._streamState.line = (diffZone.startLine - 1) + latestEndLine // change coordinate systems from originalCode to full file + + this._refreshStylesAndDiffsInURI(uri) + + 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) + }, + + }) + + return diffZone + + } + + + + private async _initializeSearchAndReplaceStream({ applyStr }: { applyStr: string }) { const uri_ = this._getActiveEditorURI() @@ -1456,191 +1644,6 @@ class EditCodeService extends Disposable implements IEditCodeService { - - private _initializeWriteoverStream(opts: StartApplyingOpts): DiffZone | undefined { - - const { from } = opts - - let startLine: number - let endLine: number - let uri: URI - - if (from === 'ClickApply') { - - const uri_ = this._getActiveEditorURI() - if (!uri_) return - uri = uri_ - - // reject all diffZones on this URI, adding to history (there can't possibly be overlap after this) - this.removeDiffAreas({ uri, behavior: 'reject', removeCtrlKs: true }) - - // in ctrl+L the start and end lines are the full document - const numLines = this._getNumLines(uri) - if (numLines === null) return - startLine = 1 - endLine = numLines - - } - else if (from === 'QuickEdit') { - const { diffareaid } = opts - const ctrlKZone = this.diffAreaOfId[diffareaid] - if (ctrlKZone.type !== 'CtrlKZone') return - - const { startLine: startLine_, endLine: endLine_, _URI } = ctrlKZone - uri = _URI - startLine = startLine_ - endLine = endLine_ - } - else { - throw new Error(`Void: diff.type not recognized on: ${from}`) - } - - const currentFileStr = this._readURI(uri) - if (currentFileStr === null) return - const originalCode = currentFileStr.split('\n').slice((startLine - 1), (endLine - 1) + 1).join('\n') - - - let streamRequestIdRef: { current: string | null } = { current: null } - - - // add to history - const { onFinishEdit } = this._addToHistory(uri) - - // __TODO__ let users customize modelFimTags - const quickEditFIMTags = defaultQuickEditFimTags - - const adding: Omit = { - type: 'DiffZone', - originalCode, - startLine, - endLine, - _URI: uri, - _streamState: { - isStreaming: true, - streamRequestIdRef, - line: startLine, - }, - _diffOfId: {}, // added later - _removeStylesFns: new Set(), - } - const diffZone = this._addDiffArea(adding) - this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) - this._onDidAddOrDeleteDiffZones.fire({ uri }) - - if (from === 'QuickEdit') { - const { diffareaid } = opts - const ctrlKZone = this.diffAreaOfId[diffareaid] - if (ctrlKZone.type !== 'CtrlKZone') return - - ctrlKZone._linkedStreamingDiffZone = diffZone.diffareaid - } - - // now handle messages - let messages: LLMChatMessage[] - - if (from === 'ClickApply') { - const userContent = rewriteCode_userMessage({ originalCode, applyStr: opts.applyStr, uri }) - messages = [ - { role: 'system', content: rewriteCode_systemMessage, }, - { role: 'user', content: userContent, } - ] - } - else if (from === 'QuickEdit') { - const { diffareaid } = opts - const ctrlKZone = this.diffAreaOfId[diffareaid] - if (ctrlKZone.type !== 'CtrlKZone') return - const { _mountInfo } = ctrlKZone - const instructions = _mountInfo?.textAreaRef.current?.value ?? '' - - const { prefix, suffix } = voidPrefixAndSuffix({ fullFileStr: currentFileStr, startLine, endLine }) - const language = filenameToVscodeLanguage(uri.fsPath) ?? '' - const userContent = ctrlKStream_userMessage({ selection: originalCode, instructions: instructions, prefix, suffix, isOllamaFIM: false, fimTags: quickEditFIMTags, language }) - // type: 'messages', - messages = [ - { role: 'system', content: ctrlKStream_systemMessage({ quickEditFIMTags: quickEditFIMTags }), }, - { role: 'user', content: userContent, } - ] - } - else { throw new Error(`featureName ${from} is invalid`) } - - - const onDone = () => { - diffZone._streamState = { isStreaming: false, } - this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) - - if (from === 'QuickEdit') { - const ctrlKZone = this.diffAreaOfId[opts.diffareaid] as CtrlKZone - - ctrlKZone._linkedStreamingDiffZone = null - this._deleteCtrlKZone(ctrlKZone) - } - this._refreshStylesAndDiffsInURI(uri) - onFinishEdit() - } - - // refresh now in case onText takes a while to get 1st message - this._refreshStylesAndDiffsInURI(uri) - - - const extractText = (fullText: string, recentlyAddedTextLen: number) => { - if (from === 'QuickEdit') { - return extractCodeFromFIM({ text: fullText, recentlyAddedTextLen, midTag: quickEditFIMTags.midTag }) - } - else if (from === 'ClickApply') { - return extractCodeFromRegular({ text: fullText, recentlyAddedTextLen }) - } - throw 1 - } - - const latestStreamInfoMutable: StreamLocationMutable = { line: diffZone.startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 } - - // state used in onText: - let fullText = '' - let prevIgnoredSuffix = '' - - streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({ - messagesType: 'chatMessages', - useProviderFor: opts.from === 'ClickApply' ? 'Apply' : 'Ctrl+K', - logging: { loggingName: `startApplying - ${from}` }, - messages, - onText: ({ newText: newText_ }) => { - - const newText = prevIgnoredSuffix + newText_ // add the previously ignored suffix because it's no longer the suffix! - fullText += prevIgnoredSuffix + newText // full text, including ```, etc - - const [croppedText, deltaCroppedText, croppedSuffix] = extractText(fullText, newText.length) - const latestEndLine = this._writeStreamedDiffZoneLLMText(uri, originalCode, croppedText, deltaCroppedText, latestStreamInfoMutable) - diffZone._streamState.line = (diffZone.startLine - 1) + latestEndLine // change coordinate systems from originalCode to full file - - this._refreshStylesAndDiffsInURI(uri) - - 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) - }, - - }) - - return diffZone - - } - - - - private _stopIfStreaming(diffZone: DiffZone) { const uri = diffZone._URI From 970f0bdb9d2e725d5efe168d3317d854028f409c Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 20 Feb 2025 01:28:46 -0800 Subject: [PATCH 07/41] revert "Void's" on the button --- src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts b/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts index 8c0ebe8b..4dce7e6c 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts @@ -270,7 +270,7 @@ export class EditorGroupWatermark extends Disposable { const keys3 = this.keybindingService.lookupKeybinding('workbench.action.openGlobalKeybindings'); const button3 = append(recentsBox, $('button')); - button3.textContent = `Void's Settings` + button3.textContent = `Void Settings` button3.style.display = 'block' button3.style.marginLeft = 'auto' button3.style.marginRight = 'auto' From 33d80bed809e7651ec3328a39caf8175e12e171f Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 20 Feb 2025 17:25:53 -0800 Subject: [PATCH 08/41] progress --- .../contrib/void/browser/editCodeService.ts | 198 ++++++++++-------- 1 file changed, 115 insertions(+), 83 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index e16ef078..de920242 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -106,17 +106,13 @@ const getLeadingWhitespacePx = (editor: ICodeEditor, startLine: number): number // finds block.orig in fileContents and return its range in file const findTextInCode = (text: string, fileContents: string) => { - console.log('TEXTTTT', JSON.stringify(text)) const idx = fileContents.indexOf(text) if (idx === -1) return 'Not found' as const const lastIdx = fileContents.lastIndexOf(text) if (lastIdx !== idx) return 'Not unique' as const - console.log('TEXTTTT22222', JSON.stringify(fileContents.substring(0, idx))) const startLine = fileContents.substring(0, idx).split('\n').length const numLines = text.split('\n').length const endLine = startLine + numLines - 1 - console.log('startline', startLine) - console.log('endline', endLine) return [startLine, endLine] } @@ -1046,29 +1042,31 @@ class EditCodeService extends Disposable implements IEditCodeService { // @throttle(100) private _writeStreamedDiffZoneLLMText(uri: URI, originalCode: string, llmTextSoFar: string, deltaText: string, latestMutable: StreamLocationMutable) { + let numNewLines = 0 + // ----------- 1. Write the new code to the document ----------- // figure out where to highlight based on where the AI is in the stream right now, use the last diff to figure that out const computedDiffs = findDiffs(originalCode, llmTextSoFar) // if streaming, use diffs to figure out where to write new code // these are two different coordinate systems - new and old line number - let newCodeEndLine: number // get file[diffArea.startLine...newFileEndLine] with line=newFileEndLine highlighted - let originalCodeStartLine: number // get original[oldStartingPoint...] (line in the original code, so starts at 1) + let endLineInLlmTextSoFar: number // get file[diffArea.startLine...newFileEndLine] with line=newFileEndLine highlighted + let startLineInOriginalCode: number // get original[oldStartingPoint...] (line in the original code, so starts at 1) const lastDiff = computedDiffs.pop() if (!lastDiff) { // console.log('!lastDiff') // if the writing is identical so far, display no changes - originalCodeStartLine = 1 - newCodeEndLine = 1 + startLineInOriginalCode = 1 + endLineInLlmTextSoFar = 1 } else { - originalCodeStartLine = lastDiff.originalStartLine + startLineInOriginalCode = lastDiff.originalStartLine if (lastDiff.type === 'insertion' || lastDiff.type === 'edit') - newCodeEndLine = lastDiff.endLine + endLineInLlmTextSoFar = lastDiff.endLine else if (lastDiff.type === 'deletion') - newCodeEndLine = lastDiff.startLine + endLineInLlmTextSoFar = lastDiff.startLine else throw new Error(`Void: diff.type not recognized on: ${lastDiff}`) } @@ -1080,6 +1078,7 @@ class EditCodeService extends Disposable implements IEditCodeService { { shouldRealignDiffAreas: true } ) latestMutable.addedSplitYet = true + numNewLines += 1 } // insert deltaText at latest line and col @@ -1087,28 +1086,33 @@ class EditCodeService extends Disposable implements IEditCodeService { { startLineNumber: latestMutable.line, startColumn: latestMutable.col, endLineNumber: latestMutable.line, endColumn: latestMutable.col }, { shouldRealignDiffAreas: true } ) - latestMutable.line += deltaText.split('\n').length - 1 + const deltaNumNewLines = deltaText.split('\n').length - 1 + latestMutable.line += deltaNumNewLines const lastNewlineIdx = deltaText.lastIndexOf('\n') latestMutable.col = lastNewlineIdx === -1 ? latestMutable.col + deltaText.length : deltaText.length - lastNewlineIdx + numNewLines += deltaNumNewLines // delete or insert to get original up to speed - if (latestMutable.originalCodeStartLine < originalCodeStartLine) { + if (latestMutable.originalCodeStartLine < startLineInOriginalCode) { // moved up, delete - const numLinesDeleted = originalCodeStartLine - latestMutable.originalCodeStartLine + const numLinesDeleted = startLineInOriginalCode - latestMutable.originalCodeStartLine this._writeText(uri, '', { startLineNumber: latestMutable.line, startColumn: latestMutable.col, endLineNumber: latestMutable.line + numLinesDeleted, endColumn: Number.MAX_SAFE_INTEGER, }, { shouldRealignDiffAreas: true } ) + numNewLines -= numLinesDeleted } - else if (latestMutable.originalCodeStartLine > originalCodeStartLine) { - this._writeText(uri, '\n' + originalCode.split('\n').slice((originalCodeStartLine - 1), (latestMutable.originalCodeStartLine - 1) - 1 + 1).join('\n'), + else if (latestMutable.originalCodeStartLine > startLineInOriginalCode) { + const newText = '\n' + originalCode.split('\n').slice((startLineInOriginalCode - 1), (latestMutable.originalCodeStartLine - 1) - 1 + 1).join('\n') + this._writeText(uri, newText, { startLineNumber: latestMutable.line, startColumn: latestMutable.col, endLineNumber: latestMutable.line, endColumn: latestMutable.col }, { shouldRealignDiffAreas: true } ) + numNewLines += newText.split('\n').length - 1 } - latestMutable.originalCodeStartLine = originalCodeStartLine + latestMutable.originalCodeStartLine = startLineInOriginalCode - return newCodeEndLine + return { endLineInLlmTextSoFar, numNewLines } } @@ -1363,8 +1367,8 @@ class EditCodeService extends Disposable implements IEditCodeService { fullText += prevIgnoredSuffix + newText // full text, including ```, etc const [croppedText, deltaCroppedText, croppedSuffix] = extractText(fullText, newText.length) - const latestEndLine = this._writeStreamedDiffZoneLLMText(uri, originalCode, croppedText, deltaCroppedText, latestStreamInfoMutable) - diffZone._streamState.line = (diffZone.startLine - 1) + latestEndLine // change coordinate systems from originalCode to full file + 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) @@ -1425,9 +1429,9 @@ class EditCodeService extends Disposable implements IEditCodeService { let { onFinishEdit } = this._addToHistory(uri) // TODO replace these with whatever block we're on initially if already started - const infoOfBlockNum: { - originalLines: [number, number], // 1-indexed - finalStartLine: number, // 1-indexed + const infoOfAddedBlockNum: { + originalBounds: [number, number], // 1-indexed + currentBounds: [number, number], // 1-indexed originalCode: string, }[] = [] @@ -1459,7 +1463,7 @@ class EditCodeService extends Disposable implements IEditCodeService { } - const findTextInCodeWithAdjustedOffset = (blockNum: number, block: ExtractedSearchReplaceBlock): string | { originalLines: [number, number], currentLines: [number, number] } => { + const findTextInCodeWithAdjustedOffset = (blockNum: number, block: ExtractedSearchReplaceBlock): string | { originalBounds: [number, number], currentBounds: [number, number] } => { const foundInCode = findTextInCode(block.orig, originalFileCode) if (typeof foundInCode === 'string') { console.log('Apply error:', foundInCode, '; trying again.') @@ -1470,7 +1474,7 @@ class EditCodeService extends Disposable implements IEditCodeService { // compute line offset if there were changes in the past let lineOffset = 0 for (let i = 0; i < blockNum; i += 1) { - const { originalLines: [otherBlockOriginalStart, otherBlockOriginalEnd], } = infoOfBlockNum[i] + const { originalBounds: [otherBlockOriginalStart, otherBlockOriginalEnd], } = infoOfAddedBlockNum[i] const finalCode = block.final if (otherBlockOriginalStart > originalEnd) continue @@ -1483,8 +1487,8 @@ class EditCodeService extends Disposable implements IEditCodeService { } return { - originalLines: [originalStart, originalEnd], - currentLines: [originalStart + lineOffset, originalEnd + lineOffset], + originalBounds: [originalStart, originalEnd], + currentBounds: [originalStart + lineOffset, originalEnd + lineOffset], } } @@ -1503,6 +1507,9 @@ class EditCodeService extends Disposable implements IEditCodeService { // refresh now in case onText takes a while to get 1st message this._refreshStylesAndDiffsInURI(uri) + + let shouldUpdateOrigStreamPos = true + let shouldSendAnotherMessage = true let nMessagesSent = 0 // this generates >>>>>>> ORIGINAL <<<<<<< REPLACE blocks and and simultaneously applies it @@ -1518,34 +1525,31 @@ class EditCodeService extends Disposable implements IEditCodeService { messages, onText: ({ fullText }) => { const blocks = extractSearchReplaceBlocks(fullText) + if (blocks.length === 2) return for (let blockNum = currStreamingBlockNum; blockNum < blocks.length; blockNum += 1) { const block = blocks[blockNum] - // if a block is done, finish it - if (block.state === 'done') { - console.log('FINISHING BLOCK') - - const { finalStartLine } = infoOfBlockNum[blockNum] - const numLines = block.final.split('\n').length - - this._writeText(uri, block.final, - { startLineNumber: finalStartLine, startColumn: 1, endLineNumber: finalStartLine + numLines, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed - { shouldRealignDiffAreas: true } - ) - currStreamingBlockNum = blockNum + 1 - } - // must be done writing original to stream code - if (block.state === 'writingOriginal') + if (block.state === 'writingOriginal') { + // update stream state + if (shouldUpdateOrigStreamPos && block.orig.trim().length >= 20) { + console.log('or') + const startingAtLine = latestStreamLocationMutable?.line ?? 1 // dont go backwards if already have a stream line + const blockOrigLines = block.orig.split('\n') + const idx = originalFileCode.indexOf(blockOrigLines.slice(startingAtLine, Infinity).join('\n'), (startingAtLine - 1)) + diffZone._streamState.line = originalFileCode.substring(0, idx).split('\n').length + shouldUpdateOrigStreamPos = false + } continue + } + shouldUpdateOrigStreamPos = true + // if this is the first time we're seeing this block, add it as a blocknum - if (!(blockNum in infoOfBlockNum)) { - console.log('----FULLTEXT!!!!!----\n', blockNum, fullText) + if (!(blockNum in infoOfAddedBlockNum)) { const pos = findTextInCodeWithAdjustedOffset(blockNum, block) - console.log('OFFSET', pos) if (typeof pos === 'string') { const errorStartingBlock = pos @@ -1565,70 +1569,98 @@ class EditCodeService extends Disposable implements IEditCodeService { continue } - infoOfBlockNum.push({ - originalLines: pos.originalLines, - finalStartLine: pos.currentLines[0], + console.log('orig bounds', pos.originalBounds) + console.log('curr bounds', pos.currentBounds) + + infoOfAddedBlockNum.push({ + originalBounds: [...pos.originalBounds], + currentBounds: [...pos.currentBounds], originalCode: block.orig, }) - latestStreamLocationMutable = { line: diffZone.startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 } + latestStreamLocationMutable = { line: pos.currentBounds[0], addedSplitYet: false, col: 1, originalCodeStartLine: 1 } + + console.log('latestStreamLocation', latestStreamLocationMutable) } - const deltaFinalText = block.final.substring((oldBlocks[blockNum]?.final ?? '').length, Infinity) - oldBlocks = blocks - if (!latestStreamLocationMutable) continue + // if a block is done, finish it + if (block.state === 'done') { + const { currentBounds: [finalStartLine, finalEndLine] } = infoOfAddedBlockNum[blockNum] + console.log('FINISHING!!!!!!', blockNum, ':', finalStartLine, finalEndLine, block.final) + + + this._writeText(uri, block.final, + { startLineNumber: finalStartLine, startColumn: 1, endLineNumber: finalEndLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed + { shouldRealignDiffAreas: true } + ) + infoOfAddedBlockNum[blockNum].currentBounds[1] = infoOfAddedBlockNum[blockNum].currentBounds[0] + block.final.split('\n').length - 1 + + currStreamingBlockNum = blockNum + 1 + } // 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 - const latestEndLine = this._writeStreamedDiffZoneLLMText(uri, diffZone.originalCode, block.final, deltaFinalText, latestStreamLocationMutable) - diffZone._streamState.line = (diffZone.startLine - 1) + latestEndLine // change coordinate systems from originalCode to full file + + const deltaFinalText = block.final.substring((oldBlocks[blockNum]?.final ?? '').length, Infinity) + oldBlocks = blocks + + const { numNewLines } = this._writeStreamedDiffZoneLLMText(uri, block.orig, block.final, deltaFinalText, latestStreamLocationMutable) + const currLine = infoOfAddedBlockNum[blockNum].currentBounds[1] + numNewLines + console.log('currline A', currLine) + infoOfAddedBlockNum[blockNum].currentBounds[1] = currLine + diffZone._streamState.line = currLine + + console.log('delta', deltaFinalText) + console.log('currLines', infoOfAddedBlockNum[blockNum].currentBounds) } // end for + console.log('diffZone._streamState.line', diffZone._streamState.line) this._refreshStylesAndDiffsInURI(uri) }, onFinalMessage: async ({ fullText }) => { - console.log('final message!!', fullText) + // 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) + // // 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: When running Apply, your model didn't output any changes that Void recognized. You might need to use a smarter model for Apply.`) - } + // if (blocks.length === 0) { + // this._notificationService.info(`Void: When running Apply, your model didn't output any changes that Void recognized. You might need to use a smarter model for Apply.`) + // } - // writeover the whole file - let newCode = originalFileCode - for (let blockNum = infoOfBlockNum.length - 1; blockNum >= 0; blockNum -= 1) { - const { originalLines } = infoOfBlockNum[blockNum] - const finalCode = blocks[blockNum].final + // // writeover the whole file + // let newCode = originalFileCode + // for (let blockNum = infoOfAddedBlockNum.length - 1; blockNum >= 0; blockNum -= 1) { + // const { originalBounds } = infoOfAddedBlockNum[blockNum] + // const finalCode = blocks[blockNum].final - if (finalCode === null) continue + // if (finalCode === null) continue - const [originalStart, originalEnd] = originalLines - 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 } - ) - } + // 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() + // onDone() }, onError: (e) => { this._notifyError(e) From 1959c53d755b76fb5ec93196d833bd7270aa9154 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 20 Feb 2025 23:23:55 -0800 Subject: [PATCH 09/41] progress --- .../contrib/void/browser/chatThreadService.ts | 6 ++ .../contrib/void/browser/editCodeService.ts | 59 +++++++++---------- .../contrib/void/common/toolsService.ts | 2 +- 3 files changed, 36 insertions(+), 31 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 05ef405b..1526406d 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -156,6 +156,8 @@ export interface IChatThreadService { openNewThread(): void; switchToThread(threadId: string): void; + // you can edit multiple messages + // the one you're currently editing is "focused", and we add items to that one when you press cmd+L. getFocusedMessageIdx(): number | undefined; isFocusingMessage(): boolean; setFocusedMessageIdx(messageIdx: number | undefined): void; @@ -164,8 +166,12 @@ export interface IChatThreadService { _useCurrentThreadState(): readonly [ThreadType['state'], (newState: Partial) => void]; _useCurrentMessageState(messageIdx: number): readonly [UserMessageState, (newState: Partial) => void]; + // call to edit a message editUserMessageAndStreamResponse({ userMessage, chatMode, messageIdx }: { userMessage: string, chatMode: ChatMode, messageIdx: number }): Promise; + + // call to add a message addUserMessageAndStreamResponse({ userMessage, chatMode }: { userMessage: string, chatMode: ChatMode }): Promise; + cancelStreaming(threadId: string): void; dismissStreamError(threadId: string): void; diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index de920242..f0ee5c45 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -1619,48 +1619,47 @@ class EditCodeService extends Disposable implements IEditCodeService { console.log('delta', deltaFinalText) console.log('currLines', infoOfAddedBlockNum[blockNum].currentBounds) - } // end for console.log('diffZone._streamState.line', diffZone._streamState.line) this._refreshStylesAndDiffsInURI(uri) }, onFinalMessage: async ({ fullText }) => { - // console.log('final message!!', fullText) + 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) + // 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: When running Apply, your model didn't output any changes that Void recognized. You might need to use a smarter model for Apply.`) - // } + if (blocks.length === 0) { + this._notificationService.info(`Void: When running Apply, your model didn't output any changes that Void recognized. You might need to use a smarter model for Apply.`) + } - // // writeover the whole file - // let newCode = originalFileCode - // for (let blockNum = infoOfAddedBlockNum.length - 1; blockNum >= 0; blockNum -= 1) { - // const { originalBounds } = infoOfAddedBlockNum[blockNum] - // const finalCode = blocks[blockNum].final + // writeover the whole file + let newCode = originalFileCode + for (let blockNum = infoOfAddedBlockNum.length - 1; blockNum >= 0; blockNum -= 1) { + const { originalBounds } = infoOfAddedBlockNum[blockNum] + const finalCode = blocks[blockNum].final - // if (finalCode === null) continue + 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 } - // ) - // } + 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() + onDone() }, onError: (e) => { this._notifyError(e) diff --git a/src/vs/workbench/contrib/void/common/toolsService.ts b/src/vs/workbench/contrib/void/common/toolsService.ts index dbdd0e15..9686920f 100644 --- a/src/vs/workbench/contrib/void/common/toolsService.ts +++ b/src/vs/workbench/contrib/void/common/toolsService.ts @@ -31,7 +31,7 @@ const paginationHelper = { export const voidTools = { read_file: { name: 'read_file', - description: 'Returns file contents of a given URI.', + description: `Returns file contents of a given URI. ${paginationHelper.desc}`, params: { uri: { type: 'string', description: undefined }, }, From 8918a144041c8de63fea2d3a587915660e23c38e Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 20 Feb 2025 23:23:58 -0800 Subject: [PATCH 10/41] almost there --- .../contrib/void/browser/editCodeService.ts | 142 ++++++++++-------- 1 file changed, 80 insertions(+), 62 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index f0ee5c45..c41fff7a 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -113,7 +113,7 @@ const findTextInCode = (text: string, fileContents: string) => { const startLine = fileContents.substring(0, idx).split('\n').length const numLines = text.split('\n').length const endLine = startLine + numLines - 1 - return [startLine, endLine] + return [startLine, endLine] as const } @@ -164,7 +164,6 @@ type CommonZoneProps = { _URI: URI; // typically we get the URI from model - _removeStylesFns: Set; // these don't remove diffs or this diffArea, only their styles } type CtrlKZone = { @@ -180,6 +179,7 @@ type CtrlKZone = { } _linkedStreamingDiffZone: number | null; // diffareaid of the diffZone currently streaming here + _removeStylesFns: Set // these don't remove diffs or this diffArea, only their styles } & CommonZoneProps @@ -199,12 +199,22 @@ type DiffZone = { }; editorId?: undefined; linkedStreamingDiffZone?: undefined; + _removeStylesFns: Set // these don't remove diffs or this diffArea, only their styles } & CommonZoneProps +type TrackingZone = { + type: 'TrackingZone'; + metadata: T; + originalCode?: undefined; + editorId?: undefined; + _removeStylesFns?: undefined; +} & CommonZoneProps + + // called DiffArea for historical purposes, we can rename to something like TextRegion if we want -type DiffArea = CtrlKZone | DiffZone +type DiffArea = CtrlKZone | DiffZone | TrackingZone const diffAreaSnapshotKeys = [ 'type', @@ -823,7 +833,7 @@ class EditCodeService extends Disposable implements IEditCodeService { this.diffAreaOfId[diffareaid] = { ...snapshottedDiffArea as DiffAreaSnapshot, _URI: uri, - _removeStylesFns: new Set(), + _removeStylesFns: new Set(), _mountInfo: null, _linkedStreamingDiffZone: null, // when restoring, we will never be streaming } @@ -881,8 +891,8 @@ class EditCodeService extends Disposable implements IEditCodeService { if (diffArea.type === 'DiffZone') this._deleteDiffs(diffArea) - diffArea._removeStylesFns.forEach(removeStyles => removeStyles()) - diffArea._removeStylesFns.clear() + diffArea._removeStylesFns?.forEach(removeStyles => removeStyles()) + diffArea._removeStylesFns?.clear() } @@ -1112,7 +1122,7 @@ class EditCodeService extends Disposable implements IEditCodeService { } latestMutable.originalCodeStartLine = startLineInOriginalCode - return { endLineInLlmTextSoFar, numNewLines } + return { endLineInLlmTextSoFar, numNewLines } // numNewLines here might not be correct.... } @@ -1429,13 +1439,13 @@ class EditCodeService extends Disposable implements IEditCodeService { let { onFinishEdit } = this._addToHistory(uri) // TODO replace these with whatever block we're on initially if already started - const infoOfAddedBlockNum: { - originalBounds: [number, number], // 1-indexed - currentBounds: [number, number], // 1-indexed - originalCode: string, - }[] = [] - let oldBlocks: ExtractedSearchReplaceBlock[] = [] + type SearchReplaceDiffAreaMetadata = { + originalBounds: [number, number], // 1-indexed + originalCode: string, + } + + const addedDiffAreaOfBlockNum: TrackingZone[] = [] const adding: Omit = { type: 'DiffZone', @@ -1463,18 +1473,21 @@ class EditCodeService extends Disposable implements IEditCodeService { } + const findTextInCodeWithAdjustedOffset = (blockNum: number, block: ExtractedSearchReplaceBlock): string | { originalBounds: [number, number], currentBounds: [number, number] } => { + // call findInText const foundInCode = findTextInCode(block.orig, originalFileCode) + + // if error, return error as a string if (typeof foundInCode === 'string') { - console.log('Apply error:', foundInCode, '; trying again.') return foundInCode } - const [originalStart, originalEnd] = foundInCode - // compute line offset if there were changes in the past + // adjust based on the changes by computing line offset + const [originalStart, originalEnd] = foundInCode let lineOffset = 0 for (let i = 0; i < blockNum; i += 1) { - const { originalBounds: [otherBlockOriginalStart, otherBlockOriginalEnd], } = infoOfAddedBlockNum[i] + const { originalBounds: [otherBlockOriginalStart, otherBlockOriginalEnd], } = addedDiffAreaOfBlockNum[i].metadata const finalCode = block.final if (otherBlockOriginalStart > originalEnd) continue @@ -1493,9 +1506,6 @@ class EditCodeService extends Disposable implements IEditCodeService { } - let latestStreamLocationMutable: StreamLocationMutable | null = null - - const onDone = () => { diffZone._streamState = { isStreaming: false, } @@ -1507,12 +1517,14 @@ class EditCodeService extends Disposable implements IEditCodeService { // refresh now in case onText takes a while to get 1st message this._refreshStylesAndDiffsInURI(uri) - + let latestStreamLocationMutable: StreamLocationMutable | null = null let shouldUpdateOrigStreamPos = true + let oldBlocks: ExtractedSearchReplaceBlock[] = [] + + // this generates >>>>>>> ORIGINAL <<<<<<< REPLACE blocks and and simultaneously applies it let shouldSendAnotherMessage = true let nMessagesSent = 0 - // this generates >>>>>>> ORIGINAL <<<<<<< REPLACE blocks and and simultaneously applies it let currStreamingBlockNum = 0 while (shouldSendAnotherMessage) { shouldSendAnotherMessage = false @@ -1524,8 +1536,11 @@ class EditCodeService extends Disposable implements IEditCodeService { logging: { loggingName: `generateSearchAndReplace` }, messages, onText: ({ fullText }) => { + // blocks are [done done done ... {writingFinal|writingOriginal}] + // ^ + // currStreamingBlockNum + const blocks = extractSearchReplaceBlocks(fullText) - if (blocks.length === 2) return for (let blockNum = currStreamingBlockNum; blockNum < blocks.length; blockNum += 1) { const block = blocks[blockNum] @@ -1534,12 +1549,23 @@ class EditCodeService extends Disposable implements IEditCodeService { if (block.state === 'writingOriginal') { // update stream state if (shouldUpdateOrigStreamPos && block.orig.trim().length >= 20) { - console.log('or') - const startingAtLine = latestStreamLocationMutable?.line ?? 1 // dont go backwards if already have a stream line - const blockOrigLines = block.orig.split('\n') - const idx = originalFileCode.indexOf(blockOrigLines.slice(startingAtLine, Infinity).join('\n'), (startingAtLine - 1)) - diffZone._streamState.line = originalFileCode.substring(0, idx).split('\n').length - shouldUpdateOrigStreamPos = false + + const startingAtLine = diffZone._streamState.line ?? 1 // dont go backwards if already have a stream line + + const originalCodeStartingAtLine = originalFileCode.split('\n').slice(startingAtLine, Infinity).join('\n') + const idxInAfter = originalCodeStartingAtLine.indexOf(block.orig) + + console.log('SEARCHED FOR:\n', block.orig, '\nIN:\n', originalCodeStartingAtLine, '\nGOT:\n', idxInAfter) + + if (idxInAfter !== -1) { + const numOriginalCodeLinesBefore = originalCodeStartingAtLine.substring(0, idxInAfter).split('\n').length + const lineNum = startingAtLine + numOriginalCodeLinesBefore + + console.log('SWITCHING TO LINE', lineNum) + + diffZone._streamState.line = lineNum + shouldUpdateOrigStreamPos = false + } } continue } @@ -1547,10 +1573,10 @@ class EditCodeService extends Disposable implements IEditCodeService { // if this is the first time we're seeing this block, add it as a blocknum - if (!(blockNum in infoOfAddedBlockNum)) { - + if (!(blockNum in addedDiffAreaOfBlockNum)) { const pos = findTextInCodeWithAdjustedOffset(blockNum, block) + // if error if (typeof pos === 'string') { const errorStartingBlock = pos console.log('ERROR STARTING BLOCK SPOT!!!!!', errorStartingBlock) @@ -1569,34 +1595,29 @@ class EditCodeService extends Disposable implements IEditCodeService { continue } - console.log('orig bounds', pos.originalBounds) - console.log('curr bounds', pos.currentBounds) - - infoOfAddedBlockNum.push({ - originalBounds: [...pos.originalBounds], - currentBounds: [...pos.currentBounds], - originalCode: block.orig, - }) - + // otherwise if no error, add the position as a diffarea + const adding: Omit, 'diffareaid'> = { + type: 'TrackingZone', + startLine: pos.currentBounds[0], + endLine: pos.currentBounds[1], + _URI: uri, + metadata: { + originalBounds: pos.originalBounds, + originalCode: block.orig, + }, + } + const trackingZone = this._addDiffArea(adding) + addedDiffAreaOfBlockNum.push(trackingZone) latestStreamLocationMutable = { line: pos.currentBounds[0], addedSplitYet: false, col: 1, originalCodeStartLine: 1 } - - console.log('latestStreamLocation', latestStreamLocationMutable) - - } - + } // <-- done adding diffarea // if a block is done, finish it if (block.state === 'done') { - const { currentBounds: [finalStartLine, finalEndLine] } = infoOfAddedBlockNum[blockNum] - console.log('FINISHING!!!!!!', blockNum, ':', finalStartLine, finalEndLine, block.final) - - + const { startLine: finalStartLine, endLine: finalEndLine } = addedDiffAreaOfBlockNum[blockNum] this._writeText(uri, block.final, { startLineNumber: finalStartLine, startColumn: 1, endLineNumber: finalEndLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed { shouldRealignDiffAreas: true } ) - infoOfAddedBlockNum[blockNum].currentBounds[1] = infoOfAddedBlockNum[blockNum].currentBounds[0] + block.final.split('\n').length - 1 - currStreamingBlockNum = blockNum + 1 } @@ -1608,20 +1629,17 @@ class EditCodeService extends Disposable implements IEditCodeService { if (!latestStreamLocationMutable) continue + // write the added text to the file + const { endLine: currentEndLine } = addedDiffAreaOfBlockNum[blockNum] const deltaFinalText = block.final.substring((oldBlocks[blockNum]?.final ?? '').length, Infinity) + this._writeStreamedDiffZoneLLMText(uri, block.orig, block.final, deltaFinalText, latestStreamLocationMutable) + diffZone._streamState.line = currentEndLine + console.log('CURRENT LINE', currentEndLine) + oldBlocks = blocks - const { numNewLines } = this._writeStreamedDiffZoneLLMText(uri, block.orig, block.final, deltaFinalText, latestStreamLocationMutable) - const currLine = infoOfAddedBlockNum[blockNum].currentBounds[1] + numNewLines - console.log('currline A', currLine) - infoOfAddedBlockNum[blockNum].currentBounds[1] = currLine - diffZone._streamState.line = currLine - - console.log('delta', deltaFinalText) - console.log('currLines', infoOfAddedBlockNum[blockNum].currentBounds) } // end for - console.log('diffZone._streamState.line', diffZone._streamState.line) this._refreshStylesAndDiffsInURI(uri) }, onFinalMessage: async ({ fullText }) => { @@ -1637,8 +1655,8 @@ class EditCodeService extends Disposable implements IEditCodeService { // writeover the whole file let newCode = originalFileCode - for (let blockNum = infoOfAddedBlockNum.length - 1; blockNum >= 0; blockNum -= 1) { - const { originalBounds } = infoOfAddedBlockNum[blockNum] + for (let blockNum = addedDiffAreaOfBlockNum.length - 1; blockNum >= 0; blockNum -= 1) { + const { originalBounds } = addedDiffAreaOfBlockNum[blockNum].metadata const finalCode = blocks[blockNum].final if (finalCode === null) continue From 73c3a8133ac431f6c092861e9c7c4b70b4f626b2 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 20 Feb 2025 23:59:16 -0800 Subject: [PATCH 11/41] massively simplified, seems to work! --- .../contrib/void/browser/editCodeService.ts | 94 ++++++++----------- 1 file changed, 39 insertions(+), 55 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index c41fff7a..2cf813cd 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -105,8 +105,13 @@ const getLeadingWhitespacePx = (editor: ICodeEditor, startLine: number): number // finds block.orig in fileContents and return its range in file -const findTextInCode = (text: string, fileContents: string) => { - const idx = fileContents.indexOf(text) +// startingAtLine is 1-indexed and inclusive +const findTextInCode = (text: string, fileContents: string, startingAtLine?: number) => { + const idx = fileContents.indexOf(text, + startingAtLine !== undefined ? + fileContents.split('\n').slice(0, startingAtLine).join('\n').length // num characters in all lines before startingAtLine + : 0 + ) if (idx === -1) return 'Not found' as const const lastIdx = fileContents.lastIndexOf(text) if (lastIdx !== idx) return 'Not unique' as const @@ -1473,36 +1478,30 @@ class EditCodeService extends Disposable implements IEditCodeService { } - - const findTextInCodeWithAdjustedOffset = (blockNum: number, block: ExtractedSearchReplaceBlock): string | { originalBounds: [number, number], currentBounds: [number, number] } => { - // call findInText - const foundInCode = findTextInCode(block.orig, originalFileCode) - - // if error, return error as a string - if (typeof foundInCode === 'string') { - return foundInCode - } - + const convertOriginalRangeToFinalRange = (originalRange: readonly [number, number]): [number, number] => { // adjust based on the changes by computing line offset - const [originalStart, originalEnd] = foundInCode + const [originalStart, originalEnd] = originalRange let lineOffset = 0 - for (let i = 0; i < blockNum; i += 1) { - const { originalBounds: [otherBlockOriginalStart, otherBlockOriginalEnd], } = addedDiffAreaOfBlockNum[i].metadata - const finalCode = block.final - - if (otherBlockOriginalStart > originalEnd) continue - if (finalCode === null) continue - - const numNewLines = finalCode.split('\n').length - const numOldLines = otherBlockOriginalEnd - otherBlockOriginalStart + 1 - + for (const blockDiffArea of addedDiffAreaOfBlockNum) { + const { + startLine, endLine, + metadata: { originalBounds: [originalStart2, originalEnd2], }, + } = blockDiffArea + if (originalStart2 >= originalEnd) continue + const numNewLines = endLine - startLine + 1 + const numOldLines = originalEnd2 - originalStart2 + 1 lineOffset += numNewLines - numOldLines } + return [originalStart + lineOffset, originalEnd + lineOffset] + } - return { - originalBounds: [originalStart, originalEnd], - currentBounds: [originalStart + lineOffset, originalEnd + lineOffset], - } + + const errMsgOfInvalidStr = (str: string & ReturnType) => { + 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.' + : '' } @@ -1549,21 +1548,11 @@ class EditCodeService extends Disposable implements IEditCodeService { if (block.state === 'writingOriginal') { // update stream state if (shouldUpdateOrigStreamPos && block.orig.trim().length >= 20) { - const startingAtLine = diffZone._streamState.line ?? 1 // dont go backwards if already have a stream line - - const originalCodeStartingAtLine = originalFileCode.split('\n').slice(startingAtLine, Infinity).join('\n') - const idxInAfter = originalCodeStartingAtLine.indexOf(block.orig) - - console.log('SEARCHED FOR:\n', block.orig, '\nIN:\n', originalCodeStartingAtLine, '\nGOT:\n', idxInAfter) - - if (idxInAfter !== -1) { - const numOriginalCodeLinesBefore = originalCodeStartingAtLine.substring(0, idxInAfter).split('\n').length - const lineNum = startingAtLine + numOriginalCodeLinesBefore - - console.log('SWITCHING TO LINE', lineNum) - - diffZone._streamState.line = lineNum + const originalRange = findTextInCode(block.orig, originalFileCode, startingAtLine) + if (typeof originalRange !== 'string') { + const [_, endLine] = convertOriginalRangeToFinalRange(originalRange) + diffZone._streamState.line = endLine shouldUpdateOrigStreamPos = false } } @@ -1574,20 +1563,13 @@ class EditCodeService extends Disposable implements IEditCodeService { // if this is the first time we're seeing this block, add it as a blocknum if (!(blockNum in addedDiffAreaOfBlockNum)) { - const pos = findTextInCodeWithAdjustedOffset(blockNum, block) + const originalBounds = findTextInCode(block.orig, originalFileCode) // if error - if (typeof pos === 'string') { - const errorStartingBlock = pos - console.log('ERROR STARTING BLOCK SPOT!!!!!', errorStartingBlock) - const errMsgForLLM = errorStartingBlock === '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.' - : errorStartingBlock === '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.' - : '' + if (typeof originalBounds === 'string') { messages.push( { role: 'assistant', content: fullText }, // latest output - { role: 'user', content: errMsgForLLM } // user explanation of what's wrong + { role: 'user', content: errMsgOfInvalidStr(originalBounds) } // user explanation of what's wrong ) if (streamRequestIdRef.current) this._llmMessageService.abort(streamRequestIdRef.current) shouldSendAnotherMessage = true @@ -1595,20 +1577,22 @@ class EditCodeService extends Disposable implements IEditCodeService { continue } + const [startLine, endLine] = convertOriginalRangeToFinalRange(originalBounds) + // otherwise if no error, add the position as a diffarea const adding: Omit, 'diffareaid'> = { type: 'TrackingZone', - startLine: pos.currentBounds[0], - endLine: pos.currentBounds[1], + startLine: startLine, + endLine: endLine, _URI: uri, metadata: { - originalBounds: pos.originalBounds, + originalBounds: [...originalBounds], originalCode: block.orig, }, } const trackingZone = this._addDiffArea(adding) addedDiffAreaOfBlockNum.push(trackingZone) - latestStreamLocationMutable = { line: pos.currentBounds[0], addedSplitYet: false, col: 1, originalCodeStartLine: 1 } + latestStreamLocationMutable = { line: startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 } } // <-- done adding diffarea // if a block is done, finish it From 09f5d3e14b5bb686389dbc37f7a6cbd11474b425 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 21 Feb 2025 00:19:58 -0800 Subject: [PATCH 12/41] nitpick style --- .../contrib/void/browser/editCodeService.ts | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 2cf813cd..ffae89c7 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -1517,7 +1517,7 @@ class EditCodeService extends Disposable implements IEditCodeService { this._refreshStylesAndDiffsInURI(uri) let latestStreamLocationMutable: StreamLocationMutable | null = null - let shouldUpdateOrigStreamPos = true + let shouldUpdateOrigStreamStyle = true let oldBlocks: ExtractedSearchReplaceBlock[] = [] @@ -1544,24 +1544,25 @@ class EditCodeService extends Disposable implements IEditCodeService { for (let blockNum = currStreamingBlockNum; blockNum < blocks.length; blockNum += 1) { const block = blocks[blockNum] - // must be done writing original to stream code if (block.state === 'writingOriginal') { - // update stream state - if (shouldUpdateOrigStreamPos && block.orig.trim().length >= 20) { + // 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 [_, endLine] = convertOriginalRangeToFinalRange(originalRange) - diffZone._streamState.line = endLine - shouldUpdateOrigStreamPos = false + const [startLine, _] = convertOriginalRangeToFinalRange(originalRange) + diffZone._streamState.line = startLine + console.log('CURRENT LINE A', startLine) + shouldUpdateOrigStreamStyle = false } } + // must be done writing original to move on to writing streamed content continue } - shouldUpdateOrigStreamPos = true + shouldUpdateOrigStreamStyle = true - // if this is the first time we're seeing this block, add it as a blocknum + // if this is the first time we're seeing this block, add it as a diffarea so we can start streaming if (!(blockNum in addedDiffAreaOfBlockNum)) { const originalBounds = findTextInCode(block.orig, originalFileCode) @@ -1595,7 +1596,7 @@ class EditCodeService extends Disposable implements IEditCodeService { latestStreamLocationMutable = { line: startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 } } // <-- done adding diffarea - // if a block is done, finish it + // if a block is done, finish it by writing all if (block.state === 'done') { const { startLine: finalStartLine, endLine: finalEndLine } = addedDiffAreaOfBlockNum[blockNum] this._writeText(uri, block.final, @@ -1614,14 +1615,17 @@ class EditCodeService extends Disposable implements IEditCodeService { // write the added text to the file - const { endLine: currentEndLine } = addedDiffAreaOfBlockNum[blockNum] const deltaFinalText = block.final.substring((oldBlocks[blockNum]?.final ?? '').length, Infinity) this._writeStreamedDiffZoneLLMText(uri, block.orig, block.final, deltaFinalText, latestStreamLocationMutable) - diffZone._streamState.line = currentEndLine - console.log('CURRENT LINE', currentEndLine) - oldBlocks = blocks + // update stream line if it's still streaming (otherwise another block might be streaming) + if (block.state !== 'done') { + const { endLine: currentEndLine } = addedDiffAreaOfBlockNum[blockNum] + diffZone._streamState.line = currentEndLine + } + + } // end for this._refreshStylesAndDiffsInURI(uri) From 06ce9e1017161eb7fca0edaad895ead605bc6723 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 21 Feb 2025 15:06:42 -0800 Subject: [PATCH 13/41] settings styles + password field --- .../contrib/void/browser/editCodeService.ts | 29 ++++++++--------- .../void/browser/react/src/util/inputs.tsx | 4 ++- .../react/src/void-settings-tsx/Settings.tsx | 32 ++++++++++--------- .../contrib/void/common/toolsService.ts | 13 ++++++++ .../contrib/void/common/voidSettingsTypes.ts | 8 +++-- .../void/electron-main/llmMessage/openai.ts | 1 + 6 files changed, 53 insertions(+), 34 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index ffae89c7..ad8fa505 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -1596,15 +1596,6 @@ class EditCodeService extends Disposable implements IEditCodeService { latestStreamLocationMutable = { line: startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 } } // <-- done adding diffarea - // if a block is done, finish it by writing all - if (block.state === 'done') { - const { startLine: finalStartLine, endLine: finalEndLine } = addedDiffAreaOfBlockNum[blockNum] - this._writeText(uri, block.final, - { startLineNumber: finalStartLine, startColumn: 1, endLineNumber: finalEndLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed - { shouldRealignDiffAreas: true } - ) - currStreamingBlockNum = blockNum + 1 - } // should always be in streaming state here if (!diffZone._streamState.isStreaming) { @@ -1613,17 +1604,25 @@ class EditCodeService extends Disposable implements IEditCodeService { } if (!latestStreamLocationMutable) continue + // if a block is done, finish it by writing all + if (block.state === 'done') { + const { startLine: finalStartLine, endLine: finalEndLine } = addedDiffAreaOfBlockNum[blockNum] + this._writeText(uri, block.final, + { startLineNumber: finalStartLine, startColumn: 1, endLineNumber: finalEndLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed + { shouldRealignDiffAreas: true } + ) + diffZone._streamState.line = finalEndLine + 1 + currStreamingBlockNum = blockNum + 1 + continue + } // write the added text to the file const deltaFinalText = block.final.substring((oldBlocks[blockNum]?.final ?? '').length, Infinity) this._writeStreamedDiffZoneLLMText(uri, block.orig, block.final, deltaFinalText, latestStreamLocationMutable) - oldBlocks = blocks + oldBlocks = blocks // oldblocks is only used if writingFinal - // update stream line if it's still streaming (otherwise another block might be streaming) - if (block.state !== 'done') { - const { endLine: currentEndLine } = addedDiffAreaOfBlockNum[blockNum] - diffZone._streamState.line = currentEndLine - } + const { endLine: currentEndLine } = addedDiffAreaOfBlockNum[blockNum] + diffZone._streamState.line = currentEndLine } // end for diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx index e62e7a9e..270870a7 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx @@ -152,12 +152,13 @@ export const VoidInputBox2 = forwardRef(fun }) -export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, placeholder, multiline }: { +export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, placeholder, isPasswordField, multiline }: { onChangeText: (value: string) => void; styles?: Partial, onCreateInstance?: (instance: InputBox) => void | IDisposable[]; inputBoxRef?: { current: InputBox | null }; placeholder: string; + isPasswordField?: boolean; multiline: boolean; }) => { @@ -182,6 +183,7 @@ export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, plac }, placeholder, tooltip: '', + type: isPasswordField ? 'password' : undefined, flexibleHeight: multiline, flexibleMaxHeight: 500, flexibleWidth: false, diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx index fd612c43..a6aec380 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx @@ -21,7 +21,7 @@ import { os } from '../../../helpers/systemInfo.js' const SubtleButton = ({ onClick, text, icon, disabled }: { onClick: () => void, text: string, icon: React.ReactNode, disabled: boolean }) => { - return
    + return
    @@ -82,9 +82,7 @@ const RefreshableModels = () => { const buttons = refreshableProviderNames.map(providerName => { if (!settingsState.settingsOfProvider[providerName]._didFillInProviderSettings) return null - return
    - -
    + return }) return <> @@ -257,7 +255,7 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider // const { title: providerTitle, } = displayInfoOfProviderName(providerName) - const { title: settingTitle, placeholder, subTextMd } = displayInfoOfSettingName(providerName, settingName) + const { title: settingTitle, placeholder, isPasswordField, subTextMd } = displayInfoOfSettingName(providerName, settingName) const accessor = useAccessor() const voidSettingsService = accessor.get('IVoidSettingsService') @@ -269,6 +267,7 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider { if (weChangedTextRef) return voidSettingsService.setSettingOfProvider(providerName, settingName, newVal) @@ -291,6 +290,7 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider return [disposable] }, [voidSettingsService, providerName, settingName])} multiline={false} + isPasswordField={isPasswordField} /> {subTextMd === undefined ? null :
    @@ -339,7 +339,7 @@ const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) = {needsModel ? providerName === 'ollama' ? - : + : : null}
    @@ -368,15 +368,16 @@ export const AutoRefreshToggle = () => { // right now this is just `enabled_autoRefreshModels` const enabled = voidSettingsState.globalSettings[settingName] - return { - voidSettingsService.setGlobalSetting(settingName, !enabled) - metricsService.capture('Click', { action: 'Autorefresh Toggle', settingName, enabled: !enabled }) - }} - text={`Automatically detect local providers and models (${refreshableProviderNames.map(providerName => displayInfoOfProviderName(providerName).title).join(', ')}).`} - icon={enabled ? : } - disabled={false} - /> + return { + voidSettingsService.setGlobalSetting(settingName, !enabled) + metricsService.capture('Click', { action: 'Autorefresh Toggle', settingName, enabled: !enabled }) + }} + text={`Automatically detect local providers and models (${refreshableProviderNames.map(providerName => displayInfoOfProviderName(providerName).title).join(', ')}).`} + icon={enabled ? : } + disabled={false} + /> + } export const AIInstructionsBox = () => { @@ -400,6 +401,7 @@ export const FeaturesTab = () => { +
    diff --git a/src/vs/workbench/contrib/void/common/toolsService.ts b/src/vs/workbench/contrib/void/common/toolsService.ts index 9686920f..522f17c5 100644 --- a/src/vs/workbench/contrib/void/common/toolsService.ts +++ b/src/vs/workbench/contrib/void/common/toolsService.ts @@ -68,6 +68,18 @@ export const voidTools = { required: ['query'], }, + // 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 @@ -86,6 +98,7 @@ export type ToolCallReturnType : T extends 'list_dir' ? string : T extends 'pathname_search' ? string | URI[] : T extends 'search' ? string | URI[] + : T extends 'create_file' ? string : never export type ToolFns = { [T in ToolName]: (p: string) => Promise<[ToolCallReturnType, boolean]> } diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index 06e708fd..96337d33 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -505,9 +505,10 @@ export const displayInfoOfProviderName = (providerName: ProviderName): DisplayIn } type DisplayInfo = { - title: string, - placeholder: string, - subTextMd?: string, + title: string; + placeholder: string; + subTextMd?: string; + isPasswordField?: boolean; } export const displayInfoOfSettingName = (providerName: ProviderName, settingName: SettingName): DisplayInfo => { if (settingName === 'apiKey') { @@ -537,6 +538,7 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName providerName === 'xAI' ? 'Get your [API Key here](https://console.x.ai).' : providerName === 'openAICompatible' ? undefined : '', + isPasswordField: true, } } else if (settingName === 'endpoint') { diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts index 66c0ffe1..37b8b468 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts @@ -201,6 +201,7 @@ export const sendOpenAIChat: _InternalSendLLMChatMessageFnType = ({ messages: me // message let newText = '' newText += chunk.choices[0]?.delta?.content ?? '' + console.log('!!!!', chunk.choices[0]?.delta) fullText += newText; onText({ newText, fullText }); From ce14986d2f7a5b9a56f039f65363086d9146a6d8 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 21 Feb 2025 16:19:01 -0800 Subject: [PATCH 14/41] split apply buttons --- .../src/markdown/ApplyBlockHoverButtons.tsx | 84 +++++++++++++++++++ .../react/src/markdown/ChatMarkdownRender.tsx | 78 +---------------- .../react/src/sidebar-tsx/SidebarChat.tsx | 2 +- .../contrib/void/common/voidSettingsTypes.ts | 2 +- .../void/electron-main/llmMessage/openai.ts | 4 +- 5 files changed, 91 insertions(+), 79 deletions(-) create mode 100644 src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx new file mode 100644 index 00000000..c7f4b52c --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx @@ -0,0 +1,84 @@ +import { useState, useEffect, useCallback } from 'react' +import { useAccessor } from '../util/services.js' + +enum CopyButtonText { + Idle = 'Copy', + Copied = 'Copied!', + Error = 'Could not copy', +} + +const COPY_FEEDBACK_TIMEOUT = 1000 // amount of time to say 'Copied!' + +const CopyButton = ({ codeStr }: { codeStr: string }) => { + const accessor = useAccessor() + + const metricsService = accessor.get('IMetricsService') + const clipboardService = accessor.get('IClipboardService') + const [copyButtonText, setCopyButtonText] = useState(CopyButtonText.Idle) + + useEffect(() => { + if (copyButtonText === CopyButtonText.Idle) return + setTimeout(() => { + setCopyButtonText(CopyButtonText.Idle) + }, COPY_FEEDBACK_TIMEOUT) + }, [copyButtonText]) + + + const onCopy = useCallback(() => { + clipboardService.writeText(codeStr) + .then(() => { setCopyButtonText(CopyButtonText.Copied) }) + .catch(() => { setCopyButtonText(CopyButtonText.Error) }) + metricsService.capture('Copy Code', { length: codeStr.length }) // capture the length only + }, [metricsService, clipboardService, codeStr]) + + const isSingleLine = !codeStr.includes('\n') + + return +} + + + +const ApplyButton = ({ codeStr }: { codeStr: string }) => { + const accessor = useAccessor() + + const editCodeService = accessor.get('IEditCodeService') + const metricsService = accessor.get('IMetricsService') + + + const onApply = useCallback(() => { + + editCodeService.startApplying({ + from: 'ClickApply', + type: 'searchReplace', + applyStr: codeStr, + }) + metricsService.capture('Apply Code', { length: codeStr.length }) // capture the length only + }, [metricsService, editCodeService, codeStr]) + + const isSingleLine = !codeStr.includes('\n') + + return + +} + + + + + +export const ApplyBlockHoverButtons = ({ codeStr }: { codeStr: string }) => { + return <> + + + +} diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index 7a9953b7..6f737ccb 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -3,22 +3,12 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import React, { JSX, useCallback, useEffect, useState } from 'react' +import React, { JSX } from 'react' import { marked, MarkedToken, Token } from 'marked' import { BlockCode } from './BlockCode.js' -import { useAccessor, useChatThreadsState, useChatThreadsStreamState } from '../util/services.js' import { ChatMessageLocation, } from '../../../aiRegexService.js' import { nameToVscodeLanguage } from '../../../helpers/detectLanguage.js' - - -enum CopyButtonState { - Copy = 'Copy', - Copied = 'Copied!', - Error = 'Could not copy', -} - -const COPY_FEEDBACK_TIMEOUT = 1000 // amount of time to say 'Copied!' - +import { ApplyBlockHoverButtons } from './ApplyBlockHoverButtons.js' type ApplyBoxLocation = ChatMessageLocation & { tokenIdx: string } @@ -29,60 +19,6 @@ const getApplyBoxId = ({ threadId, messageIdx, tokenIdx }: ApplyBoxLocation) => -const ApplyButtonsOnHover = ({ applyStr }: { applyStr: string }) => { - const accessor = useAccessor() - - const [copyButtonState, setCopyButtonState] = useState(CopyButtonState.Copy) - const editCodeService = accessor.get('IEditCodeService') - const clipboardService = accessor.get('IClipboardService') - const metricsService = accessor.get('IMetricsService') - - useEffect(() => { - - if (copyButtonState !== CopyButtonState.Copy) { - setTimeout(() => { - setCopyButtonState(CopyButtonState.Copy) - }, COPY_FEEDBACK_TIMEOUT) - } - }, [copyButtonState]) - - const onCopy = useCallback(() => { - clipboardService.writeText(applyStr) - .then(() => { setCopyButtonState(CopyButtonState.Copied) }) - .catch(() => { setCopyButtonState(CopyButtonState.Error) }) - metricsService.capture('Copy Code', { length: applyStr.length }) // capture the length only - - }, [metricsService, clipboardService, applyStr]) - - const onApply = useCallback(() => { - - editCodeService.startApplying({ - from: 'ClickApply', - type: 'searchReplace', - applyStr, - }) - metricsService.capture('Apply Code', { length: applyStr.length }) // capture the length only - }, [metricsService, editCodeService, applyStr]) - - const isSingleLine = !applyStr.includes('\n') - - return <> - - - -} - export const CodeSpan = ({ children, className }: { children: React.ReactNode, className?: string }) => { return } + buttonsOnHover={} /> } diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx index e9169280..30351697 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx @@ -703,7 +703,7 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM className={` relative ${mode === 'edit' ? 'px-2 w-full max-w-full' - : role === 'user' ? `px-2 self-end w-fit max-w-full whitespace-pre-wrap` // user words should be pre + : role === 'user' ? `my-0.5 px-2 self-end w-fit max-w-full whitespace-pre-wrap` // user words should be pre : role === 'assistant' ? `px-2 self-start w-full max-w-full` : '' } `} diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index 96337d33..529a872c 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -545,7 +545,7 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName return { title: providerName === 'ollama' ? 'Endpoint' : providerName === 'vLLM' ? 'Endpoint' : - providerName === 'openAICompatible' ? 'baseURL' :// (do not include /chat/completions) + providerName === 'openAICompatible' ? 'baseURL' : // (do not include /chat/completions) '(never)', placeholder: providerName === 'ollama' ? defaultProviderSettings.ollama.endpoint diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts index 37b8b468..7769a983 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts @@ -10,12 +10,12 @@ import { InternalToolInfo } from '../../common/toolsService.js'; import { addSystemMessageAndToolSupport } from './preprocessLLMMessages.js'; import { developerInfoOfModelName, developerInfoOfProviderName } from '../../common/voidSettingsTypes.js'; import { isAToolName } from './postprocessToolCalls.js'; -// import { parseMaxTokensStr } from './util.js'; // developer command - https://cdn.openai.com/spec/model-spec-2024-05-08.html#follow-the-chain-of-command // prompting - https://platform.openai.com/docs/guides/reasoning#advice-on-prompting +// npm i @openrouter/ai-sdk-provider ai ollama-ai-provider export const toOpenAITool = (toolInfo: InternalToolInfo) => { const { name, description, params, required } = toolInfo @@ -201,7 +201,7 @@ export const sendOpenAIChat: _InternalSendLLMChatMessageFnType = ({ messages: me // message let newText = '' newText += chunk.choices[0]?.delta?.content ?? '' - console.log('!!!!', chunk.choices[0]?.delta) + console.log('!!!!', JSON.stringify(chunk, null, 2)) fullText += newText; onText({ newText, fullText }); From b005b1e95aafeb99c26c9adc801df3c75c1da625 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 21 Feb 2025 18:53:55 -0800 Subject: [PATCH 15/41] progress (BROKEN) --- .../contrib/void/browser/editCodeService.ts | 62 +++++++++++-------- .../src/markdown/ApplyBlockHoverButtons.tsx | 12 ++-- .../react/src/markdown/ChatMarkdownRender.tsx | 10 ++- .../contrib/void/common/voidFileService.ts | 6 +- 4 files changed, 55 insertions(+), 35 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index ad8fa505..228d7a2f 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -248,7 +248,7 @@ type StreamLocationMutable = { line: number, col: number, addedSplitYet: boolean export interface IEditCodeService { readonly _serviceBrand: undefined; - startApplying(opts: StartApplyingOpts): number | undefined; + startApplying(opts: StartApplyingOpts): number | void; interruptStreaming(diffareaid: number): void; addCtrlKZone(opts: AddCtrlKOpts): number | undefined; removeCtrlKZone(opts: { diffareaid: number }): void; @@ -270,7 +270,7 @@ class EditCodeService extends Disposable implements IEditCodeService { // only applies to diffZones // streamingDiffZones: Set = new Set() - private readonly _onDidChangeStreaming = new Emitter<{ uri: URI; diffareaid: number }>(); + private readonly _onDidChangeStreaming = new Emitter<{ uri: URI }>(); private readonly _onDidAddOrDeleteDiffZones = new Emitter<{ uri: URI }>(); @@ -471,7 +471,7 @@ class EditCodeService extends Disposable implements IEditCodeService { const buttonsWidget = new AcceptAllRejectAllWidget({ editor, onAcceptAll: () => { - this.removeDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false }) + this.removeDiffAreas({ uri, behavior: 'keep', removeCtrlKs: false }) this._metricsService.capture('Accept All', {}) }, onRejectAll: () => { @@ -918,6 +918,11 @@ class EditCodeService extends Disposable implements IEditCodeService { this._onDidAddOrDeleteDiffZones.fire({ uri: diffZone._URI }) } + private _deleteTrackingZone(trackingZone: TrackingZone) { + delete this.diffAreaOfId[trackingZone.diffareaid] + this.diffAreasOfURI[trackingZone._URI.fsPath].delete(trackingZone.diffareaid.toString()) + } + private _deleteCtrlKZone(ctrlKZone: CtrlKZone) { this._clearAllEffects(ctrlKZone._URI) ctrlKZone._mountInfo?.dispose() @@ -1200,11 +1205,11 @@ class EditCodeService extends Disposable implements IEditCodeService { } else if (opts.type === 'searchReplace') { - this._initializeSearchAndReplaceStream(opts) - return undefined + const addedDiffZone = this._initializeSearchAndReplaceStream(opts) + return addedDiffZone?.diffareaid } - else return undefined + return undefined } @@ -1297,7 +1302,7 @@ class EditCodeService extends Disposable implements IEditCodeService { _removeStylesFns: new Set(), } const diffZone = this._addDiffArea(adding) - this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) + this._onDidChangeStreaming.fire({ uri }) this._onDidAddOrDeleteDiffZones.fire({ uri }) if (from === 'QuickEdit') { @@ -1339,7 +1344,7 @@ class EditCodeService extends Disposable implements IEditCodeService { const onDone = () => { diffZone._streamState = { isStreaming: false, } - this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) + this._onDidChangeStreaming.fire({ uri }) if (from === 'QuickEdit') { const ctrlKZone = this.diffAreaOfId[opts.diffareaid] as CtrlKZone @@ -1414,14 +1419,14 @@ class EditCodeService extends Disposable implements IEditCodeService { - private async _initializeSearchAndReplaceStream({ applyStr }: { applyStr: string }) { + private _initializeSearchAndReplaceStream({ applyStr }: { applyStr: string }) { const uri_ = this._getActiveEditorURI() if (!uri_) return const uri = uri_ // generate search/replace block text - const originalFileCode = await this._voidFileService.readFile(uri) + const originalFileCode = this._voidFileService.readModel(uri) if (originalFileCode === null) return const numLines = this._getNumLines(uri) @@ -1439,6 +1444,7 @@ class EditCodeService extends Disposable implements IEditCodeService { { role: 'user', content: userMessageContent }, ] + // can use this as a proxy to set the diffArea's stream state requestId let streamRequestIdRef: { current: string | null } = { current: null } let { onFinishEdit } = this._addToHistory(uri) @@ -1450,7 +1456,7 @@ class EditCodeService extends Disposable implements IEditCodeService { originalCode: string, } - const addedDiffAreaOfBlockNum: TrackingZone[] = [] + const addedTrackingZoneOfBlockNum: TrackingZone[] = [] const adding: Omit = { type: 'DiffZone', @@ -1467,7 +1473,7 @@ class EditCodeService extends Disposable implements IEditCodeService { _removeStylesFns: new Set(), } const diffZone = this._addDiffArea(adding) - this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) + this._onDidChangeStreaming.fire({ uri }) this._onDidAddOrDeleteDiffZones.fire({ uri }) @@ -1482,7 +1488,7 @@ class EditCodeService extends Disposable implements IEditCodeService { // adjust based on the changes by computing line offset const [originalStart, originalEnd] = originalRange let lineOffset = 0 - for (const blockDiffArea of addedDiffAreaOfBlockNum) { + for (const blockDiffArea of addedTrackingZoneOfBlockNum) { const { startLine, endLine, metadata: { originalBounds: [originalStart2, originalEnd2], }, @@ -1505,17 +1511,23 @@ class EditCodeService extends Disposable implements IEditCodeService { } - const onDone = () => { diffZone._streamState = { isStreaming: false, } - this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) + this._onDidChangeStreaming.fire({ uri }) this._refreshStylesAndDiffsInURI(uri) + + // delete the tracking zones + for (const trackingZone of addedTrackingZoneOfBlockNum) + this._deleteTrackingZone(trackingZone) + onFinishEdit() + shouldSendAnotherMessage = false } // refresh now in case onText takes a while to get 1st message this._refreshStylesAndDiffsInURI(uri) + // stream style related let latestStreamLocationMutable: StreamLocationMutable | null = null let shouldUpdateOrigStreamStyle = true @@ -1552,7 +1564,6 @@ class EditCodeService extends Disposable implements IEditCodeService { if (typeof originalRange !== 'string') { const [startLine, _] = convertOriginalRangeToFinalRange(originalRange) diffZone._streamState.line = startLine - console.log('CURRENT LINE A', startLine) shouldUpdateOrigStreamStyle = false } } @@ -1563,7 +1574,7 @@ class EditCodeService extends Disposable implements IEditCodeService { // if this is the first time we're seeing this block, add it as a diffarea so we can start streaming - if (!(blockNum in addedDiffAreaOfBlockNum)) { + if (!(blockNum in addedTrackingZoneOfBlockNum)) { const originalBounds = findTextInCode(block.orig, originalFileCode) // if error @@ -1592,7 +1603,7 @@ class EditCodeService extends Disposable implements IEditCodeService { }, } const trackingZone = this._addDiffArea(adding) - addedDiffAreaOfBlockNum.push(trackingZone) + addedTrackingZoneOfBlockNum.push(trackingZone) latestStreamLocationMutable = { line: startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 } } // <-- done adding diffarea @@ -1606,7 +1617,7 @@ class EditCodeService extends Disposable implements IEditCodeService { // if a block is done, finish it by writing all if (block.state === 'done') { - const { startLine: finalStartLine, endLine: finalEndLine } = addedDiffAreaOfBlockNum[blockNum] + 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 } @@ -1621,7 +1632,7 @@ class EditCodeService extends Disposable implements IEditCodeService { this._writeStreamedDiffZoneLLMText(uri, block.orig, block.final, deltaFinalText, latestStreamLocationMutable) oldBlocks = blocks // oldblocks is only used if writingFinal - const { endLine: currentEndLine } = addedDiffAreaOfBlockNum[blockNum] + const { endLine: currentEndLine } = addedTrackingZoneOfBlockNum[blockNum] diffZone._streamState.line = currentEndLine @@ -1642,8 +1653,8 @@ class EditCodeService extends Disposable implements IEditCodeService { // writeover the whole file let newCode = originalFileCode - for (let blockNum = addedDiffAreaOfBlockNum.length - 1; blockNum >= 0; blockNum -= 1) { - const { originalBounds } = addedDiffAreaOfBlockNum[blockNum].metadata + for (let blockNum = addedTrackingZoneOfBlockNum.length - 1; blockNum >= 0; blockNum -= 1) { + const { originalBounds } = addedTrackingZoneOfBlockNum[blockNum].metadata const finalCode = blocks[blockNum].final if (finalCode === null) continue @@ -1676,6 +1687,7 @@ class EditCodeService extends Disposable implements IEditCodeService { } + return diffZone } @@ -1689,7 +1701,7 @@ class EditCodeService extends Disposable implements IEditCodeService { this._llmMessageService.abort(streamRequestId) diffZone._streamState = { isStreaming: false, } - this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) + this._onDidChangeStreaming.fire({ uri }) } _undoHistory(uri: URI) { @@ -1737,7 +1749,7 @@ class EditCodeService extends Disposable implements IEditCodeService { // remove a batch of diffareas all at once (and handle accept/reject of their diffs) - public removeDiffAreas({ uri, removeCtrlKs, behavior }: { uri: URI, removeCtrlKs: boolean, behavior: 'reject' | 'accept' }) { + public removeDiffAreas({ uri, removeCtrlKs, behavior }: { uri: URI, removeCtrlKs: boolean, behavior: 'reject' | 'keep' }) { const diffareaids = this.diffAreasOfURI[uri.fsPath] if (diffareaids.size === 0) return // do nothing @@ -1750,7 +1762,7 @@ class EditCodeService extends Disposable implements IEditCodeService { if (diffArea.type == 'DiffZone') { if (behavior === 'reject') this._revertAndDeleteDiffZone(diffArea) - else if (behavior === 'accept') this._deleteDiffZone(diffArea) + else if (behavior === 'keep') this._deleteDiffZone(diffArea) } else if (diffArea.type === 'CtrlKZone' && removeCtrlKs) { this._deleteCtrlKZone(diffArea) diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx index c7f4b52c..d279c631 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx @@ -43,7 +43,7 @@ const CopyButton = ({ codeStr }: { codeStr: string }) => { -const ApplyButton = ({ codeStr }: { codeStr: string }) => { +const ApplyButton = ({ codeStr, codeBoxId }: { codeStr: string, codeBoxId: string }) => { const accessor = useAccessor() const editCodeService = accessor.get('IEditCodeService') @@ -51,13 +51,15 @@ const ApplyButton = ({ codeStr }: { codeStr: string }) => { const onApply = useCallback(() => { - - editCodeService.startApplying({ + const diffareaid = editCodeService.startApplying({ from: 'ClickApply', type: 'searchReplace', applyStr: codeStr, }) + metricsService.capture('Apply Code', { length: codeStr.length }) // capture the length only + + }, [metricsService, editCodeService, codeStr]) const isSingleLine = !codeStr.includes('\n') @@ -76,9 +78,9 @@ const ApplyButton = ({ codeStr }: { codeStr: string }) => { -export const ApplyBlockHoverButtons = ({ codeStr }: { codeStr: string }) => { +export const ApplyBlockHoverButtons = ({ codeStr, codeBoxId }: { codeStr: string, codeBoxId: string | null }) => { return <> - + {codeBoxId !== null && } } diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index 6f737ccb..b5378de7 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -13,7 +13,7 @@ import { ApplyBlockHoverButtons } from './ApplyBlockHoverButtons.js' type ApplyBoxLocation = ChatMessageLocation & { tokenIdx: string } -const getApplyBoxId = ({ threadId, messageIdx, tokenIdx }: ApplyBoxLocation) => { +const getCodeBoxId = ({ threadId, messageIdx, tokenIdx }: ApplyBoxLocation) => { return `${threadId}-${messageIdx}-${tokenIdx}` } @@ -45,10 +45,16 @@ const RenderToken = ({ token, nested = false, noSpace = false, chatMessageLocati if (t.type === "code") { + const codeBoxId = chatMessageLocation ? getCodeBoxId({ + threadId: chatMessageLocation.threadId, + messageIdx: chatMessageLocation.messageIdx, + tokenIdx: tokenIdx, + }) : null + return } + buttonsOnHover={} /> } diff --git a/src/vs/workbench/contrib/void/common/voidFileService.ts b/src/vs/workbench/contrib/void/common/voidFileService.ts index 668f1869..a7c25631 100644 --- a/src/vs/workbench/contrib/void/common/voidFileService.ts +++ b/src/vs/workbench/contrib/void/common/voidFileService.ts @@ -20,7 +20,7 @@ export interface IVoidFileService { readonly _serviceBrand: undefined; readFile(uri: URI, range?: { startLineNumber: number, endLineNumber: number }): Promise; - + readModel(uri: URI, range?: { startLineNumber: number, endLineNumber: number }): string | null; } export const IVoidFileService = createDecorator('VoidFileService'); @@ -39,7 +39,7 @@ export class VoidFileService implements IVoidFileService { readFile = async (uri: URI, range?: { startLineNumber: number, endLineNumber: number }): Promise => { // attempt to read the model - const modelResult = await this._readModel(uri, range); + const modelResult = this.readModel(uri, range); if (modelResult) return modelResult; // if no model, read the raw file @@ -71,7 +71,7 @@ export class VoidFileService implements IVoidFileService { } - _readModel = async (uri: URI, range?: { startLineNumber: number, endLineNumber: number }): Promise => { + readModel = (uri: URI, range?: { startLineNumber: number, endLineNumber: number }): string | null => { // read saved model (sometimes null if the user reloads application) let model = this.modelService.getModel(uri); From 86dfc5521d1c88a6a28b091beca375ed447d19d3 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 21 Feb 2025 20:10:02 -0800 Subject: [PATCH 16/41] more progress (still broken) + fix weird _useThread() + rm initStreamingDiffZoneId --- .../contrib/void/browser/chatThreadService.ts | 49 ++++++------ .../contrib/void/browser/editCodeService.ts | 77 +++++++++++++------ .../contrib/void/browser/quickEditActions.ts | 1 - .../src/markdown/ApplyBlockHoverButtons.tsx | 40 +++++++++- .../src/quick-edit-tsx/QuickEditChat.tsx | 26 +++---- .../react/src/sidebar-tsx/SidebarChat.tsx | 13 ++-- .../void/browser/react/src/util/services.tsx | 15 ++++ .../contrib/void/browser/sidebarActions.ts | 10 +-- 8 files changed, 150 insertions(+), 81 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 1526406d..c325fc8e 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -162,9 +162,12 @@ export interface IChatThreadService { isFocusingMessage(): boolean; setFocusedMessageIdx(messageIdx: number | undefined): void; - // _useFocusedStagingState(messageIdx?: number | undefined): readonly [StagingInfo, (stagingInfo: StagingInfo) => void]; - _useCurrentThreadState(): readonly [ThreadType['state'], (newState: Partial) => void]; - _useCurrentMessageState(messageIdx: number): readonly [UserMessageState, (newState: Partial) => void]; + // exposed getters/setters + getCurrentMessageState: (messageIdx: number) => UserMessageState + setCurrentMessageState: (messageIdx: number, newState: Partial) => void + getCurrentThreadStagingSelections: () => StagingSelectionItem[] + setCurrentThreadStagingSelections: (stagingSelections: StagingSelectionItem[]) => void + // call to edit a message editUserMessageAndStreamResponse({ userMessage, chatMode, messageIdx }: { userMessage: string, chatMode: ChatMode, messageIdx: number }): Promise; @@ -622,33 +625,27 @@ class ChatThreadService extends Disposable implements IChatThreadService { } + getCurrentThreadStagingSelections = () => { + return this.getCurrentThread().state.stagingSelections + } + + setCurrentThreadStagingSelections = (stagingSelections: StagingSelectionItem[]) => { + this._setCurrentThreadState({ stagingSelections }) + } + // gets `staging` and `setStaging` of the currently focused element, given the index of the currently selected message (or undefined if no message is selected) - _useCurrentMessageState(messageIdx: number) { - - const thread = this.getCurrentThread() - const messages = thread.messages - const currMessage = messages[messageIdx] - - if (currMessage.role !== 'user') { - return [defaultMessageState, (s: any) => { }] as const - } - - const state = currMessage.state - const setState = (newState: Partial) => this._setCurrentMessageState(newState, messageIdx) - - return [state, setState] as const - + getCurrentMessageState(messageIdx: number): UserMessageState { + const currMessage = this.getCurrentThread()?.messages?.[messageIdx] + if (!currMessage || currMessage.role !== 'user') return defaultMessageState + return currMessage.state + } + setCurrentMessageState(messageIdx: number, newState: Partial) { + const currMessage = this.getCurrentThread()?.messages?.[messageIdx] + if (!currMessage || currMessage.role !== 'user') return + this._setCurrentMessageState(newState, messageIdx) } - _useCurrentThreadState() { - const thread = this.getCurrentThread() - - const state = thread.state - const setState = this._setCurrentThreadState.bind(this) - - return [state, setState] as const - } } diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 228d7a2f..657584ed 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -248,10 +248,17 @@ type StreamLocationMutable = { line: number, col: number, addedSplitYet: boolean export interface IEditCodeService { readonly _serviceBrand: undefined; - startApplying(opts: StartApplyingOpts): number | void; - interruptStreaming(diffareaid: number): void; + startApplying(opts: StartApplyingOpts): URI | null; + addCtrlKZone(opts: AddCtrlKOpts): number | undefined; removeCtrlKZone(opts: { diffareaid: number }): void; + + isDiffZoneStreaming(opts: { diffareaid: number }): boolean; + isCtrlKZoneStreaming(opts: { diffareaid: number }): boolean; + + interruptDiffZoneStreaming(opts: { diffareaid: number }): void; + interruptCtrlKStreaming(opts: { diffareaid: number }): void; + // testDiffs(): void; } @@ -274,6 +281,7 @@ class EditCodeService extends Disposable implements IEditCodeService { private readonly _onDidAddOrDeleteDiffZones = new Emitter<{ uri: URI }>(); + constructor( // @IHistoryService private readonly _historyService: IHistoryService, // history service is the history of pressing alt left/right @ICodeEditorService private readonly _editorService: ICodeEditorService, @@ -309,7 +317,7 @@ class EditCodeService extends Disposable implements IEditCodeService { // when a stream starts or ends let removeAcceptRejectAllUI: (() => void) | null = null - const onChangeUriState = () => { + const changeUriState = () => { const uri = model.uri const diffZones = [...this.diffAreasOfURI[uri.fsPath].values()] .map(diffareaid => this.diffAreaOfId[diffareaid]) @@ -322,8 +330,8 @@ class EditCodeService extends Disposable implements IEditCodeService { removeAcceptRejectAllUI = null } } - this._register(this._onDidAddOrDeleteDiffZones.event(({ uri: uri_ }) => { if (uri_.fsPath === model.uri.fsPath) onChangeUriState() })) - this._register(this._onDidChangeStreaming.event(({ uri: uri_ }) => { if (uri_.fsPath === model.uri.fsPath) onChangeUriState() })) + this._register(this._onDidAddOrDeleteDiffZones.event(({ uri: uri_ }) => { if (uri_.fsPath === model.uri.fsPath) changeUriState() })) + this._register(this._onDidChangeStreaming.event(({ uri: uri_ }) => { if (uri_.fsPath === model.uri.fsPath) changeUriState() })) } // initialize all existing models + initialize when a new model mounts for (let model of this._modelService.getModels()) { initializeModel(model) } @@ -526,7 +534,6 @@ class EditCodeService extends Disposable implements IEditCodeService { mountCtrlK(domNode, accessor, { diffareaid: ctrlKZone.diffareaid, - initStreamingDiffZoneId: ctrlKZone._linkedStreamingDiffZone, textAreaRef: (r) => { textAreaRef.current = r @@ -1198,19 +1205,15 @@ class EditCodeService extends Disposable implements IEditCodeService { public startApplying(opts: StartApplyingOpts) { - if (opts.type === 'rewrite') { - const addedDiffZone = this._initializeWriteoverStream(opts) - return addedDiffZone?.diffareaid + const addedDiffArea = this._initializeWriteoverStream(opts) + return addedDiffArea?._URI ?? null } - else if (opts.type === 'searchReplace') { - const addedDiffZone = this._initializeSearchAndReplaceStream(opts) - return addedDiffZone?.diffareaid + const addedDiffArea = this._initializeSearchAndReplaceStream(opts) + return addedDiffArea?._URI ?? null } - - return undefined - + return null } @@ -1708,18 +1711,46 @@ class EditCodeService extends Disposable implements IEditCodeService { this._undoRedoService.undo(uri) } - // call this outside undo/redo (it calls undo). this is only for aborting a diffzone stream - interruptStreaming(diffareaid: number) { - const diffArea = this.diffAreaOfId[diffareaid] - if (!diffArea) return - if (diffArea.type !== 'DiffZone') return - if (!diffArea._streamState.isStreaming) return - this._stopIfStreaming(diffArea) - this._undoHistory(diffArea._URI) + + + isCtrlKZoneStreaming({ diffareaid }: { diffareaid: number }) { + const ctrlKZone = this.diffAreaOfId[diffareaid] + if (!ctrlKZone) return false + if (ctrlKZone.type !== 'CtrlKZone') return false + return !!ctrlKZone._linkedStreamingDiffZone } + isDiffZoneStreaming({ diffareaid }: { diffareaid: number }) { + const diffZone = this.diffAreaOfId[diffareaid] + if (diffZone?.type !== 'DiffZone') return false + return diffZone._streamState.isStreaming + } + + + // call this outside undo/redo (it calls undo). this is only for aborting a diffzone stream + interruptDiffZoneStreaming({ diffareaid }: { diffareaid: number }) { + const diffZone = this.diffAreaOfId[diffareaid] + if (diffZone?.type !== 'DiffZone') return + if (!diffZone._streamState.isStreaming) return + + this._stopIfStreaming(diffZone) + this._undoHistory(diffZone._URI) + } + + interruptCtrlKStreaming({ diffareaid }: { diffareaid: number }) { + const ctrlKZone = this.diffAreaOfId[diffareaid] + if (ctrlKZone?.type !== 'CtrlKZone') return + if (!ctrlKZone._linkedStreamingDiffZone) return + + const linkedStreamingDiffZone = this.diffAreaOfId[ctrlKZone._linkedStreamingDiffZone] + if (!linkedStreamingDiffZone) return + if (linkedStreamingDiffZone.type !== 'DiffZone') return + + this.interruptDiffZoneStreaming({ diffareaid: linkedStreamingDiffZone.diffareaid }) + + } diff --git a/src/vs/workbench/contrib/void/browser/quickEditActions.ts b/src/vs/workbench/contrib/void/browser/quickEditActions.ts index 1099e74c..da8f5c55 100644 --- a/src/vs/workbench/contrib/void/browser/quickEditActions.ts +++ b/src/vs/workbench/contrib/void/browser/quickEditActions.ts @@ -17,7 +17,6 @@ import { IMetricsService } from '../common/metricsService.js'; export type QuickEditPropsType = { diffareaid: number, - initStreamingDiffZoneId: number | null, textAreaRef: (ref: HTMLTextAreaElement | null) => void; onChangeHeight: (height: number) => void; onChangeText: (text: string) => void; diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx index d279c631..0c6176ff 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx @@ -1,5 +1,7 @@ import { useState, useEffect, useCallback } from 'react' -import { useAccessor } from '../util/services.js' +import { useAccessor, useIsURIStreaming } from '../util/services.js' +import { useRefState } from '../util/helpers.js' +import { isFeatureNameDisabled } from '../../../../common/voidSettingsTypes.js' enum CopyButtonText { Idle = 'Copy', @@ -50,7 +52,15 @@ const ApplyButton = ({ codeStr, codeBoxId }: { codeStr: string, codeBoxId: strin const metricsService = accessor.get('IMetricsService') - const onApply = useCallback(() => { + const [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone] = useRefState(initStreamingDiffZoneId) + const isStreaming = currStreamingDiffZoneRef.current !== null + const isDisabled = !!isFeatureNameDisabled('Ctrl+K', settingsState) + + useIsDiffZoneStreaming(isDiffAreaStreaming) + + + const onSubmit = useCallback(() => { + const diffareaid = editCodeService.startApplying({ from: 'ClickApply', type: 'searchReplace', @@ -60,7 +70,31 @@ const ApplyButton = ({ codeStr, codeBoxId }: { codeStr: string, codeBoxId: strin metricsService.capture('Apply Code', { length: codeStr.length }) // capture the length only - }, [metricsService, editCodeService, codeStr]) + + if (isDisabled) return + if (currStreamingDiffZoneRef.current !== null) return + textAreaFnsRef.current?.disable() + + const id = editCodeService.startApplying({ + from: 'QuickEdit', + type: 'rewrite', + diffareaid: diffareaid, + }) + setCurrentlyStreamingDiffZone(id ?? null) + }, [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone, isDisabled, editCodeService, diffareaid]) + + const onInterrupt = useCallback(() => { + if (currStreamingDiffZoneRef.current === null) return + editCodeService.interruptStreaming(currStreamingDiffZoneRef.current) + setCurrentlyStreamingDiffZone(null) + textAreaFnsRef.current?.enable() + }, [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone, editCodeService]) + + + + + + const isSingleLine = !codeStr.includes('\n') diff --git a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx index f79c9af9..f00bc251 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------*/ import React, { FormEvent, useCallback, useEffect, useRef, useState } from 'react'; -import { useSettingsState, useSidebarState, useChatThreadsState, useQuickEditState, useAccessor } from '../util/services.js'; +import { useSettingsState, useSidebarState, useChatThreadsState, useQuickEditState, useAccessor, useIsCtrlKZoneStreaming } from '../util/services.js'; import { TextAreaFns, VoidInputBox2 } from '../util/inputs.js'; import { QuickEditPropsType } from '../../../quickEditActions.js'; import { ButtonStop, ButtonSubmit, IconX, VoidChatArea } from '../sidebar-tsx/SidebarChat.js'; @@ -16,7 +16,6 @@ import { isFeatureNameDisabled } from '../../../../../../../workbench/contrib/vo export const QuickEditChat = ({ diffareaid, - initStreamingDiffZoneId, onChangeHeight, onChangeText: onChangeText_, textAreaRef: textAreaRef_, @@ -49,28 +48,25 @@ export const QuickEditChat = ({ const [instructionsAreEmpty, setInstructionsAreEmpty] = useState(!(initText ?? '')) // the user's instructions const isDisabled = instructionsAreEmpty || !!isFeatureNameDisabled('Ctrl+K', settingsState) - const [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone] = useRefState(initStreamingDiffZoneId) - const isStreaming = currStreamingDiffZoneRef.current !== null + const isStreamingRefState = useIsCtrlKZoneStreaming(diffareaid) const onSubmit = useCallback(() => { if (isDisabled) return - if (currStreamingDiffZoneRef.current !== null) return + if (isStreamingRefState.current) return textAreaFnsRef.current?.disable() - const id = editCodeService.startApplying({ + editCodeService.startApplying({ from: 'QuickEdit', - type:'rewrite', - diffareaid: diffareaid, + type: 'rewrite', + diffareaid, }) - setCurrentlyStreamingDiffZone(id ?? null) - }, [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone, isDisabled, editCodeService, diffareaid]) + }, [isStreamingRefState, isDisabled, editCodeService, diffareaid]) const onInterrupt = useCallback(() => { - if (currStreamingDiffZoneRef.current === null) return - editCodeService.interruptStreaming(currStreamingDiffZoneRef.current) - setCurrentlyStreamingDiffZone(null) + if (!isStreamingRefState.current ) return + editCodeService.interruptCtrlKStreaming({ diffareaid }) textAreaFnsRef.current?.enable() - }, [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone, editCodeService]) + }, [isStreamingRefState, editCodeService]) const onX = useCallback(() => { @@ -89,7 +85,7 @@ export const QuickEditChat = ({ onSubmit={onSubmit} onAbort={onInterrupt} onClose={onX} - isStreaming={isStreaming} + isStreaming={isStreamingRefState.current} isDisabled={isDisabled} featureName="Ctrl+K" className="py-2 w-full" diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx index 30351697..fd2d8c9e 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx @@ -557,11 +557,11 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM let setStagingSelections = (s: StagingSelectionItem[]) => { } if (messageIdx !== undefined) { - const [_state, _setState] = chatThreadsService._useCurrentMessageState(messageIdx) + const _state = chatThreadsService.getCurrentMessageState(messageIdx) isBeingEdited = _state.isBeingEdited - setIsBeingEdited = (v) => _setState({ isBeingEdited: v }) stagingSelections = _state.stagingSelections - setStagingSelections = (s) => { _setState({ stagingSelections: s }) } + setIsBeingEdited = (v) => chatThreadsService.setCurrentMessageState(messageIdx, { isBeingEdited: v }) + setStagingSelections = (s) => chatThreadsService.setCurrentMessageState(messageIdx, { stagingSelections: s }) } @@ -780,9 +780,8 @@ export const SidebarChat = () => { const currentThread = chatThreadsService.getCurrentThread() const previousMessages = currentThread?.messages ?? [] - const [_state, _setState] = chatThreadsService._useCurrentThreadState() - const selections = _state.stagingSelections - const setSelections = (s: StagingSelectionItem[]) => { _setState({ stagingSelections: s }) } + const selections = chatThreadsService.getCurrentThread().state.stagingSelections + const setSelections = (s: StagingSelectionItem[]) => { chatThreadsService.setCurrentThreadStagingSelections(s) } // stream state const currThreadStreamState = useChatThreadsStreamState(chatThreadsState.currentThreadId) @@ -818,7 +817,7 @@ export const SidebarChat = () => { textAreaFnsRef.current?.setValue('') textAreaRef.current?.focus() // focus input after submit - }, [chatThreadsService, isDisabled, isStreaming, textAreaRef, textAreaFnsRef, selections, setSelections]) + }, [chatThreadsService, isDisabled, isStreaming, textAreaRef, textAreaFnsRef, setSelections]) const onAbort = () => { const threadId = currentThread.id diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx index 2d516ace..7ca177ce 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx @@ -47,6 +47,7 @@ import { IEnvironmentService } from '../../../../../../../platform/environment/c import { IConfigurationService } from '../../../../../../../platform/configuration/common/configuration.js' 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' @@ -353,3 +354,17 @@ export const useIsDark = () => { return isDark } + + + + +export const useIsCtrlKZoneStreaming = (diffareaid: number) => { + + return { current: true } + +} + + +export const useIsDiffZoneStreaming = (uri: URI) => { + +} diff --git a/src/vs/workbench/contrib/void/browser/sidebarActions.ts b/src/vs/workbench/contrib/void/browser/sidebarActions.ts index 39eb8381..722eaa57 100644 --- a/src/vs/workbench/contrib/void/browser/sidebarActions.ts +++ b/src/vs/workbench/contrib/void/browser/sidebarActions.ts @@ -141,13 +141,11 @@ registerAction2(class extends Action2 { let setSelections = (s: StagingSelectionItem[]) => { } if (focusedMessageIdx === undefined) { - const [state, setState] = chatThreadService._useCurrentThreadState() - selections = state.stagingSelections - setSelections = (s) => setState({ stagingSelections: s }) + selections = chatThreadService.getCurrentThreadStagingSelections() + setSelections = (s: StagingSelectionItem[]) => chatThreadService.setCurrentThreadStagingSelections(s) } else { - const [state, setState] = chatThreadService._useCurrentMessageState(focusedMessageIdx) - selections = state.stagingSelections - setSelections = (s) => setState({ stagingSelections: s }) + selections = chatThreadService.getCurrentMessageState(focusedMessageIdx).stagingSelections + setSelections = (s) => chatThreadService.setCurrentMessageState(focusedMessageIdx, { stagingSelections: s }) } // if matches with existing selection, overwrite (since text may change) From 9005de65a6dd5aa7a79eb3b99391bb65c468e999 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 21 Feb 2025 20:53:09 -0800 Subject: [PATCH 17/41] quick edit stream state? --- .../contrib/void/browser/editCodeService.ts | 103 +++++++++++------- .../src/markdown/ApplyBlockHoverButtons.tsx | 58 ++++------ .../react/src/markdown/ChatMarkdownRender.tsx | 3 +- .../src/quick-edit-tsx/QuickEditChat.tsx | 20 ++-- .../react/src/sidebar-tsx/SidebarChat.tsx | 4 +- .../void/browser/react/src/util/services.tsx | 53 +++++---- 6 files changed, 136 insertions(+), 105 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 657584ed..0d1cc369 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -35,7 +35,7 @@ import { filenameToVscodeLanguage } from './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 } from '../../../../base/common/event.js'; +import { Emitter, Event } 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'; @@ -66,7 +66,6 @@ registerColor('void.sweepIdxBG', configOfBG(sweepIdxBG), '', true); - const getLeadingWhitespacePx = (editor: ICodeEditor, startLine: number): number => { const model = editor.getModel(); @@ -122,6 +121,7 @@ const findTextInCode = (text: string, fileContents: string, startingAtLine?: num } +type AcceptRejectAllState = 'idle' | 'acceptRejectAll' | 'streaming' export type StartApplyingOpts = { @@ -142,7 +142,7 @@ export type AddCtrlKOpts = { } // // TODO diffArea should be removed if we just discovered it has no more diffs in it -// for (const diffareaid of this.diffAreasOfURI[uri.fsPath]) { +// for (const diffareaid of this.diffAreasOfURI[uri.fsPath] || []) { // const diffArea = this.diffAreaOfId[diffareaid] // if (Object.keys(diffArea._diffOfId).length === 0 && !diffArea._sweepState.isStreaming) { // const { onFinishEdit } = this._addToHistory(uri) @@ -197,10 +197,12 @@ type DiffZone = { isStreaming: true; streamRequestIdRef: { current: string | null }; line: number; + applyBoxId?: string; } | { isStreaming: false; streamRequestIdRef?: undefined; line?: undefined; + applyBoxId?: undefined; }; editorId?: undefined; linkedStreamingDiffZone?: undefined; @@ -253,11 +255,15 @@ export interface IEditCodeService { addCtrlKZone(opts: AddCtrlKOpts): number | undefined; removeCtrlKZone(opts: { diffareaid: number }): void; - isDiffZoneStreaming(opts: { diffareaid: number }): boolean; + // CtrlKZone streaming state isCtrlKZoneStreaming(opts: { diffareaid: number }): boolean; - - interruptDiffZoneStreaming(opts: { diffareaid: number }): void; interruptCtrlKStreaming(opts: { diffareaid: number }): void; + onDidChangeCtrlKZoneStreaming: Event<{ uri: URI; diffareaid: number }>; + + // // DiffZone streaming state + // isApplyBoxIdStreaming(opts: { applyBoxId: string }): boolean; + // interruptApplyBoxId(opts: { applyBoxId: string }): void; + // onDidChangeApplyBoxIdStreaming: Event<{ applyBoxId: string }>; // testDiffs(): void; } @@ -277,10 +283,14 @@ class EditCodeService extends Disposable implements IEditCodeService { // only applies to diffZones // streamingDiffZones: Set = new Set() - private readonly _onDidChangeStreaming = new Emitter<{ uri: URI }>(); - private readonly _onDidAddOrDeleteDiffZones = new Emitter<{ uri: URI }>(); + private readonly _onDidChangeDiffZoneStreaming = new Emitter<{ uri: URI; diffareaid: number }>(); + private readonly _onDidChangeCtrlKZoneStreaming = new Emitter<{ uri: URI; diffareaid: number }>(); + private readonly _onDidAddOrDeleteDiffZone = new Emitter<{ uri: URI }>(); + private readonly _onDidChangeAcceptRejectAllState = new Emitter<{ uri: URI, state: AcceptRejectAllState }>(); // was going to be used, but decided not to + onDidChangeDiffZoneStreaming = this._onDidChangeDiffZoneStreaming.event + onDidChangeCtrlKZoneStreaming = this._onDidChangeCtrlKZoneStreaming.event constructor( // @IHistoryService private readonly _historyService: IHistoryService, // history service is the history of pressing alt left/right @@ -315,23 +325,33 @@ class EditCodeService extends Disposable implements IEditCodeService { }) ) - // when a stream starts or ends + // add the accept|reject UI here let removeAcceptRejectAllUI: (() => void) | null = null + this._register(this._onDidChangeAcceptRejectAllState.event(({ uri, state }) => { + if (state === 'acceptRejectAll' && !removeAcceptRejectAllUI) { + removeAcceptRejectAllUI = this._addAcceptRejectAllUI(uri) ?? null + } else { + removeAcceptRejectAllUI?.() + removeAcceptRejectAllUI = null + } + })) + + // when a stream starts or ends const changeUriState = () => { const uri = model.uri const diffZones = [...this.diffAreasOfURI[uri.fsPath].values()] .map(diffareaid => this.diffAreaOfId[diffareaid]) .filter(diffArea => !!diffArea && diffArea.type === 'DiffZone') const isStreaming = diffZones.find(diffZone => !!diffZone._streamState.isStreaming) - if (diffZones.length !== 0 && !isStreaming && !removeAcceptRejectAllUI) { - removeAcceptRejectAllUI = this._addAcceptRejectAllUI(uri) ?? null - } else { - removeAcceptRejectAllUI?.() - removeAcceptRejectAllUI = null - } + + const state: AcceptRejectAllState = isStreaming ? 'streaming' : (diffZones.length === 0 ? 'idle' : 'acceptRejectAll') + this._onDidChangeAcceptRejectAllState.fire({ uri, state }) } - this._register(this._onDidAddOrDeleteDiffZones.event(({ uri: uri_ }) => { if (uri_.fsPath === model.uri.fsPath) changeUriState() })) - this._register(this._onDidChangeStreaming.event(({ uri: uri_ }) => { if (uri_.fsPath === model.uri.fsPath) changeUriState() })) + this._register(this._onDidChangeDiffZoneStreaming.event(({ uri: uri_ }) => { if (uri_.fsPath === model.uri.fsPath) changeUriState() })) + this._register(this._onDidAddOrDeleteDiffZone.event(({ uri: uri_ }) => { if (uri_.fsPath === model.uri.fsPath) changeUriState() })) + + + } // initialize all existing models + initialize when a new model mounts for (let model of this._modelService.getModels()) { initializeModel(model) } @@ -412,7 +432,7 @@ class EditCodeService extends Disposable implements IEditCodeService { private _addDiffAreaStylesToURI = (uri: URI) => { const model = this._getModel(uri) - for (const diffareaid of this.diffAreasOfURI[uri.fsPath]) { + for (const diffareaid of this.diffAreasOfURI[uri.fsPath] || []) { const diffArea = this.diffAreaOfId[diffareaid] if (diffArea.type === 'DiffZone') { @@ -441,7 +461,7 @@ class EditCodeService extends Disposable implements IEditCodeService { private _computeDiffsAndAddStylesToURI = (uri: URI) => { const fullFileText = this._readURI(uri) ?? '' - for (const diffareaid of this.diffAreasOfURI[uri.fsPath]) { + for (const diffareaid of this.diffAreasOfURI[uri.fsPath] || []) { const diffArea = this.diffAreaOfId[diffareaid] if (diffArea.type !== 'DiffZone') continue @@ -465,7 +485,7 @@ class EditCodeService extends Disposable implements IEditCodeService { // find all diffzones that aren't streaming const diffZones: DiffZone[] = [] - for (let diffareaid of this.diffAreasOfURI[uri.fsPath]) { + for (let diffareaid of this.diffAreasOfURI[uri.fsPath] || []) { const diffArea = this.diffAreaOfId[diffareaid] if (diffArea.type !== 'DiffZone') continue if (diffArea._streamState.isStreaming) continue @@ -583,7 +603,7 @@ class EditCodeService extends Disposable implements IEditCodeService { private _refreshCtrlKInputs = async (uri: URI) => { - for (const diffareaid of this.diffAreasOfURI[uri.fsPath]) { + for (const diffareaid of this.diffAreasOfURI[uri.fsPath] || []) { const diffArea = this.diffAreaOfId[diffareaid] if (diffArea.type !== 'CtrlKZone') continue if (!diffArea._mountInfo) { @@ -852,7 +872,7 @@ class EditCodeService extends Disposable implements IEditCodeService { } this.diffAreasOfURI[uri.fsPath].add(diffareaid) } - this._onDidAddOrDeleteDiffZones.fire({ uri }) + this._onDidAddOrDeleteDiffZone.fire({ uri }) // restore file content const numLines = this._getNumLines(uri) @@ -910,7 +930,7 @@ class EditCodeService extends Disposable implements IEditCodeService { // clears all Diffs (and their styles) and all styles of DiffAreas, etc private _clearAllEffects(uri: URI) { - for (let diffareaid of this.diffAreasOfURI[uri.fsPath]) { + for (let diffareaid of this.diffAreasOfURI[uri.fsPath] || []) { const diffArea = this.diffAreaOfId[diffareaid] this._clearAllDiffAreaEffects(diffArea) } @@ -922,7 +942,7 @@ class EditCodeService extends Disposable implements IEditCodeService { this._clearAllDiffAreaEffects(diffZone) delete this.diffAreaOfId[diffZone.diffareaid] this.diffAreasOfURI[diffZone._URI.fsPath].delete(diffZone.diffareaid.toString()) - this._onDidAddOrDeleteDiffZones.fire({ uri: diffZone._URI }) + this._onDidAddOrDeleteDiffZone.fire({ uri: diffZone._URI }) } private _deleteTrackingZone(trackingZone: TrackingZone) { @@ -1221,7 +1241,7 @@ class EditCodeService extends Disposable implements IEditCodeService { private _findOverlappingDiffArea({ startLine, endLine, uri, filter }: { startLine: number, endLine: number, uri: URI, filter?: (diffArea: DiffArea) => boolean }): DiffArea | null { // check if there's overlap with any other diffAreas and return early if there is - for (const diffareaid of this.diffAreasOfURI[uri.fsPath]) { + for (const diffareaid of this.diffAreasOfURI[uri.fsPath] || []) { const diffArea = this.diffAreaOfId[diffareaid] if (!diffArea) continue if (!filter?.(diffArea)) continue @@ -1305,8 +1325,8 @@ class EditCodeService extends Disposable implements IEditCodeService { _removeStylesFns: new Set(), } const diffZone = this._addDiffArea(adding) - this._onDidChangeStreaming.fire({ uri }) - this._onDidAddOrDeleteDiffZones.fire({ uri }) + this._onDidChangeDiffZoneStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) + this._onDidAddOrDeleteDiffZone.fire({ uri }) if (from === 'QuickEdit') { const { diffareaid } = opts @@ -1314,6 +1334,7 @@ class EditCodeService extends Disposable implements IEditCodeService { if (ctrlKZone.type !== 'CtrlKZone') return ctrlKZone._linkedStreamingDiffZone = diffZone.diffareaid + this._onDidChangeCtrlKZoneStreaming.fire({ uri, diffareaid: ctrlKZone.diffareaid }) } // now handle messages @@ -1347,12 +1368,13 @@ class EditCodeService extends Disposable implements IEditCodeService { const onDone = () => { diffZone._streamState = { isStreaming: false, } - this._onDidChangeStreaming.fire({ uri }) + this._onDidChangeDiffZoneStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) if (from === 'QuickEdit') { const ctrlKZone = this.diffAreaOfId[opts.diffareaid] as CtrlKZone ctrlKZone._linkedStreamingDiffZone = null + this._onDidChangeCtrlKZoneStreaming.fire({ uri, diffareaid: ctrlKZone.diffareaid }) this._deleteCtrlKZone(ctrlKZone) } this._refreshStylesAndDiffsInURI(uri) @@ -1476,8 +1498,8 @@ class EditCodeService extends Disposable implements IEditCodeService { _removeStylesFns: new Set(), } const diffZone = this._addDiffArea(adding) - this._onDidChangeStreaming.fire({ uri }) - this._onDidAddOrDeleteDiffZones.fire({ uri }) + this._onDidChangeDiffZoneStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) + this._onDidAddOrDeleteDiffZone.fire({ uri }) const revertAndContinueHistory = () => { @@ -1516,7 +1538,7 @@ class EditCodeService extends Disposable implements IEditCodeService { const onDone = () => { diffZone._streamState = { isStreaming: false, } - this._onDidChangeStreaming.fire({ uri }) + this._onDidChangeDiffZoneStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) this._refreshStylesAndDiffsInURI(uri) // delete the tracking zones @@ -1704,7 +1726,7 @@ class EditCodeService extends Disposable implements IEditCodeService { this._llmMessageService.abort(streamRequestId) diffZone._streamState = { isStreaming: false, } - this._onDidChangeStreaming.fire({ uri }) + this._onDidChangeDiffZoneStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) } _undoHistory(uri: URI) { @@ -1715,6 +1737,12 @@ class EditCodeService extends Disposable implements IEditCodeService { + isDiffZoneStreaming({ diffareaid }: { diffareaid: number }) { + const diffZone = this.diffAreaOfId[diffareaid] + if (diffZone?.type !== 'DiffZone') return false + return diffZone._streamState.isStreaming + } + isCtrlKZoneStreaming({ diffareaid }: { diffareaid: number }) { const ctrlKZone = this.diffAreaOfId[diffareaid] if (!ctrlKZone) return false @@ -1722,14 +1750,7 @@ class EditCodeService extends Disposable implements IEditCodeService { return !!ctrlKZone._linkedStreamingDiffZone } - isDiffZoneStreaming({ diffareaid }: { diffareaid: number }) { - const diffZone = this.diffAreaOfId[diffareaid] - if (diffZone?.type !== 'DiffZone') return false - return diffZone._streamState.isStreaming - } - - // call this outside undo/redo (it calls undo). this is only for aborting a diffzone stream interruptDiffZoneStreaming({ diffareaid }: { diffareaid: number }) { const diffZone = this.diffAreaOfId[diffareaid] if (diffZone?.type !== 'DiffZone') return @@ -1739,6 +1760,7 @@ class EditCodeService extends Disposable implements IEditCodeService { this._undoHistory(diffZone._URI) } + // diffareaid of the ctrlKZone (even though the stream state is dictated by the linked diffZone) interruptCtrlKStreaming({ diffareaid }: { diffareaid: number }) { const ctrlKZone = this.diffAreaOfId[diffareaid] if (ctrlKZone?.type !== 'CtrlKZone') return @@ -1749,14 +1771,11 @@ class EditCodeService extends Disposable implements IEditCodeService { if (linkedStreamingDiffZone.type !== 'DiffZone') return this.interruptDiffZoneStreaming({ diffareaid: linkedStreamingDiffZone.diffareaid }) - } - - // public removeDiffZone(diffZone: DiffZone, behavior: 'reject' | 'accept') { // const uri = diffZone._URI // const { onFinishEdit } = this._addToHistory(uri) diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx index 0c6176ff..6224110b 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx @@ -1,5 +1,5 @@ import { useState, useEffect, useCallback } from 'react' -import { useAccessor, useIsURIStreaming } from '../util/services.js' +import { useAccessor, useIsDiffZoneStreaming } from '../util/services.js' import { useRefState } from '../util/helpers.js' import { isFeatureNameDisabled } from '../../../../common/voidSettingsTypes.js' @@ -52,43 +52,33 @@ const ApplyButton = ({ codeStr, codeBoxId }: { codeStr: string, codeBoxId: strin const metricsService = accessor.get('IMetricsService') - const [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone] = useRefState(initStreamingDiffZoneId) - const isStreaming = currStreamingDiffZoneRef.current !== null - const isDisabled = !!isFeatureNameDisabled('Ctrl+K', settingsState) - - useIsDiffZoneStreaming(isDiffAreaStreaming) - - - const onSubmit = useCallback(() => { - - const diffareaid = editCodeService.startApplying({ - from: 'ClickApply', - type: 'searchReplace', - applyStr: codeStr, - }) - - metricsService.capture('Apply Code', { length: codeStr.length }) // capture the length only + // const isStreaming = useIsDiffZoneStreaming(isDiffAreaStreaming) - if (isDisabled) return - if (currStreamingDiffZoneRef.current !== null) return - textAreaFnsRef.current?.disable() + // const onSubmit = useCallback(() => { - const id = editCodeService.startApplying({ - from: 'QuickEdit', - type: 'rewrite', - diffareaid: diffareaid, - }) - setCurrentlyStreamingDiffZone(id ?? null) - }, [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone, isDisabled, editCodeService, diffareaid]) + // const uri = editCodeService.startApplying({ + // from: 'ClickApply', + // type: 'searchReplace', + // applyStr: codeStr, + // }) - const onInterrupt = useCallback(() => { - if (currStreamingDiffZoneRef.current === null) return - editCodeService.interruptStreaming(currStreamingDiffZoneRef.current) - setCurrentlyStreamingDiffZone(null) - textAreaFnsRef.current?.enable() - }, [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone, editCodeService]) + // metricsService.capture('Apply Code', { length: codeStr.length }) // capture the length only + + + + // if (isStreaming) return + + // setCurrentlyStreamingDiffZone(id ?? null) + // }, [isStreaming, editCodeService]) + + // const onInterrupt = useCallback(() => { + // if (currStreamingDiffZoneRef.current === null) return + // editCodeService.interruptStreaming(currStreamingDiffZoneRef.current) + // setCurrentlyStreamingDiffZone(null) + // textAreaFnsRef.current?.enable() + // }, [isStreaming, editCodeService]) @@ -101,7 +91,7 @@ const ApplyButton = ({ codeStr, codeBoxId }: { codeStr: string, codeBoxId: strin return diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index b5378de7..d6af4843 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -9,6 +9,7 @@ import { BlockCode } from './BlockCode.js' import { ChatMessageLocation, } from '../../../aiRegexService.js' import { nameToVscodeLanguage } from '../../../helpers/detectLanguage.js' import { ApplyBlockHoverButtons } from './ApplyBlockHoverButtons.js' +import { URI } from '../../../../../../../base/common/uri.js' type ApplyBoxLocation = ChatMessageLocation & { tokenIdx: string } @@ -33,7 +34,7 @@ export const CodeSpan = ({ children, className }: { children: React.ReactNode, c } -const RenderToken = ({ token, nested = false, noSpace = false, chatMessageLocation, tokenIdx }: { token: Token | string, nested?: boolean, noSpace?: boolean, chatMessageLocation?: ChatMessageLocation, tokenIdx: string }): JSX.Element => { +const RenderToken = ({ token, nested, noSpace, chatMessageLocation, tokenIdx }: { token: Token | string, nested?: boolean, noSpace?: boolean, chatMessageLocation?: ChatMessageLocation, tokenIdx: string }): JSX.Element => { // deal with built-in tokens first (assume marked token) diff --git a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx index f00bc251..44899741 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------*/ import React, { FormEvent, useCallback, useEffect, useRef, useState } from 'react'; -import { useSettingsState, useSidebarState, useChatThreadsState, useQuickEditState, useAccessor, useIsCtrlKZoneStreaming } from '../util/services.js'; +import { useSettingsState, useSidebarState, useChatThreadsState, useQuickEditState, useAccessor, useCtrlKZoneStreamingState } from '../util/services.js'; import { TextAreaFns, VoidInputBox2 } from '../util/inputs.js'; import { QuickEditPropsType } from '../../../quickEditActions.js'; import { ButtonStop, ButtonSubmit, IconX, VoidChatArea } from '../sidebar-tsx/SidebarChat.js'; @@ -48,11 +48,17 @@ export const QuickEditChat = ({ const [instructionsAreEmpty, setInstructionsAreEmpty] = useState(!(initText ?? '')) // the user's instructions const isDisabled = instructionsAreEmpty || !!isFeatureNameDisabled('Ctrl+K', settingsState) - const isStreamingRefState = useIsCtrlKZoneStreaming(diffareaid) + + const [isStreamingRef, setIsStreamingRef] = useRefState(false) + useCtrlKZoneStreamingState(useCallback((diffareaid2, isStreaming) => { + if (diffareaid !== diffareaid2) return + setIsStreamingRef(isStreaming) + }, [diffareaid, setIsStreamingRef])) + const onSubmit = useCallback(() => { if (isDisabled) return - if (isStreamingRefState.current) return + if (isStreamingRef.current) return textAreaFnsRef.current?.disable() editCodeService.startApplying({ @@ -60,13 +66,13 @@ export const QuickEditChat = ({ type: 'rewrite', diffareaid, }) - }, [isStreamingRefState, isDisabled, editCodeService, diffareaid]) + }, [isStreamingRef, isDisabled, editCodeService, diffareaid]) const onInterrupt = useCallback(() => { - if (!isStreamingRefState.current ) return + if (!isStreamingRef.current) return editCodeService.interruptCtrlKStreaming({ diffareaid }) textAreaFnsRef.current?.enable() - }, [isStreamingRefState, editCodeService]) + }, [isStreamingRef, editCodeService]) const onX = useCallback(() => { @@ -85,7 +91,7 @@ export const QuickEditChat = ({ onSubmit={onSubmit} onAbort={onInterrupt} onClose={onX} - isStreaming={isStreamingRefState.current} + isStreaming={isStreamingRef.current} isDisabled={isDisabled} featureName="Ctrl+K" className="py-2 w-full" diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx index fd2d8c9e..5f7c42ce 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx @@ -552,9 +552,9 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM // global state let isBeingEdited = false - let setIsBeingEdited = (v: boolean) => { } let stagingSelections: StagingSelectionItem[] = [] - let setStagingSelections = (s: StagingSelectionItem[]) => { } + let setIsBeingEdited = (_: boolean) => { } + let setStagingSelections = (_: StagingSelectionItem[]) => { } if (messageIdx !== undefined) { const _state = chatThreadsService.getCurrentMessageState(messageIdx) diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx index 7ca177ce..bf44e045 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx @@ -14,10 +14,6 @@ import { VoidUriState } from '../../../voidUriStateService.js'; import { VoidQuickEditState } from '../../../quickEditStateService.js' import { RefreshModelStateOfProvider } from '../../../../../../../workbench/contrib/void/common/refreshModelService.js' - - - - import { ServicesAccessor } from '../../../../../../../editor/browser/editorExtensions.js'; import { IModelService } from '../../../../../../../editor/common/services/model.js'; import { IClipboardService } from '../../../../../../../platform/clipboard/common/clipboardService.js'; @@ -47,7 +43,6 @@ import { IEnvironmentService } from '../../../../../../../platform/environment/c import { IConfigurationService } from '../../../../../../../platform/configuration/common/configuration.js' 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' @@ -80,6 +75,13 @@ const refreshModelProviderListeners: Set<(p: RefreshableProviderName, s: Refresh let colorThemeState: ColorScheme const colorThemeStateListeners: Set<(s: ColorScheme) => void> = new Set() +const ctrlKZoneStreamingStateListeners: Set<(diffareaid: number, s: boolean) => void> = new Set() + +let diffZoneStreamingState: Record +const diffZoneStreamingStateListeners: Set<(diffareaid: number, state: boolean) => void> = new Set() + + + // must call this before you can use any of the hooks below // this should only be called ONCE! this is the only place you don't need to dispose onDidChange. If you use state.onDidChange anywhere else, make sure to dispose it! let wasCalled = false @@ -163,7 +165,7 @@ export const _registerServices = (accessor: ServicesAccessor) => { refreshModelService.onDidChangeState((providerName) => { refreshModelState = refreshModelService.state refreshModelStateListeners.forEach(l => l(refreshModelState)) - refreshModelProviderListeners.forEach(l => l(providerName, refreshModelState)) + refreshModelProviderListeners.forEach(l => l(providerName, refreshModelState)) // no state }) ) @@ -175,6 +177,15 @@ export const _registerServices = (accessor: ServicesAccessor) => { }) ) + // no state + disposables.push( + editCodeService.onDidChangeCtrlKZoneStreaming(({ diffareaid }) => { + const isStreaming = editCodeService.isCtrlKZoneStreaming({ diffareaid }) + ctrlKZoneStreamingStateListeners.forEach(l => l(diffareaid, isStreaming)) + }) + ) + + return disposables } @@ -341,6 +352,23 @@ export const useRefreshModelListener = (listener: (providerName: RefreshableProv } +export const useCtrlKZoneStreamingState = (listener: (diffareaid: number, s: boolean) => void) => { + useEffect(() => { + ctrlKZoneStreamingStateListeners.add(listener) + return () => { ctrlKZoneStreamingStateListeners.delete(listener) } + }, [listener]) +} + + +export const useIsDiffZoneStreaming = (diffareaid: number) => { + return { current: true } + +} + + + + + export const useIsDark = () => { const [s, ss] = useState(colorThemeState) useEffect(() => { @@ -355,16 +383,3 @@ export const useIsDark = () => { } - - - -export const useIsCtrlKZoneStreaming = (diffareaid: number) => { - - return { current: true } - -} - - -export const useIsDiffZoneStreaming = (uri: URI) => { - -} From 64ac6d4a1254bc65996d163321c60cd72d9736a7 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 21 Feb 2025 21:09:03 -0800 Subject: [PATCH 18/41] ctrlK --- .../contrib/void/browser/editCodeService.ts | 30 +++++++------------ .../src/quick-edit-tsx/QuickEditChat.tsx | 2 +- .../void/browser/react/src/util/services.tsx | 4 +-- 3 files changed, 14 insertions(+), 22 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 0d1cc369..b14c085a 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -287,8 +287,6 @@ class EditCodeService extends Disposable implements IEditCodeService { private readonly _onDidChangeCtrlKZoneStreaming = new Emitter<{ uri: URI; diffareaid: number }>(); private readonly _onDidAddOrDeleteDiffZone = new Emitter<{ uri: URI }>(); - private readonly _onDidChangeAcceptRejectAllState = new Emitter<{ uri: URI, state: AcceptRejectAllState }>(); // was going to be used, but decided not to - onDidChangeDiffZoneStreaming = this._onDidChangeDiffZoneStreaming.event onDidChangeCtrlKZoneStreaming = this._onDidChangeCtrlKZoneStreaming.event @@ -325,19 +323,9 @@ class EditCodeService extends Disposable implements IEditCodeService { }) ) - // add the accept|reject UI here - let removeAcceptRejectAllUI: (() => void) | null = null - this._register(this._onDidChangeAcceptRejectAllState.event(({ uri, state }) => { - if (state === 'acceptRejectAll' && !removeAcceptRejectAllUI) { - removeAcceptRejectAllUI = this._addAcceptRejectAllUI(uri) ?? null - } else { - removeAcceptRejectAllUI?.() - removeAcceptRejectAllUI = null - } - })) - - // when a stream starts or ends - const changeUriState = () => { + // when a stream starts or ends, add/remove the accept|reject UI + let _removeAcceptRejectAllUI: (() => void) | null = null + const updateAcceptRejectAllUI = () => { const uri = model.uri const diffZones = [...this.diffAreasOfURI[uri.fsPath].values()] .map(diffareaid => this.diffAreaOfId[diffareaid]) @@ -345,12 +333,16 @@ class EditCodeService extends Disposable implements IEditCodeService { const isStreaming = diffZones.find(diffZone => !!diffZone._streamState.isStreaming) const state: AcceptRejectAllState = isStreaming ? 'streaming' : (diffZones.length === 0 ? 'idle' : 'acceptRejectAll') - this._onDidChangeAcceptRejectAllState.fire({ uri, state }) + if (state === 'acceptRejectAll' && !_removeAcceptRejectAllUI) { + _removeAcceptRejectAllUI = this._addAcceptRejectAllUI(uri) ?? null + } else { + _removeAcceptRejectAllUI?.() + _removeAcceptRejectAllUI = null + } } - this._register(this._onDidChangeDiffZoneStreaming.event(({ uri: uri_ }) => { if (uri_.fsPath === model.uri.fsPath) changeUriState() })) - this._register(this._onDidAddOrDeleteDiffZone.event(({ uri: uri_ }) => { if (uri_.fsPath === model.uri.fsPath) changeUriState() })) - + this._register(this._onDidChangeDiffZoneStreaming.event(({ uri: uri_ }) => { if (uri_.fsPath === model.uri.fsPath) updateAcceptRejectAllUI() })) + this._register(this._onDidAddOrDeleteDiffZone.event(({ uri: uri_ }) => { if (uri_.fsPath === model.uri.fsPath) updateAcceptRejectAllUI() })) } // initialize all existing models + initialize when a new model mounts diff --git a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx index 44899741..fe70caa3 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx @@ -49,7 +49,7 @@ export const QuickEditChat = ({ const isDisabled = instructionsAreEmpty || !!isFeatureNameDisabled('Ctrl+K', settingsState) - const [isStreamingRef, setIsStreamingRef] = useRefState(false) + const [isStreamingRef, setIsStreamingRef] = useRefState(editCodeService.isCtrlKZoneStreaming({ diffareaid })) useCtrlKZoneStreamingState(useCallback((diffareaid2, isStreaming) => { if (diffareaid !== diffareaid2) return setIsStreamingRef(isStreaming) diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx index bf44e045..9ea92f0c 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx @@ -348,7 +348,7 @@ export const useRefreshModelListener = (listener: (providerName: RefreshableProv useEffect(() => { refreshModelProviderListeners.add(listener) return () => { refreshModelProviderListeners.delete(listener) } - }, [listener]) + }, [listener, refreshModelProviderListeners]) } @@ -356,7 +356,7 @@ export const useCtrlKZoneStreamingState = (listener: (diffareaid: number, s: boo useEffect(() => { ctrlKZoneStreamingStateListeners.add(listener) return () => { ctrlKZoneStreamingStateListeners.delete(listener) } - }, [listener]) + }, [listener, ctrlKZoneStreamingStateListeners]) } From 1079893527ae9b8f7fc31cc0ed00ceea3f7c1928 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 21 Feb 2025 23:31:06 -0800 Subject: [PATCH 19/41] add codeBoxId, but now will change to uri --- .../contrib/void/browser/editCodeService.ts | 96 +++++++++++----- .../src/markdown/ApplyBlockHoverButtons.tsx | 106 +++++++++++------- .../src/quick-edit-tsx/QuickEditChat.tsx | 1 + .../void/browser/react/src/util/services.tsx | 21 ++-- 4 files changed, 149 insertions(+), 75 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index b14c085a..9c37821f 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -128,13 +128,16 @@ export type StartApplyingOpts = { from: 'QuickEdit'; type: 'rewrite'; diffareaid: number; // id of the CtrlK area (contains text selection) + chatCodeBoxId: string | null; } | { from: 'ClickApply'; type: 'searchReplace' | 'rewrite'; applyStr: string; + chatCodeBoxId: string | null; } + export type AddCtrlKOpts = { startLine: number, endLine: number, @@ -197,12 +200,11 @@ type DiffZone = { isStreaming: true; streamRequestIdRef: { current: string | null }; line: number; - applyBoxId?: string; + codeBoxId: string | null; } | { isStreaming: false; streamRequestIdRef?: undefined; line?: undefined; - applyBoxId?: undefined; }; editorId?: undefined; linkedStreamingDiffZone?: undefined; @@ -260,10 +262,10 @@ export interface IEditCodeService { interruptCtrlKStreaming(opts: { diffareaid: number }): void; onDidChangeCtrlKZoneStreaming: Event<{ uri: URI; diffareaid: number }>; - // // DiffZone streaming state - // isApplyBoxIdStreaming(opts: { applyBoxId: string }): boolean; - // interruptApplyBoxId(opts: { applyBoxId: string }): void; - // onDidChangeApplyBoxIdStreaming: Event<{ applyBoxId: string }>; + // // DiffZone codeBoxId streaming state + isCodeBoxIdStreaming(opts: { codeBoxId: string }): boolean; + interruptCodeBoxId(opts: { codeBoxId: string }): void; + onDidChangeCodeBoxIdStreaming: Event<{ codeBoxId: string }>; // testDiffs(): void; } @@ -284,12 +286,14 @@ class EditCodeService extends Disposable implements IEditCodeService { // only applies to diffZones // streamingDiffZones: Set = new Set() private readonly _onDidChangeDiffZoneStreaming = new Emitter<{ uri: URI; diffareaid: number }>(); - private readonly _onDidChangeCtrlKZoneStreaming = new Emitter<{ uri: URI; diffareaid: number }>(); - private readonly _onDidAddOrDeleteDiffZone = new Emitter<{ uri: URI }>(); + private readonly _onDidAddOrDeleteDiffZones = new Emitter<{ uri: URI }>(); - onDidChangeDiffZoneStreaming = this._onDidChangeDiffZoneStreaming.event + private readonly _onDidChangeCtrlKZoneStreaming = new Emitter<{ uri: URI; diffareaid: number }>(); onDidChangeCtrlKZoneStreaming = this._onDidChangeCtrlKZoneStreaming.event + private readonly _onDidChangeCodeBoxIdStreaming = new Emitter<{ uri: URI; diffareaid: number; codeBoxId: string }>(); + onDidChangeCodeBoxIdStreaming = this._onDidChangeCodeBoxIdStreaming.event + constructor( // @IHistoryService private readonly _historyService: IHistoryService, // history service is the history of pressing alt left/right @ICodeEditorService private readonly _editorService: ICodeEditorService, @@ -342,7 +346,17 @@ class EditCodeService extends Disposable implements IEditCodeService { } this._register(this._onDidChangeDiffZoneStreaming.event(({ uri: uri_ }) => { if (uri_.fsPath === model.uri.fsPath) updateAcceptRejectAllUI() })) - this._register(this._onDidAddOrDeleteDiffZone.event(({ uri: uri_ }) => { if (uri_.fsPath === model.uri.fsPath) updateAcceptRejectAllUI() })) + this._register(this._onDidAddOrDeleteDiffZones.event(({ uri: uri_ }) => { if (uri_.fsPath === model.uri.fsPath) updateAcceptRejectAllUI() })) + + // codeBoxId + this._register(this._onDidChangeDiffZoneStreaming.event(({ diffareaid }) => { + const diffZone = this.diffAreaOfId[diffareaid] + if (diffZone?.type !== 'DiffZone') return + if (!diffZone._streamState.isStreaming) return + const { codeBoxId } = diffZone._streamState + if (codeBoxId === null) return + this._onDidChangeCodeBoxIdStreaming.fire({ uri: model.uri, codeBoxId, diffareaid }) + })) } // initialize all existing models + initialize when a new model mounts @@ -864,7 +878,7 @@ class EditCodeService extends Disposable implements IEditCodeService { } this.diffAreasOfURI[uri.fsPath].add(diffareaid) } - this._onDidAddOrDeleteDiffZone.fire({ uri }) + this._onDidAddOrDeleteDiffZones.fire({ uri }) // restore file content const numLines = this._getNumLines(uri) @@ -934,7 +948,7 @@ class EditCodeService extends Disposable implements IEditCodeService { this._clearAllDiffAreaEffects(diffZone) delete this.diffAreaOfId[diffZone.diffareaid] this.diffAreasOfURI[diffZone._URI.fsPath].delete(diffZone.diffareaid.toString()) - this._onDidAddOrDeleteDiffZone.fire({ uri: diffZone._URI }) + this._onDidAddOrDeleteDiffZones.fire({ uri: diffZone._URI }) } private _deleteTrackingZone(trackingZone: TrackingZone) { @@ -1252,7 +1266,7 @@ class EditCodeService extends Disposable implements IEditCodeService { private _initializeWriteoverStream(opts: StartApplyingOpts): DiffZone | undefined { - const { from } = opts + const { from, chatCodeBoxId } = opts let startLine: number let endLine: number @@ -1312,13 +1326,14 @@ class EditCodeService extends Disposable implements IEditCodeService { isStreaming: true, streamRequestIdRef, line: startLine, + codeBoxId: chatCodeBoxId, }, _diffOfId: {}, // added later _removeStylesFns: new Set(), } const diffZone = this._addDiffArea(adding) this._onDidChangeDiffZoneStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) - this._onDidAddOrDeleteDiffZone.fire({ uri }) + this._onDidAddOrDeleteDiffZones.fire({ uri }) if (from === 'QuickEdit') { const { diffareaid } = opts @@ -1436,7 +1451,8 @@ class EditCodeService extends Disposable implements IEditCodeService { - private _initializeSearchAndReplaceStream({ applyStr }: { applyStr: string }) { + private _initializeSearchAndReplaceStream(opts: StartApplyingOpts & { from: 'ClickApply' }) { + const { applyStr, chatCodeBoxId } = opts const uri_ = this._getActiveEditorURI() if (!uri_) return @@ -1485,13 +1501,14 @@ class EditCodeService extends Disposable implements IEditCodeService { isStreaming: true, streamRequestIdRef, line: startLine, + codeBoxId: chatCodeBoxId, }, _diffOfId: {}, // added later _removeStylesFns: new Set(), } const diffZone = this._addDiffArea(adding) this._onDidChangeDiffZoneStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) - this._onDidAddOrDeleteDiffZone.fire({ uri }) + this._onDidAddOrDeleteDiffZones.fire({ uri }) const revertAndContinueHistory = () => { @@ -1729,12 +1746,16 @@ class EditCodeService extends Disposable implements IEditCodeService { - isDiffZoneStreaming({ diffareaid }: { diffareaid: number }) { + _interruptDiffZoneStreaming({ diffareaid }: { diffareaid: number }) { const diffZone = this.diffAreaOfId[diffareaid] - if (diffZone?.type !== 'DiffZone') return false - return diffZone._streamState.isStreaming + if (diffZone?.type !== 'DiffZone') return + if (!diffZone._streamState.isStreaming) return + + this._stopIfStreaming(diffZone) + this._undoHistory(diffZone._URI) } + isCtrlKZoneStreaming({ diffareaid }: { diffareaid: number }) { const ctrlKZone = this.diffAreaOfId[diffareaid] if (!ctrlKZone) return false @@ -1743,15 +1764,6 @@ class EditCodeService extends Disposable implements IEditCodeService { } - interruptDiffZoneStreaming({ diffareaid }: { diffareaid: number }) { - const diffZone = this.diffAreaOfId[diffareaid] - if (diffZone?.type !== 'DiffZone') return - if (!diffZone._streamState.isStreaming) return - - this._stopIfStreaming(diffZone) - this._undoHistory(diffZone._URI) - } - // diffareaid of the ctrlKZone (even though the stream state is dictated by the linked diffZone) interruptCtrlKStreaming({ diffareaid }: { diffareaid: number }) { const ctrlKZone = this.diffAreaOfId[diffareaid] @@ -1762,11 +1774,37 @@ class EditCodeService extends Disposable implements IEditCodeService { if (!linkedStreamingDiffZone) return if (linkedStreamingDiffZone.type !== 'DiffZone') return - this.interruptDiffZoneStreaming({ diffareaid: linkedStreamingDiffZone.diffareaid }) + this._interruptDiffZoneStreaming({ diffareaid: linkedStreamingDiffZone.diffareaid }) } + isCodeBoxIdStreaming({ codeBoxId }: { codeBoxId: string }) { + // brute force is OK for now + for (const diffareaid in this.diffAreaOfId) { + const diffArea = this.diffAreaOfId[diffareaid] + if (!diffArea) continue + if (diffArea.type !== 'DiffZone') continue + if (!diffArea._streamState.isStreaming) continue + if (diffArea._streamState.codeBoxId === codeBoxId) return true + } + return false + } + + interruptCodeBoxId({ codeBoxId }: { codeBoxId: string }) { + // brute force for now is OK + for (const diffareaid in this.diffAreaOfId) { + const diffArea = this.diffAreaOfId[diffareaid] + if (!diffArea) continue + if (diffArea.type !== 'DiffZone') continue + if (!diffArea._streamState.isStreaming) continue + if (diffArea._streamState.codeBoxId === codeBoxId) { + this._interruptDiffZoneStreaming({ diffareaid: diffArea.diffareaid }) + return + } + } + } + // public removeDiffZone(diffZone: DiffZone, behavior: 'reject' | 'accept') { // const uri = diffZone._URI diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx index 6224110b..ecd5d7d9 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx @@ -1,5 +1,5 @@ import { useState, useEffect, useCallback } from 'react' -import { useAccessor, useIsDiffZoneStreaming } from '../util/services.js' +import { useAccessor, useCodeBoxIdStreamingState, useSettingsState } from '../util/services.js' import { useRefState } from '../util/helpers.js' import { isFeatureNameDisabled } from '../../../../common/voidSettingsTypes.js' @@ -44,54 +44,35 @@ const CopyButton = ({ codeStr }: { codeStr: string }) => { } +const useStreamStateRef = ({ codeBoxId }: { codeBoxId: string | null }) => { + const accessor = useAccessor() + const editCodeService = accessor.get('IEditCodeService') + const [isStreamingRef, setIsStreamingRef] = useRefState(editCodeService.isCodeBoxIdStreaming({ codeBoxId })) + useCodeBoxIdStreamingState(useCallback((codeBoxId2, isStreaming) => { + if (codeBoxId !== codeBoxId2) return + setIsStreamingRef(isStreaming) + }, [codeBoxId, setIsStreamingRef])) + return [isStreamingRef, setIsStreamingRef] as const +} -const ApplyButton = ({ codeStr, codeBoxId }: { codeStr: string, codeBoxId: string }) => { + + +const StopButton = ({ codeBoxId }: { codeBoxId: string }) => { const accessor = useAccessor() const editCodeService = accessor.get('IEditCodeService') const metricsService = accessor.get('IMetricsService') + const settingsState = useSettingsState() - // const isStreaming = useIsDiffZoneStreaming(isDiffAreaStreaming) + const [isStreamingRef, _] = useStreamStateRef({ codeBoxId }) - // const onSubmit = useCallback(() => { - - // const uri = editCodeService.startApplying({ - // from: 'ClickApply', - // type: 'searchReplace', - // applyStr: codeStr, - // }) - - // metricsService.capture('Apply Code', { length: codeStr.length }) // capture the length only - - - - // if (isStreaming) return - - // setCurrentlyStreamingDiffZone(id ?? null) - // }, [isStreaming, editCodeService]) - - // const onInterrupt = useCallback(() => { - // if (currStreamingDiffZoneRef.current === null) return - // editCodeService.interruptStreaming(currStreamingDiffZoneRef.current) - // setCurrentlyStreamingDiffZone(null) - // textAreaFnsRef.current?.enable() - // }, [isStreaming, editCodeService]) - - - - - - - - const isSingleLine = !codeStr.includes('\n') - return @@ -103,8 +84,57 @@ const ApplyButton = ({ codeStr, codeBoxId }: { codeStr: string, codeBoxId: strin export const ApplyBlockHoverButtons = ({ codeStr, codeBoxId }: { codeStr: string, codeBoxId: string | null }) => { + + + + const accessor = useAccessor() + + const editCodeService = accessor.get('IEditCodeService') + const metricsService = accessor.get('IMetricsService') + + const settingsState = useSettingsState() + + const isDisabled = !!isFeatureNameDisabled('Apply', settingsState) + + const [isStreamingRef, _] = useStreamStateRef({ codeBoxId }) + + const onSubmit = useCallback(() => { + if (isDisabled) return + if (isStreamingRef.current) return + editCodeService.startApplying({ + from: 'ClickApply', + type: 'searchReplace', + applyStr: codeStr, + chatCodeBoxId: codeBoxId, + }) + metricsService.capture('Apply Code', { length: codeStr.length }) // capture the length only + }, [isStreamingRef, editCodeService, codeBoxId, codeStr, metricsService]) + + + const onInterrupt = useCallback(() => { + if (isStreamingRef.current) return + if (codeBoxId === null) return + editCodeService.interruptCodeBoxId({ codeBoxId, }) + metricsService.capture('Stop Apply', {}) + }, [isStreamingRef, editCodeService, codeBoxId, metricsService]) + + + + const isSingleLine = !codeStr.includes('\n') + + const applyButton = + + + return <> - - {codeBoxId !== null && } + {!isStreamingRef.current && } + {!isStreamingRef.current && codeBoxId !== null && } + {!isStreamingRef.current && } } diff --git a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx index fe70caa3..9e8f674f 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx @@ -65,6 +65,7 @@ export const QuickEditChat = ({ from: 'QuickEdit', type: 'rewrite', diffareaid, + chatCodeBoxId: null, }) }, [isStreamingRef, isDisabled, editCodeService, diffareaid]) diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx index 9ea92f0c..6e1f5102 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx @@ -76,9 +76,7 @@ let colorThemeState: ColorScheme const colorThemeStateListeners: Set<(s: ColorScheme) => void> = new Set() const ctrlKZoneStreamingStateListeners: Set<(diffareaid: number, s: boolean) => void> = new Set() - -let diffZoneStreamingState: Record -const diffZoneStreamingStateListeners: Set<(diffareaid: number, state: boolean) => void> = new Set() +const codeBoxIdStreamingStateListeners: Set<(codeBoxId: string, s: boolean) => void> = new Set() @@ -184,6 +182,12 @@ export const _registerServices = (accessor: ServicesAccessor) => { ctrlKZoneStreamingStateListeners.forEach(l => l(diffareaid, isStreaming)) }) ) + disposables.push( + editCodeService.onDidChangeCodeBoxIdStreaming(({ codeBoxId }) => { + const isStreaming = editCodeService.isCodeBoxIdStreaming({ codeBoxId }) + codeBoxIdStreamingStateListeners.forEach(l => l(codeBoxId, isStreaming)) + }) + ) @@ -351,7 +355,6 @@ export const useRefreshModelListener = (listener: (providerName: RefreshableProv }, [listener, refreshModelProviderListeners]) } - export const useCtrlKZoneStreamingState = (listener: (diffareaid: number, s: boolean) => void) => { useEffect(() => { ctrlKZoneStreamingStateListeners.add(listener) @@ -359,16 +362,18 @@ export const useCtrlKZoneStreamingState = (listener: (diffareaid: number, s: boo }, [listener, ctrlKZoneStreamingStateListeners]) } - -export const useIsDiffZoneStreaming = (diffareaid: number) => { - return { current: true } - +export const useCodeBoxIdStreamingState = (listener: (codeBoxId: string, s: boolean) => void) => { + useEffect(() => { + codeBoxIdStreamingStateListeners.add(listener) + return () => { codeBoxIdStreamingStateListeners.delete(listener) } + }, [listener, codeBoxIdStreamingStateListeners]) } + export const useIsDark = () => { const [s, ss] = useState(colorThemeState) useEffect(() => { From 63b71dec24d18e6f3169b05b268bebb9463d3fd3 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sat, 22 Feb 2025 00:40:46 -0800 Subject: [PATCH 20/41] should work, just need to debug --- .../contrib/void/browser/editCodeService.ts | 110 ++++++++------- .../src/markdown/ApplyBlockHoverButtons.tsx | 127 +++++++++++------- .../react/src/markdown/ChatMarkdownRender.tsx | 6 +- .../src/quick-edit-tsx/QuickEditChat.tsx | 2 +- .../void/browser/react/src/util/services.tsx | 25 ++-- 5 files changed, 144 insertions(+), 126 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 9c37821f..bdecb940 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -121,19 +121,19 @@ const findTextInCode = (text: string, fileContents: string, startingAtLine?: num } -type AcceptRejectAllState = 'idle' | 'acceptRejectAll' | 'streaming' +export type URIStreamState = 'idle' | 'acceptRejectAll' | 'streaming' export type StartApplyingOpts = { from: 'QuickEdit'; type: 'rewrite'; diffareaid: number; // id of the CtrlK area (contains text selection) - chatCodeBoxId: string | null; + chatApplyBoxId: string | null; } | { from: 'ClickApply'; type: 'searchReplace' | 'rewrite'; applyStr: string; - chatCodeBoxId: string | null; + chatApplyBoxId: string | null; } @@ -177,6 +177,7 @@ type CommonZoneProps = { type CtrlKZone = { type: 'CtrlKZone'; originalCode?: undefined; + chatApplyBoxId?: undefined; editorId: string; // the editor the input lives on @@ -196,11 +197,11 @@ type DiffZone = { type: 'DiffZone', originalCode: string; _diffOfId: Record; // diffid -> diff in this DiffArea + chatApplyBoxId: string | null; _streamState: { isStreaming: true; streamRequestIdRef: { current: string | null }; line: number; - codeBoxId: string | null; } | { isStreaming: false; streamRequestIdRef?: undefined; @@ -219,6 +220,7 @@ type TrackingZone = { originalCode?: undefined; editorId?: undefined; _removeStylesFns?: undefined; + chatApplyBoxId?: undefined; } & CommonZoneProps @@ -232,6 +234,7 @@ const diffAreaSnapshotKeys = [ 'startLine', 'endLine', 'editorId', + 'chatApplyBoxId', ] as const satisfies (keyof DiffArea)[] @@ -256,6 +259,7 @@ export interface IEditCodeService { 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; @@ -263,9 +267,9 @@ export interface IEditCodeService { onDidChangeCtrlKZoneStreaming: Event<{ uri: URI; diffareaid: number }>; // // DiffZone codeBoxId streaming state - isCodeBoxIdStreaming(opts: { codeBoxId: string }): boolean; - interruptCodeBoxId(opts: { codeBoxId: string }): void; - onDidChangeCodeBoxIdStreaming: Event<{ codeBoxId: string }>; + getURIStreamState(opts: { uri: URI | null }): URIStreamState; + interruptURIStreaming(opts: { uri: URI }): void; + onDidChangeURIStreamState: Event<{ uri: URI; state: URIStreamState }>; // testDiffs(): void; } @@ -291,8 +295,11 @@ class EditCodeService extends Disposable implements IEditCodeService { private readonly _onDidChangeCtrlKZoneStreaming = new Emitter<{ uri: URI; diffareaid: number }>(); onDidChangeCtrlKZoneStreaming = this._onDidChangeCtrlKZoneStreaming.event - private readonly _onDidChangeCodeBoxIdStreaming = new Emitter<{ uri: URI; diffareaid: number; codeBoxId: string }>(); - onDidChangeCodeBoxIdStreaming = this._onDidChangeCodeBoxIdStreaming.event + private readonly _onDidChangeURIStreamState = new Emitter<{ uri: URI; state: URIStreamState }>(); + onDidChangeURIStreamState = this._onDidChangeURIStreamState.event + + + constructor( // @IHistoryService private readonly _historyService: IHistoryService, // history service is the history of pressing alt left/right @@ -327,36 +334,30 @@ class EditCodeService extends Disposable implements IEditCodeService { }) ) - // when a stream starts or ends, add/remove the accept|reject UI - let _removeAcceptRejectAllUI: (() => void) | null = null + // when a stream starts or ends, fire the event for onDidChangeURIStreamState + let prevStreamState = this.getURIStreamState({ uri: model.uri }) const updateAcceptRejectAllUI = () => { - const uri = model.uri - const diffZones = [...this.diffAreasOfURI[uri.fsPath].values()] - .map(diffareaid => this.diffAreaOfId[diffareaid]) - .filter(diffArea => !!diffArea && diffArea.type === 'DiffZone') - const isStreaming = diffZones.find(diffZone => !!diffZone._streamState.isStreaming) + const state = this.getURIStreamState({ uri: model.uri }) + if (prevStreamState === state) return + this._onDidChangeURIStreamState.fire({ uri: model.uri, state }) + } - const state: AcceptRejectAllState = isStreaming ? 'streaming' : (diffZones.length === 0 ? 'idle' : 'acceptRejectAll') + // add/remove the accept|reject UI + let _removeAcceptRejectAllUI: (() => void) | null = null + this._register(this._onDidChangeURIStreamState.event(({ uri: uri_ }) => { + if (uri_.fsPath !== model.uri.fsPath) return + const state = this.getURIStreamState({ uri: model.uri }) if (state === 'acceptRejectAll' && !_removeAcceptRejectAllUI) { - _removeAcceptRejectAllUI = this._addAcceptRejectAllUI(uri) ?? null + _removeAcceptRejectAllUI = this._addAcceptRejectAllUI(model.uri) ?? null } else { _removeAcceptRejectAllUI?.() _removeAcceptRejectAllUI = null } - } + })) this._register(this._onDidChangeDiffZoneStreaming.event(({ uri: uri_ }) => { if (uri_.fsPath === model.uri.fsPath) updateAcceptRejectAllUI() })) this._register(this._onDidAddOrDeleteDiffZones.event(({ uri: uri_ }) => { if (uri_.fsPath === model.uri.fsPath) updateAcceptRejectAllUI() })) - // codeBoxId - this._register(this._onDidChangeDiffZoneStreaming.event(({ diffareaid }) => { - const diffZone = this.diffAreaOfId[diffareaid] - if (diffZone?.type !== 'DiffZone') return - if (!diffZone._streamState.isStreaming) return - const { codeBoxId } = diffZone._streamState - if (codeBoxId === null) return - this._onDidChangeCodeBoxIdStreaming.fire({ uri: model.uri, codeBoxId, diffareaid }) - })) } // initialize all existing models + initialize when a new model mounts @@ -505,7 +506,7 @@ class EditCodeService extends Disposable implements IEditCodeService { const buttonsWidget = new AcceptAllRejectAllWidget({ editor, onAcceptAll: () => { - this.removeDiffAreas({ uri, behavior: 'keep', removeCtrlKs: false }) + this.removeDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false }) this._metricsService.capture('Accept All', {}) }, onRejectAll: () => { @@ -1266,7 +1267,7 @@ class EditCodeService extends Disposable implements IEditCodeService { private _initializeWriteoverStream(opts: StartApplyingOpts): DiffZone | undefined { - const { from, chatCodeBoxId } = opts + const { from, chatApplyBoxId } = opts let startLine: number let endLine: number @@ -1318,6 +1319,7 @@ class EditCodeService extends Disposable implements IEditCodeService { const adding: Omit = { type: 'DiffZone', + chatApplyBoxId, originalCode, startLine, endLine, @@ -1326,7 +1328,6 @@ class EditCodeService extends Disposable implements IEditCodeService { isStreaming: true, streamRequestIdRef, line: startLine, - codeBoxId: chatCodeBoxId, }, _diffOfId: {}, // added later _removeStylesFns: new Set(), @@ -1452,7 +1453,7 @@ class EditCodeService extends Disposable implements IEditCodeService { private _initializeSearchAndReplaceStream(opts: StartApplyingOpts & { from: 'ClickApply' }) { - const { applyStr, chatCodeBoxId } = opts + const { applyStr, chatApplyBoxId } = opts const uri_ = this._getActiveEditorURI() if (!uri_) return @@ -1493,6 +1494,7 @@ class EditCodeService extends Disposable implements IEditCodeService { const adding: Omit = { type: 'DiffZone', + chatApplyBoxId, originalCode: originalFileCode, startLine, endLine, @@ -1501,7 +1503,6 @@ class EditCodeService extends Disposable implements IEditCodeService { isStreaming: true, streamRequestIdRef, line: startLine, - codeBoxId: chatCodeBoxId, }, _diffOfId: {}, // added later _removeStylesFns: new Set(), @@ -1555,7 +1556,6 @@ class EditCodeService extends Disposable implements IEditCodeService { this._deleteTrackingZone(trackingZone) onFinishEdit() - shouldSendAnotherMessage = false } // refresh now in case onText takes a while to get 1st message @@ -1746,7 +1746,7 @@ class EditCodeService extends Disposable implements IEditCodeService { - _interruptDiffZoneStreaming({ diffareaid }: { diffareaid: number }) { + _interruptSingleDiffZoneStreaming({ diffareaid }: { diffareaid: number }) { const diffZone = this.diffAreaOfId[diffareaid] if (diffZone?.type !== 'DiffZone') return if (!diffZone._streamState.isStreaming) return @@ -1774,35 +1774,33 @@ class EditCodeService extends Disposable implements IEditCodeService { if (!linkedStreamingDiffZone) return if (linkedStreamingDiffZone.type !== 'DiffZone') return - this._interruptDiffZoneStreaming({ diffareaid: linkedStreamingDiffZone.diffareaid }) + this._interruptSingleDiffZoneStreaming({ diffareaid: linkedStreamingDiffZone.diffareaid }) } - isCodeBoxIdStreaming({ codeBoxId }: { codeBoxId: string }) { - // brute force is OK for now - for (const diffareaid in this.diffAreaOfId) { - const diffArea = this.diffAreaOfId[diffareaid] - if (!diffArea) continue - if (diffArea.type !== 'DiffZone') continue - if (!diffArea._streamState.isStreaming) continue - if (diffArea._streamState.codeBoxId === codeBoxId) return true - } - return false + + getURIStreamState = ({ uri }: { uri: URI | null }) => { + if (uri === null) return 'idle' + + const diffZones = [...this.diffAreasOfURI[uri.fsPath].values()] + .map(diffareaid => this.diffAreaOfId[diffareaid]) + .filter(diffArea => !!diffArea && diffArea.type === 'DiffZone') + const isStreaming = diffZones.find(diffZone => !!diffZone._streamState.isStreaming) + + const state: URIStreamState = isStreaming ? 'streaming' : (diffZones.length === 0 ? 'idle' : 'acceptRejectAll') + return state } - interruptCodeBoxId({ codeBoxId }: { codeBoxId: string }) { + interruptURIStreaming({ uri }: { uri: URI }) { // brute force for now is OK - for (const diffareaid in this.diffAreaOfId) { + for (const diffareaid of this.diffAreasOfURI[uri.fsPath] || []) { const diffArea = this.diffAreaOfId[diffareaid] - if (!diffArea) continue - if (diffArea.type !== 'DiffZone') continue + if (diffArea?.type !== 'DiffZone') continue if (!diffArea._streamState.isStreaming) continue - if (diffArea._streamState.codeBoxId === codeBoxId) { - this._interruptDiffZoneStreaming({ diffareaid: diffArea.diffareaid }) - return - } + this._stopIfStreaming(diffArea) } + this._undoHistory(uri) } @@ -1829,7 +1827,7 @@ class EditCodeService extends Disposable implements IEditCodeService { // remove a batch of diffareas all at once (and handle accept/reject of their diffs) - public removeDiffAreas({ uri, removeCtrlKs, behavior }: { uri: URI, removeCtrlKs: boolean, behavior: 'reject' | 'keep' }) { + public removeDiffAreas({ uri, removeCtrlKs, behavior }: { uri: URI, removeCtrlKs: boolean, behavior: 'reject' | 'accept' }) { const diffareaids = this.diffAreasOfURI[uri.fsPath] if (diffareaids.size === 0) return // do nothing @@ -1842,7 +1840,7 @@ class EditCodeService extends Disposable implements IEditCodeService { if (diffArea.type == 'DiffZone') { if (behavior === 'reject') this._revertAndDeleteDiffZone(diffArea) - else if (behavior === 'keep') this._deleteDiffZone(diffArea) + else if (behavior === 'accept') this._deleteDiffZone(diffArea) } else if (diffArea.type === 'CtrlKZone' && removeCtrlKs) { this._deleteCtrlKZone(diffArea) diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx index ecd5d7d9..50f14775 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx @@ -1,7 +1,8 @@ import { useState, useEffect, useCallback } from 'react' -import { useAccessor, useCodeBoxIdStreamingState, useSettingsState } from '../util/services.js' +import { useAccessor, useURIStreamState, useSettingsState } from '../util/services.js' import { useRefState } from '../util/helpers.js' import { isFeatureNameDisabled } from '../../../../common/voidSettingsTypes.js' +import { URI } from '../../../../../../../base/common/uri.js' enum CopyButtonText { Idle = 'Copy', @@ -44,80 +45,71 @@ const CopyButton = ({ codeStr }: { codeStr: string }) => { } -const useStreamStateRef = ({ codeBoxId }: { codeBoxId: string | null }) => { - const accessor = useAccessor() - const editCodeService = accessor.get('IEditCodeService') - const [isStreamingRef, setIsStreamingRef] = useRefState(editCodeService.isCodeBoxIdStreaming({ codeBoxId })) - useCodeBoxIdStreamingState(useCallback((codeBoxId2, isStreaming) => { - if (codeBoxId !== codeBoxId2) return - setIsStreamingRef(isStreaming) - }, [codeBoxId, setIsStreamingRef])) - return [isStreamingRef, setIsStreamingRef] as const + + + +// state persisted for duration of react only +const streamingURIOfApplyBoxIdRef: { current: { [applyBoxId: string]: URI | undefined } } = { current: {} } +const useStreamingURIOfApplyBoxId = (applyBoxId: string | null) => { + const [_, ss] = useState(0) + const uri = applyBoxId === null ? null : streamingURIOfApplyBoxIdRef.current[applyBoxId] + const setUri = useCallback((uri: URI | null) => { + if (applyBoxId === null) return + ss(c => c + 1) + if (uri === null) { + delete streamingURIOfApplyBoxIdRef.current[applyBoxId] + } + else { + streamingURIOfApplyBoxIdRef.current = { + ...streamingURIOfApplyBoxIdRef.current, + [applyBoxId]: uri, + } + } + }, [applyBoxId]) + return [uri, setUri] as const } +export const ApplyBlockHoverButtons = ({ codeStr, applyBoxId }: { codeStr: string, applyBoxId: string | null }) => { -const StopButton = ({ codeBoxId }: { codeBoxId: string }) => { - const accessor = useAccessor() - - const editCodeService = accessor.get('IEditCodeService') - const metricsService = accessor.get('IMetricsService') const settingsState = useSettingsState() - const [isStreamingRef, _] = useStreamStateRef({ codeBoxId }) - - - - return - -} - - - - - -export const ApplyBlockHoverButtons = ({ codeStr, codeBoxId }: { codeStr: string, codeBoxId: string | null }) => { - - + const isDisabled = !!isFeatureNameDisabled('Apply', settingsState) || applyBoxId === null const accessor = useAccessor() - const editCodeService = accessor.get('IEditCodeService') const metricsService = accessor.get('IMetricsService') - const settingsState = useSettingsState() + // get streaming URI of this applyBlockId (cached in react) + const [appliedURI, setAppliedURI] = useStreamingURIOfApplyBoxId(applyBoxId) - const isDisabled = !!isFeatureNameDisabled('Apply', settingsState) + // get stream state of this URI + const [streamStateRef, setStreamState] = useRefState(editCodeService.getURIStreamState({ uri: appliedURI ?? null })) + useURIStreamState(useCallback((uri, streamState) => { + if (appliedURI?.fsPath !== uri.fsPath) return + setStreamState(streamState) + }, [appliedURI, setStreamState])) - const [isStreamingRef, _] = useStreamStateRef({ codeBoxId }) const onSubmit = useCallback(() => { if (isDisabled) return - if (isStreamingRef.current) return - editCodeService.startApplying({ + const uri = editCodeService.startApplying({ from: 'ClickApply', type: 'searchReplace', applyStr: codeStr, - chatCodeBoxId: codeBoxId, + chatApplyBoxId: applyBoxId, }) + setAppliedURI(uri) metricsService.capture('Apply Code', { length: codeStr.length }) // capture the length only - }, [isStreamingRef, editCodeService, codeBoxId, codeStr, metricsService]) + }, [streamStateRef, setAppliedURI, editCodeService, applyBoxId, codeStr, metricsService]) const onInterrupt = useCallback(() => { - if (isStreamingRef.current) return - if (codeBoxId === null) return - editCodeService.interruptCodeBoxId({ codeBoxId, }) + if (!appliedURI) return + editCodeService.interruptURIStreaming({ uri: appliedURI, }) metricsService.capture('Stop Apply', {}) - }, [isStreamingRef, editCodeService, codeBoxId, metricsService]) - + }, [streamStateRef, editCodeService, appliedURI, metricsService]) const isSingleLine = !codeStr.includes('\n') @@ -130,11 +122,42 @@ export const ApplyBlockHoverButtons = ({ codeStr, codeBoxId }: { codeStr: string Apply + const stopButton = + + const acceptRejectButtons = <> + + + return <> - {!isStreamingRef.current && } - {!isStreamingRef.current && codeBoxId !== null && } - {!isStreamingRef.current && } + {streamStateRef.current !== 'streaming' && } + {streamStateRef.current === 'idle' && !isDisabled && applyButton} + {streamStateRef.current === 'streaming' && stopButton} + {streamStateRef.current === 'acceptRejectAll' && acceptRejectButtons} } diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index d6af4843..f6d08287 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -14,7 +14,7 @@ import { URI } from '../../../../../../../base/common/uri.js' type ApplyBoxLocation = ChatMessageLocation & { tokenIdx: string } -const getCodeBoxId = ({ threadId, messageIdx, tokenIdx }: ApplyBoxLocation) => { +const getApplyBoxId = ({ threadId, messageIdx, tokenIdx }: ApplyBoxLocation) => { return `${threadId}-${messageIdx}-${tokenIdx}` } @@ -46,7 +46,7 @@ const RenderToken = ({ token, nested, noSpace, chatMessageLocation, tokenIdx }: if (t.type === "code") { - const codeBoxId = chatMessageLocation ? getCodeBoxId({ + const applyBoxId = chatMessageLocation ? getApplyBoxId({ threadId: chatMessageLocation.threadId, messageIdx: chatMessageLocation.messageIdx, tokenIdx: tokenIdx, @@ -55,7 +55,7 @@ const RenderToken = ({ token, nested, noSpace, chatMessageLocation, tokenIdx }: return } + buttonsOnHover={} /> } diff --git a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx index 9e8f674f..1fbcc303 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx @@ -65,7 +65,7 @@ export const QuickEditChat = ({ from: 'QuickEdit', type: 'rewrite', diffareaid, - chatCodeBoxId: null, + chatApplyBoxId: null, }) }, [isStreamingRef, isDisabled, editCodeService, diffareaid]) diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx index 6e1f5102..5e164428 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx @@ -3,7 +3,7 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import React, { useState, useEffect } from 'react' +import React, { useState, useEffect, useCallback } from 'react' import { ThreadStreamState, ThreadsState } from '../../../chatThreadService.js' import { RefreshableProviderName, SettingsOfProvider } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js' import { IDisposable } from '../../../../../../../base/common/lifecycle.js' @@ -24,7 +24,7 @@ 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 } from '../../../editCodeService.js'; +import { IEditCodeService, URIStreamState } from '../../../editCodeService.js'; import { IVoidUriStateService } from '../../../voidUriStateService.js'; import { IQuickEditStateService } from '../../../quickEditStateService.js'; import { ISidebarStateService } from '../../../sidebarStateService.js'; @@ -43,6 +43,7 @@ import { IEnvironmentService } from '../../../../../../../platform/environment/c import { IConfigurationService } from '../../../../../../../platform/configuration/common/configuration.js' 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' @@ -76,7 +77,7 @@ let colorThemeState: ColorScheme const colorThemeStateListeners: Set<(s: ColorScheme) => void> = new Set() const ctrlKZoneStreamingStateListeners: Set<(diffareaid: number, s: boolean) => void> = new Set() -const codeBoxIdStreamingStateListeners: Set<(codeBoxId: string, s: boolean) => void> = new Set() +const uriStreamingStateListeners: Set<(uri: URI, s: URIStreamState) => void> = new Set() @@ -183,9 +184,9 @@ export const _registerServices = (accessor: ServicesAccessor) => { }) ) disposables.push( - editCodeService.onDidChangeCodeBoxIdStreaming(({ codeBoxId }) => { - const isStreaming = editCodeService.isCodeBoxIdStreaming({ codeBoxId }) - codeBoxIdStreamingStateListeners.forEach(l => l(codeBoxId, isStreaming)) + editCodeService.onDidChangeURIStreamState(({ uri }) => { + const isStreaming = editCodeService.getURIStreamState({ uri }) + uriStreamingStateListeners.forEach(l => l(uri, isStreaming)) }) ) @@ -362,18 +363,14 @@ export const useCtrlKZoneStreamingState = (listener: (diffareaid: number, s: boo }, [listener, ctrlKZoneStreamingStateListeners]) } -export const useCodeBoxIdStreamingState = (listener: (codeBoxId: string, s: boolean) => void) => { +export const useURIStreamState = (listener: (uri: URI, s: URIStreamState) => void) => { useEffect(() => { - codeBoxIdStreamingStateListeners.add(listener) - return () => { codeBoxIdStreamingStateListeners.delete(listener) } - }, [listener, codeBoxIdStreamingStateListeners]) + uriStreamingStateListeners.add(listener) + return () => { uriStreamingStateListeners.delete(listener) } + }, [listener, uriStreamingStateListeners]) } - - - - export const useIsDark = () => { const [s, ss] = useState(colorThemeState) useEffect(() => { From 19ebf4a1a2421f075734f9dac15e99e8168ab41c Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sat, 22 Feb 2025 03:03:05 -0800 Subject: [PATCH 21/41] seems to work! some annoying react state stuff.. --- .../contrib/void/browser/chatThreadService.ts | 3 +- .../contrib/void/browser/editCodeService.ts | 25 ++++--- .../browser/helpers/extractCodeFromResult.ts | 10 ++- .../src/markdown/ApplyBlockHoverButtons.tsx | 74 +++++++++---------- .../react/src/markdown/ChatMarkdownRender.tsx | 2 +- 5 files changed, 59 insertions(+), 55 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index c325fc8e..e6a52c54 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -17,6 +17,7 @@ import { InternalToolInfo, IToolsService, ToolCallReturnType, ToolFns, ToolName, import { toLLMChatMessage } from '../common/llmMessageTypes.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; import { IVoidFileService } from '../common/voidFileService.js'; +import { generateUuid } from '../../../../base/common/uuid.js'; const findLastIndex = (arr: T[], condition: (t: T) => boolean): number => { @@ -123,7 +124,7 @@ export type ThreadStreamState = { const newThreadObject = () => { const now = new Date().toISOString() return { - id: new Date().getTime().toString(), + id: generateUuid(), createdAt: now, lastModified: now, messages: [], diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index bdecb940..a76ada5e 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -338,23 +338,24 @@ class EditCodeService extends Disposable implements IEditCodeService { let prevStreamState = this.getURIStreamState({ uri: model.uri }) const updateAcceptRejectAllUI = () => { const state = this.getURIStreamState({ uri: model.uri }) - if (prevStreamState === state) return + let prevStateActual = prevStreamState + prevStreamState = state + if (state === prevStateActual) return this._onDidChangeURIStreamState.fire({ uri: model.uri, state }) } - // add/remove the accept|reject UI + let _removeAcceptRejectAllUI: (() => void) | null = null - this._register(this._onDidChangeURIStreamState.event(({ uri: uri_ }) => { - if (uri_.fsPath !== model.uri.fsPath) return - const state = this.getURIStreamState({ uri: model.uri }) - if (state === 'acceptRejectAll' && !_removeAcceptRejectAllUI) { - _removeAcceptRejectAllUI = this._addAcceptRejectAllUI(model.uri) ?? null + this._register(this._onDidChangeURIStreamState.event(({ uri, state }) => { + if (uri.fsPath !== model.uri.fsPath) return + if (state === 'acceptRejectAll') { + if (!_removeAcceptRejectAllUI) + _removeAcceptRejectAllUI = this._addAcceptRejectAllUI(model.uri) ?? null } else { _removeAcceptRejectAllUI?.() _removeAcceptRejectAllUI = null } })) - this._register(this._onDidChangeDiffZoneStreaming.event(({ uri: uri_ }) => { if (uri_.fsPath === model.uri.fsPath) updateAcceptRejectAllUI() })) this._register(this._onDidAddOrDeleteDiffZones.event(({ uri: uri_ }) => { if (uri_.fsPath === model.uri.fsPath) updateAcceptRejectAllUI() })) @@ -1666,8 +1667,10 @@ class EditCodeService extends Disposable implements IEditCodeService { this._writeStreamedDiffZoneLLMText(uri, block.orig, block.final, deltaFinalText, latestStreamLocationMutable) oldBlocks = blocks // oldblocks is only used if writingFinal - const { endLine: currentEndLine } = addedTrackingZoneOfBlockNum[blockNum] - diffZone._streamState.line = currentEndLine + // const { endLine: currentEndLine } = addedTrackingZoneOfBlockNum[blockNum] // would be bad to do this because a lot of the bottom lines might be the same. more accurate to go with latestStreamLocationMutable + // diffZone._streamState.line = currentEndLine + diffZone._streamState.line = latestStreamLocationMutable.line + } // end for @@ -1682,7 +1685,7 @@ class EditCodeService extends Disposable implements IEditCodeService { const blocks = extractSearchReplaceBlocks(fullText) if (blocks.length === 0) { - this._notificationService.info(`Void: When running Apply, your model didn't output any changes that Void recognized. You might need to use a smarter model for Apply.`) + this._notificationService.info(`Void: We ran Apply, but the LLM didn't output any changes.`) } // writeover the whole file diff --git a/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts b/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts index b7665eca..cd3276ff 100644 --- a/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts +++ b/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts @@ -201,7 +201,7 @@ export const extractSearchReplaceBlocks = (str: string) => { const ORIGINAL_ = ORIGINAL + `\n` const DIVIDER_ = '\n' + DIVIDER + `\n` - const FINAL_ = '\n' + FINAL + // logic for FINAL_ is slightly more complicated - should be '\n' + FINAL, but that ignores if the final output is empty const blocks: ExtractedSearchReplaceBlock[] = [] @@ -229,7 +229,13 @@ export const extractSearchReplaceBlocks = (str: string) => { i = dividerStart // wrote ===== - let finalStart = str.indexOf(FINAL_, i) + + + 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_) blocks.push({ diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx index 50f14775..94c11af0 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx @@ -32,7 +32,7 @@ const CopyButton = ({ codeStr }: { codeStr: string }) => { .then(() => { setCopyButtonText(CopyButtonText.Copied) }) .catch(() => { setCopyButtonText(CopyButtonText.Error) }) metricsService.capture('Copy Code', { length: codeStr.length }) // capture the length only - }, [metricsService, clipboardService, codeStr]) + }, [metricsService, clipboardService, codeStr, setCopyButtonText]) const isSingleLine = !codeStr.includes('\n') @@ -49,67 +49,60 @@ const CopyButton = ({ codeStr }: { codeStr: string }) => { // state persisted for duration of react only -const streamingURIOfApplyBoxIdRef: { current: { [applyBoxId: string]: URI | undefined } } = { current: {} } -const useStreamingURIOfApplyBoxId = (applyBoxId: string | null) => { - const [_, ss] = useState(0) - const uri = applyBoxId === null ? null : streamingURIOfApplyBoxIdRef.current[applyBoxId] - const setUri = useCallback((uri: URI | null) => { - if (applyBoxId === null) return - ss(c => c + 1) - if (uri === null) { - delete streamingURIOfApplyBoxIdRef.current[applyBoxId] - } - else { - streamingURIOfApplyBoxIdRef.current = { - ...streamingURIOfApplyBoxIdRef.current, - [applyBoxId]: uri, - } - } - }, [applyBoxId]) - return [uri, setUri] as const -} +const applyingURIOfApplyBoxIdRef: { current: { [applyBoxId: string]: URI | undefined } } = { current: {} } -export const ApplyBlockHoverButtons = ({ codeStr, applyBoxId }: { codeStr: string, applyBoxId: string | null }) => { +export const ApplyBlockHoverButtons = ({ codeStr, applyBoxId }: { codeStr: string, applyBoxId: string }) => { + console.log('applyboxid', applyBoxId, applyingURIOfApplyBoxIdRef) const settingsState = useSettingsState() - - const isDisabled = !!isFeatureNameDisabled('Apply', settingsState) || applyBoxId === null + const isDisabled = !!isFeatureNameDisabled('Apply', settingsState) || !applyBoxId const accessor = useAccessor() const editCodeService = accessor.get('IEditCodeService') const metricsService = accessor.get('IMetricsService') - // get streaming URI of this applyBlockId (cached in react) - const [appliedURI, setAppliedURI] = useStreamingURIOfApplyBoxId(applyBoxId) + const [applyingUriRef, setApplyingUri_] = useRefState(applyingURIOfApplyBoxIdRef.current[applyBoxId] ?? null) + const [streamStateRef, setStreamState_] = useRefState(editCodeService.getURIStreamState({ uri: applyingUriRef.current ?? null })) - // get stream state of this URI - const [streamStateRef, setStreamState] = useRefState(editCodeService.getURIStreamState({ uri: appliedURI ?? null })) - useURIStreamState(useCallback((uri, streamState) => { - if (appliedURI?.fsPath !== uri.fsPath) return - setStreamState(streamState) - }, [appliedURI, setStreamState])) + const setApplyingUri = useCallback((uri: URI | null) => { // switched the box's URI to whatever they clicked on most recently + setApplyingUri_(uri) + const newStreamState = editCodeService.getURIStreamState({ uri }) + if (uri) applyingURIOfApplyBoxIdRef.current[applyBoxId] = uri + setStreamState_(newStreamState) + }, [applyBoxId, setApplyingUri_, editCodeService, setStreamState_]) + // listen for stream updates + useURIStreamState( + useCallback((uri, streamState) => { + const shouldUpdate = applyingUriRef.current?.fsPath === uri.fsPath + if (!shouldUpdate) return + setStreamState_(streamState) // editCodeService.getURIStreamState({ uri: applyingUriRef.current ?? null }) + }, [applyingUriRef, setStreamState_]) + ) const onSubmit = useCallback(() => { if (isDisabled) return + if (streamStateRef.current === 'streaming') return const uri = editCodeService.startApplying({ from: 'ClickApply', type: 'searchReplace', applyStr: codeStr, chatApplyBoxId: applyBoxId, }) - setAppliedURI(uri) + setApplyingUri(uri) metricsService.capture('Apply Code', { length: codeStr.length }) // capture the length only - }, [streamStateRef, setAppliedURI, editCodeService, applyBoxId, codeStr, metricsService]) + }, [editCodeService, applyBoxId, codeStr, metricsService, isDisabled, streamStateRef, setApplyingUri]) const onInterrupt = useCallback(() => { - if (!appliedURI) return - editCodeService.interruptURIStreaming({ uri: appliedURI, }) + if (streamStateRef.current !== 'streaming') return + if (!applyingUriRef.current) return + + editCodeService.interruptURIStreaming({ uri: applyingUriRef.current, }) metricsService.capture('Stop Apply', {}) - }, [streamStateRef, editCodeService, appliedURI, metricsService]) + }, [editCodeService, metricsService, streamStateRef, applyingUriRef]) const isSingleLine = !codeStr.includes('\n') @@ -135,8 +128,8 @@ export const ApplyBlockHoverButtons = ({ codeStr, applyBoxId }: { codeStr: strin // 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`} onClick={() => { - if (!appliedURI) return - editCodeService.removeDiffAreas({ uri: appliedURI, behavior: 'accept', removeCtrlKs: false }) + if (!applyingUriRef.current) return + editCodeService.removeDiffAreas({ uri: applyingUriRef.current, behavior: 'accept', removeCtrlKs: false }) }} > Accept @@ -145,14 +138,15 @@ export const ApplyBlockHoverButtons = ({ codeStr, applyBoxId }: { codeStr: strin // 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`} onClick={() => { - if (!appliedURI) return - editCodeService.removeDiffAreas({ uri: appliedURI, behavior: 'reject', removeCtrlKs: false }) + if (!applyingUriRef.current) return + editCodeService.removeDiffAreas({ uri: applyingUriRef.current, behavior: 'reject', removeCtrlKs: false }) }} > Reject + console.log('streamStateRef.current', streamStateRef.current) return <> {streamStateRef.current !== 'streaming' && } diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index f6d08287..320dccbb 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -55,7 +55,7 @@ const RenderToken = ({ token, nested, noSpace, chatMessageLocation, tokenIdx }: return } + buttonsOnHover={applyBoxId && } /> } From 16f6181395c3b7814e8352973ec04dd346b19470 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sat, 22 Feb 2025 03:23:44 -0800 Subject: [PATCH 22/41] + --- .../void/browser/react/src/sidebar-tsx/SidebarChat.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx index 5f7c42ce..3c95f434 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx @@ -590,7 +590,7 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM _mustInitialize.current = false } - }, [role, mode, _justEnabledEdit, textAreaRefState, textAreaFnsRef.current, _justEnabledEdit.current, _mustInitialize.current]) + }, [chatMessage, role, mode, _justEnabledEdit, textAreaRefState, textAreaFnsRef.current, _justEnabledEdit.current, _mustInitialize.current]) const EditSymbol = mode === 'display' ? Pencil : X const onOpenEdit = () => { setIsBeingEdited(true) @@ -631,7 +631,7 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM // stream the edit const userMessage = textAreaRefState.value; - await chatThreadsService.editUserMessageAndStreamResponse({ userMessage, chatMode: 'agent', messageIdx }) + await chatThreadsService.editUserMessageAndStreamResponse({ userMessage, chatMode: 'agent', messageIdx, }) } const onAbort = () => { From ea10765abd684d7ebd100fe1e5ddd5253b1dcb0a Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sat, 22 Feb 2025 03:41:16 -0800 Subject: [PATCH 23/41] remove applybox stuff from editCode --- .../contrib/void/browser/editCodeService.ts | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index a76ada5e..0f92d4ef 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -128,12 +128,10 @@ export type StartApplyingOpts = { from: 'QuickEdit'; type: 'rewrite'; diffareaid: number; // id of the CtrlK area (contains text selection) - chatApplyBoxId: string | null; } | { from: 'ClickApply'; type: 'searchReplace' | 'rewrite'; applyStr: string; - chatApplyBoxId: string | null; } @@ -177,7 +175,6 @@ type CommonZoneProps = { type CtrlKZone = { type: 'CtrlKZone'; originalCode?: undefined; - chatApplyBoxId?: undefined; editorId: string; // the editor the input lives on @@ -197,7 +194,6 @@ type DiffZone = { type: 'DiffZone', originalCode: string; _diffOfId: Record; // diffid -> diff in this DiffArea - chatApplyBoxId: string | null; _streamState: { isStreaming: true; streamRequestIdRef: { current: string | null }; @@ -220,7 +216,6 @@ type TrackingZone = { originalCode?: undefined; editorId?: undefined; _removeStylesFns?: undefined; - chatApplyBoxId?: undefined; } & CommonZoneProps @@ -234,7 +229,6 @@ const diffAreaSnapshotKeys = [ 'startLine', 'endLine', 'editorId', - 'chatApplyBoxId', ] as const satisfies (keyof DiffArea)[] @@ -1268,7 +1262,7 @@ class EditCodeService extends Disposable implements IEditCodeService { private _initializeWriteoverStream(opts: StartApplyingOpts): DiffZone | undefined { - const { from, chatApplyBoxId } = opts + const { from } = opts let startLine: number let endLine: number @@ -1320,7 +1314,6 @@ class EditCodeService extends Disposable implements IEditCodeService { const adding: Omit = { type: 'DiffZone', - chatApplyBoxId, originalCode, startLine, endLine, @@ -1454,7 +1447,7 @@ class EditCodeService extends Disposable implements IEditCodeService { private _initializeSearchAndReplaceStream(opts: StartApplyingOpts & { from: 'ClickApply' }) { - const { applyStr, chatApplyBoxId } = opts + const { applyStr } = opts const uri_ = this._getActiveEditorURI() if (!uri_) return @@ -1495,7 +1488,6 @@ class EditCodeService extends Disposable implements IEditCodeService { const adding: Omit = { type: 'DiffZone', - chatApplyBoxId, originalCode: originalFileCode, startLine, endLine, From 2ecce822f384fed964cb2f0464e06cf240a8cd73 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sat, 22 Feb 2025 04:30:17 -0800 Subject: [PATCH 24/41] fix react - tedious --- .../src/markdown/ApplyBlockHoverButtons.tsx | 62 +++++++++---------- .../src/quick-edit-tsx/QuickEditChat.tsx | 1 - 2 files changed, 31 insertions(+), 32 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx index 94c11af0..8c9f26d1 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx @@ -3,6 +3,7 @@ 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', @@ -52,6 +53,7 @@ const CopyButton = ({ codeStr }: { codeStr: string }) => { const applyingURIOfApplyBoxIdRef: { current: { [applyBoxId: string]: URI | undefined } } = { current: {} } + export const ApplyBlockHoverButtons = ({ codeStr, applyBoxId }: { codeStr: string, applyBoxId: string }) => { console.log('applyboxid', applyBoxId, applyingURIOfApplyBoxIdRef) @@ -63,46 +65,43 @@ export const ApplyBlockHoverButtons = ({ codeStr, applyBoxId }: { codeStr: strin const editCodeService = accessor.get('IEditCodeService') const metricsService = accessor.get('IMetricsService') - const [applyingUriRef, setApplyingUri_] = useRefState(applyingURIOfApplyBoxIdRef.current[applyBoxId] ?? null) - const [streamStateRef, setStreamState_] = useRefState(editCodeService.getURIStreamState({ uri: applyingUriRef.current ?? null })) + const [_, rerender] = useState(0) - const setApplyingUri = useCallback((uri: URI | null) => { // switched the box's URI to whatever they clicked on most recently - setApplyingUri_(uri) - const newStreamState = editCodeService.getURIStreamState({ uri }) - if (uri) applyingURIOfApplyBoxIdRef.current[applyBoxId] = uri - setStreamState_(newStreamState) - }, [applyBoxId, setApplyingUri_, editCodeService, setStreamState_]) + const applyingUri = useCallback(() => applyingURIOfApplyBoxIdRef.current[applyBoxId] ?? null, [applyBoxId]) + const streamState = useCallback(() => editCodeService.getURIStreamState({ uri: applyingUri() }), [editCodeService, applyingUri]) // listen for stream updates useURIStreamState( - useCallback((uri, streamState) => { - const shouldUpdate = applyingUriRef.current?.fsPath === uri.fsPath - if (!shouldUpdate) return - setStreamState_(streamState) // editCodeService.getURIStreamState({ uri: applyingUriRef.current ?? null }) - }, [applyingUriRef, setStreamState_]) + useCallback((uri, newStreamState) => { + const shouldUpdate = applyingUri()?.fsPath !== uri.fsPath + if (shouldUpdate) return + rerender(c => c + 1) + if (newStreamState !== streamState()) console.log('AAAAAAAAAAAAAAAAAAA') + }, [applyBoxId, editCodeService, applyingUri, rerender, streamState]) ) const onSubmit = useCallback(() => { if (isDisabled) return - if (streamStateRef.current === 'streaming') return - const uri = editCodeService.startApplying({ + if (streamState() === 'streaming') return + const newApplyingUri = editCodeService.startApplying({ from: 'ClickApply', type: 'searchReplace', applyStr: codeStr, - chatApplyBoxId: applyBoxId, }) - setApplyingUri(uri) + applyingURIOfApplyBoxIdRef.current[applyBoxId] = newApplyingUri ?? undefined + rerender(c => c + 1) metricsService.capture('Apply Code', { length: codeStr.length }) // capture the length only - }, [editCodeService, applyBoxId, codeStr, metricsService, isDisabled, streamStateRef, setApplyingUri]) + }, [isDisabled, streamState, editCodeService, codeStr, applyBoxId, metricsService]) const onInterrupt = useCallback(() => { - if (streamStateRef.current !== 'streaming') return - if (!applyingUriRef.current) return + if (streamState() !== 'streaming') return + const uri = applyingUri() + if (!uri) return - editCodeService.interruptURIStreaming({ uri: applyingUriRef.current, }) + editCodeService.interruptURIStreaming({ uri }) metricsService.capture('Stop Apply', {}) - }, [editCodeService, metricsService, streamStateRef, applyingUriRef]) + }, [streamState, applyingUri,editCodeService, metricsService]) const isSingleLine = !codeStr.includes('\n') @@ -128,8 +127,8 @@ export const ApplyBlockHoverButtons = ({ codeStr, applyBoxId }: { codeStr: strin // 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`} onClick={() => { - if (!applyingUriRef.current) return - editCodeService.removeDiffAreas({ uri: applyingUriRef.current, behavior: 'accept', removeCtrlKs: false }) + const uri = applyingUri() + if (uri) editCodeService.removeDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false }) }} > Accept @@ -138,20 +137,21 @@ export const ApplyBlockHoverButtons = ({ codeStr, applyBoxId }: { codeStr: strin // 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`} onClick={() => { - if (!applyingUriRef.current) return - editCodeService.removeDiffAreas({ uri: applyingUriRef.current, behavior: 'reject', removeCtrlKs: false }) + const uri = applyingUri() + if (uri) editCodeService.removeDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false }) }} > Reject - console.log('streamStateRef.current', streamStateRef.current) + console.log('streamStateRef.current', streamState()) + const currStreamState = streamState() return <> - {streamStateRef.current !== 'streaming' && } - {streamStateRef.current === 'idle' && !isDisabled && applyButton} - {streamStateRef.current === 'streaming' && stopButton} - {streamStateRef.current === 'acceptRejectAll' && acceptRejectButtons} + {currStreamState !== 'streaming' && } + {currStreamState === 'idle' && !isDisabled && applyButton} + {currStreamState === 'streaming' && stopButton} + {currStreamState === 'acceptRejectAll' && acceptRejectButtons} } diff --git a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx index 1fbcc303..fe70caa3 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx @@ -65,7 +65,6 @@ export const QuickEditChat = ({ from: 'QuickEdit', type: 'rewrite', diffareaid, - chatApplyBoxId: null, }) }, [isStreamingRef, isDisabled, editCodeService, diffareaid]) From 96de4f028f7fee8ff66571d16aa742b4d1d33344 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sat, 22 Feb 2025 04:35:56 -0800 Subject: [PATCH 25/41] remove unnecessary check --- .../browser/react/src/markdown/ApplyBlockHoverButtons.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx index 8c9f26d1..b31bfb7b 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx @@ -76,8 +76,7 @@ export const ApplyBlockHoverButtons = ({ codeStr, applyBoxId }: { codeStr: strin const shouldUpdate = applyingUri()?.fsPath !== uri.fsPath if (shouldUpdate) return rerender(c => c + 1) - if (newStreamState !== streamState()) console.log('AAAAAAAAAAAAAAAAAAA') - }, [applyBoxId, editCodeService, applyingUri, rerender, streamState]) + }, [applyBoxId, editCodeService, applyingUri]) ) const onSubmit = useCallback(() => { @@ -101,7 +100,7 @@ export const ApplyBlockHoverButtons = ({ codeStr, applyBoxId }: { codeStr: strin editCodeService.interruptURIStreaming({ uri }) metricsService.capture('Stop Apply', {}) - }, [streamState, applyingUri,editCodeService, metricsService]) + }, [streamState, applyingUri, editCodeService, metricsService]) const isSingleLine = !codeStr.includes('\n') From f40acd76f100f35985d448c1c01565ff7f3503f9 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sat, 22 Feb 2025 17:52:28 -0800 Subject: [PATCH 26/41] endpoint --- .../contrib/void/electron-main/llmMessage/sendLLMMessage.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts index 0a182aec..b3a12eb6 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts @@ -35,6 +35,8 @@ export const sendLLMMessage = ({ metricsService.capture(eventId, { providerName, modelName, + customEndpointURL: settingsOfProvider[providerName].endpoint, + numModelsAtEndpoint: settingsOfProvider[providerName].models.length, ...messagesType === 'chatMessages' ? { numMessages: messages_?.length, messagesShape: messages_?.map(msg => ({ role: msg.role, length: msg.content.length })), From dd24c3180d5e2a2cc868cde1c299190be34433a2 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sat, 22 Feb 2025 17:52:58 -0800 Subject: [PATCH 27/41] + --- .../contrib/void/electron-main/llmMessage/sendLLMMessage.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts index b3a12eb6..3e07a8f5 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts @@ -35,8 +35,8 @@ export const sendLLMMessage = ({ metricsService.capture(eventId, { providerName, modelName, - customEndpointURL: settingsOfProvider[providerName].endpoint, - numModelsAtEndpoint: settingsOfProvider[providerName].models.length, + customEndpointURL: settingsOfProvider[providerName]?.endpoint, + numModelsAtEndpoint: settingsOfProvider[providerName].models?.length, ...messagesType === 'chatMessages' ? { numMessages: messages_?.length, messagesShape: messages_?.map(msg => ({ role: msg.role, length: msg.content.length })), From bf4392a52f1e65fa97082fd338583fe36d429750 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sat, 22 Feb 2025 17:53:14 -0800 Subject: [PATCH 28/41] safe --- .../contrib/void/electron-main/llmMessage/sendLLMMessage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts index 3e07a8f5..7bb4e141 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts @@ -36,7 +36,7 @@ export const sendLLMMessage = ({ providerName, modelName, customEndpointURL: settingsOfProvider[providerName]?.endpoint, - numModelsAtEndpoint: settingsOfProvider[providerName].models?.length, + numModelsAtEndpoint: settingsOfProvider[providerName]?.models?.length, ...messagesType === 'chatMessages' ? { numMessages: messages_?.length, messagesShape: messages_?.map(msg => ({ role: msg.role, length: msg.content.length })), From 2a876d8efe43558a42eee73c56f57886c1689c10 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sat, 22 Feb 2025 21:08:34 -0800 Subject: [PATCH 29/41] + --- .../void/browser/react/src/markdown/ChatMarkdownRender.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index 320dccbb..8168bed3 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -9,7 +9,6 @@ import { BlockCode } from './BlockCode.js' import { ChatMessageLocation, } from '../../../aiRegexService.js' import { nameToVscodeLanguage } from '../../../helpers/detectLanguage.js' import { ApplyBlockHoverButtons } from './ApplyBlockHoverButtons.js' -import { URI } from '../../../../../../../base/common/uri.js' type ApplyBoxLocation = ChatMessageLocation & { tokenIdx: string } From bdb897d03271dac674d9fae8d1579ee23103cc70 Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Sat, 22 Feb 2025 01:01:00 -0800 Subject: [PATCH 30/41] tool UI draft --- .../contrib/void/browser/chatThreadService.ts | 8 +- .../react/src/sidebar-tsx/SidebarChat.tsx | 153 ++++++++++++++- .../void/browser/react/tailwind.config.js | 7 + .../contrib/void/common/toolsService.ts | 178 +++++++++++------- 4 files changed, 269 insertions(+), 77 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index e6a52c54..d92fb772 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -48,13 +48,13 @@ export type FileSelection = { export type StagingSelectionItem = CodeSelection | FileSelection -type ToolMessage = { +export type ToolMessage = { role: 'tool'; name: T; // internal use params: string; // internal use id: string; // apis require this tool use id content: string; // result - result: ToolCallReturnType; // text message of result + result: ToolCallReturnType[T]; // text message of result } @@ -430,10 +430,10 @@ class ChatThreadService extends Disposable implements IChatThreadService { // 1. let toolResult: Awaited> - let toolResultVal: ToolCallReturnType + let toolResultVal: ToolCallReturnType[ToolName] try { toolResult = await this._toolsService.toolFns[toolName](tool.params) - toolResultVal = toolResult[0] + toolResultVal = toolResult } catch (error) { this._setStreamState(threadId, { error }) shouldSendAnotherMessage = false diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx index 3c95f434..7a0563c3 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx @@ -7,7 +7,7 @@ import React, { ButtonHTMLAttributes, FormEvent, FormHTMLAttributes, Fragment, K import { useAccessor, useSidebarState, useChatThreadsState, useChatThreadsStreamState, useUriState, useSettingsState } from '../util/services.js'; -import { ChatMessage, StagingSelectionItem } from '../../../chatThreadService.js'; +import { ChatMessage, StagingSelectionItem, ToolMessage } from '../../../chatThreadService.js'; import { BlockCode } from '../markdown/BlockCode.js'; import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js'; @@ -21,10 +21,11 @@ import { useScrollbarStyles } from '../util/useScrollbarStyles.js'; import { VOID_CTRL_L_ACTION_ID } from '../../../actionIDs.js'; import { filenameToVscodeLanguage } from '../../../helpers/detectLanguage.js'; import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js'; -import { Pencil, X } from 'lucide-react'; +import { ChevronRight, Pencil, X } from 'lucide-react'; import { FeatureName, isFeatureNameDisabled } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'; import { WarningBox } from '../void-settings-tsx/WarningBox.js'; import { ChatMessageLocation } from '../../../aiRegexService.js'; +import { ToolCallReturnType, ToolName } from '../../../../common/toolsService.js'; @@ -542,6 +543,146 @@ export const SelectedFiles = ( } +type ToolReusltToComponent = { [T in ToolName]: (props: { message: ToolMessage }) => React.ReactNode } +interface ToolResultProps { + title: string; + desc: string; + desc2?: number; + children?: React.ReactNode; +} + +const ToolResult = ({ + title, + desc, + desc2, + children, +}: ToolResultProps) => { + const [isExpanded, setIsExpanded] = useState(false); + + const isDropdown = !!children + + return ( +
    +
    +
    children && setIsExpanded(!isExpanded)} + > + {isDropdown && ( + + )} +
    + {title} + {`"`}{desc}{`"`} + {desc2 !== undefined && ( + + {`(`}{desc2}{` result`}{desc2 !== 1 ? 's' : ''}{`)`} + + )} +
    +
    +
    + {children} +
    +
    +
    + ); +}; + + + +const toolResultToComponent: ToolReusltToComponent = { + 'read_file': ({ message }) => ( + + ), + 'list_dir': ({ message }) => ( + +
    + {message.result.children?.map((item, i) => ( +
    + • {item.name} + {item.isDirectory && '/'} +
    + ))} + {message.result.hasNextPage && ( +
    + {message.result.itemsRemaining} more items... +
    + )} +
    +
    + ), + 'pathname_search': ({ message }) => ( + +
    + {Array.isArray(message.result.uris) ? + message.result.uris.map((uri, i) => ( + + )) : +
    {message.result.uris}
    + } + {message.result.hasNextPage && ( +
    + More results available... +
    + )} +
    +
    + ), + 'search': ({ message }) => ( + +
    + {typeof message.result.uris === 'string' ? + message.result.uris : + message.result.uris.map((uri, i) => ( + + )) + } + {message.result.hasNextPage && ( +
    + More results available... +
    + )} +
    +
    + ) +}; + + + type ChatBubbleMode = 'display' | 'edit' const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatMessage, messageIdx?: number, isLoading?: boolean, }) => { @@ -695,7 +836,13 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: { chatMessage: ChatM chatbubbleContents = } else if (role === 'tool') { - chatbubbleContents = chatMessage.name + + const ToolComponent = toolResultToComponent[chatMessage.name] as ({ message }: { message: any }) => React.ReactNode // ts isnt smart enough to deal with the types here... + + chatbubbleContents = + + console.log('tool result:', chatMessage.name, chatMessage.params, chatMessage.result) + } return
    = keyof typeof voidTools[T]['params'] export type ToolParamsObj = { [paramName in ToolParamNames]: 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': {} +} -export type ToolCallReturnType - = T extends 'read_file' ? string - : T extends 'list_dir' ? string - : T extends 'pathname_search' ? string | URI[] - : T extends 'search' ? string | URI[] - : T extends 'create_file' ? string - : never +type DirectoryItem = { + name: string; + isDirectory: boolean; + isSymbolicLink: boolean; +} -export type ToolFns = { [T in ToolName]: (p: string) => Promise<[ToolCallReturnType, boolean]> } -export type ToolResultToString = { [T in ToolName]: (result: [ToolCallReturnType, boolean]) => string } +export type ToolFns = { [T in ToolName]: (p: string) => Promise } +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 MAX_DEPTH = 1 -async function generateDirectoryTreeMd(fileService: IFileService, rootURI: URI, pageNumber: number): Promise<[string, boolean]> { - let output = ''; - const indentation = (depth: number, isLast: boolean): string => { - if (depth === 0) return ''; - return `${'| '.repeat(depth - 1)}${isLast ? '└── ' : '├── '}`; - }; - - let hasNextPage = false - - async function traverseChildren(uri: URI, depth: number, isLast: boolean) { - const stat = await fileService.resolve(uri, { resolveMetadata: false }); - - // we might want to say where symlink links to - if (depth === 0 && pageNumber !== 1) - output += '' - else - output += `${indentation(depth, isLast)}${stat.name}${stat.isDirectory ? '/' : ''}${stat.isSymbolicLink ? ` (symbolic link)` : ''}\n`; - - // list children - 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) ?? []; - - if (!stat.isDirectory) return; - - if (listChildren.length === 0) return - if (depth === MAX_DEPTH) return // right now MAX_DEPTH=1 to make pagination work nicely - - for (let i = 0; i < Math.min(listChildren.length, MAX_CHILDREN_URIs_PAGE); i++) { - await traverseChildren(listChildren[i].resource, depth + 1, i === listChildren.length - 1); - } - const nCutoffResults = (originalChildrenLength - 1) - toChildIdx - if (nCutoffResults >= 1) { - output += `${indentation(depth + 1, true)}(${nCutoffResults} results remaining...)\n` - hasNextPage = true - } +const computeDirectoryResult = async ( + fileService: IFileService, + rootURI: URI, + pageNumber: number = 1 +): Promise => { + const stat = await fileService.resolve(rootURI, { resolveMetadata: false }); + if (!stat.isDirectory) { + return { rootURI, children: null, hasNextPage: false, hasPrevPage: false, itemsRemaining: 0 }; } - await traverseChildren(rootURI, 0, false); + 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, + isDirectory: child.isDirectory, + isSymbolicLink: child.isSymbolicLink || false + })); + + 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; +}; + + - return [output, hasNextPage] -} const validateJSON = (s: string): { [s: string]: unknown } => { @@ -217,6 +241,8 @@ export class ToolsService implements IToolsService { this.toolFns = { read_file: async (s: string) => { + console.log('read_file') + const o = validateJSON(s) const { uri: uriStr, pageNumber: pageNumberUnknown } = o @@ -227,22 +253,30 @@ export class ToolsService implements IToolsService { const fromIdx = MAX_FILE_CHARS_PAGE * (pageNumber - 1) const toIdx = MAX_FILE_CHARS_PAGE * pageNumber - 1 - let fileContents = readFileContents.slice(fromIdx, toIdx + 1) // paginate + const fileContents = readFileContents.slice(fromIdx, toIdx + 1) || '(empty)' // paginate const hasNextPage = (readFileContents.length - 1) - toIdx >= 1 - return [fileContents || '(empty)', hasNextPage] + + 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 [treeStr, hasNextPage] = await generateDirectoryTreeMd(fileService, uri, pageNumber) - return [treeStr, hasNextPage] + 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 @@ -254,15 +288,18 @@ export class ToolsService implements IToolsService { const fromIdx = MAX_CHILDREN_URIs_PAGE * (pageNumber - 1) const toIdx = MAX_CHILDREN_URIs_PAGE * pageNumber - 1 - const URIs = data.results + 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 [URIs, hasNextPage] + return { queryStr, uris, hasNextPage } }, search: async (s: string) => { + console.log('search') + const o = validateJSON(s) const { query: queryUnknown, pageNumber: pageNumberUnknown } = o @@ -274,35 +311,37 @@ export class ToolsService implements IToolsService { const fromIdx = MAX_CHILDREN_URIs_PAGE * (pageNumber - 1) const toIdx = MAX_CHILDREN_URIs_PAGE * pageNumber - 1 - const URIs = data.results + const uris = data.results .slice(fromIdx, toIdx + 1) // paginate .map(({ resource, results }) => resource) const hasNextPage = (data.results.length - 1) - toIdx >= 1 - return [URIs, hasNextPage] + console.log('search result:', uris) + + return { queryStr, uris, hasNextPage } }, } - const nextPageStr = (hasNextPage: boolean) => hasNextPage ? '\n\n(more on next page...)' : '' this.toolResultToString = { - read_file: ([fileContents, hasNextPage]) => { - return fileContents + nextPageStr(hasNextPage) + read_file: (result) => { + return nextPageStr(result.hasNextPage) }, - list_dir: ([dirTreeStr, hasNextPage]) => { - return dirTreeStr + nextPageStr(hasNextPage) + list_dir: (result) => { + const dirTreeStr = directoryResultToString(result) + return dirTreeStr + nextPageStr(result.hasNextPage) }, - pathname_search: ([URIs, hasNextPage]) => { - if (typeof URIs === 'string') return URIs - return URIs.map(uri => uri.fsPath).join('\n') + nextPageStr(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: ([URIs, hasNextPage]) => { - if (typeof URIs === 'string') return URIs - return URIs.map(uri => uri.fsPath).join('\n') + nextPageStr(hasNextPage) + search: (result) => { + if (typeof result.uris === 'string') return result.uris + return result.uris.map(uri => uri.fsPath).join('\n') + nextPageStr(result.hasNextPage) }, } @@ -314,4 +353,3 @@ export class ToolsService implements IToolsService { } registerSingleton(IToolsService, ToolsService, InstantiationType.Eager); - From a9b3cc146bf4ff62ce1d1d8d586729ce1baa715d Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Sat, 22 Feb 2025 02:07:33 -0800 Subject: [PATCH 31/41] refactor --- .../react/src/sidebar-tsx/SidebarChat.tsx | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx index 7a0563c3..23767833 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx @@ -545,16 +545,16 @@ export const SelectedFiles = ( type ToolReusltToComponent = { [T in ToolName]: (props: { message: ToolMessage }) => React.ReactNode } interface ToolResultProps { - title: string; - desc: string; - desc2?: number; + actionTitle: string; + actionParam: string; + actionNumResults?: number; children?: React.ReactNode; } const ToolResult = ({ - title, - desc, - desc2, + actionTitle, + actionParam, + actionNumResults, children, }: ToolResultProps) => { const [isExpanded, setIsExpanded] = useState(false); @@ -563,7 +563,7 @@ const ToolResult = ({ return (
    -
    +
    children && setIsExpanded(!isExpanded)} @@ -574,11 +574,11 @@ const ToolResult = ({ /> )}
    - {title} - {`"`}{desc}{`"`} - {desc2 !== undefined && ( + {actionTitle} + {`"`}{actionParam}{`"`} + {actionNumResults !== undefined && ( - {`(`}{desc2}{` result`}{desc2 !== 1 ? 's' : ''}{`)`} + {`(`}{actionNumResults}{` result`}{actionNumResults !== 1 ? 's' : ''}{`)`} )}
    @@ -598,15 +598,15 @@ const ToolResult = ({ const toolResultToComponent: ToolReusltToComponent = { 'read_file': ({ message }) => ( ), 'list_dir': ({ message }) => (
    {message.result.children?.map((item, i) => ( @@ -625,9 +625,9 @@ const toolResultToComponent: ToolReusltToComponent = { ), 'pathname_search': ({ message }) => (
    {Array.isArray(message.result.uris) ? @@ -653,9 +653,9 @@ const toolResultToComponent: ToolReusltToComponent = { ), 'search': ({ message }) => (
    {typeof message.result.uris === 'string' ? From d96a9d5f6b3f4de26def58f48e1126cb0f3aa51e Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Sat, 22 Feb 2025 02:51:33 -0800 Subject: [PATCH 32/41] fix typeerror --- .../react/src/sidebar-tsx/SidebarThreadSelector.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx index 72dc6d39..6da3d5b9 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarThreadSelector.tsx @@ -68,13 +68,14 @@ export const SidebarThreadSelector = () => { let firstMsg = null; // let secondMsg = null; - const firstMsgIdx = pastThread.messages.findIndex( - (msg) => msg.role !== 'system' && !!msg.displayContent + const firstUserMsgIdx = pastThread.messages.findIndex( + (msg) => msg.role !== 'system' && msg.role !== 'tool' && !!msg.displayContent ); - if (firstMsgIdx !== -1) { + if (firstUserMsgIdx !== -1) { // firstMsg = truncate(pastThread.messages[firstMsgIdx].displayContent ?? ''); - firstMsg = pastThread.messages[firstMsgIdx].displayContent ?? ''; + const firsUsertMsgObj = pastThread.messages[firstUserMsgIdx] + firstMsg = firsUsertMsgObj.role === 'user' && firsUsertMsgObj.displayContent || ''; } else { firstMsg = '""'; } From c650091418af8ab3cce23c1991ea9d9d712db9ab Mon Sep 17 00:00:00 2001 From: Mathew Pareles Date: Sat, 22 Feb 2025 21:10:39 -0800 Subject: [PATCH 33/41] style --- .../browser/react/src/sidebar-tsx/SidebarChat.tsx | 12 ++++++------ .../contrib/void/browser/react/src/util/inputs.tsx | 2 +- .../contrib/void/browser/react/tailwind.config.js | 1 + 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx index 23767833..96e22bd4 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx @@ -610,8 +610,8 @@ const toolResultToComponent: ToolReusltToComponent = { >
    {message.result.children?.map((item, i) => ( -
    - • {item.name} +
    + {item.name} {item.isDirectory && '/'}
    ))} @@ -632,12 +632,12 @@ const toolResultToComponent: ToolReusltToComponent = {
    {Array.isArray(message.result.uris) ? message.result.uris.map((uri, i) => ( -
    + )) : @@ -661,12 +661,12 @@ const toolResultToComponent: ToolReusltToComponent = { {typeof message.result.uris === 'string' ? message.result.uris : message.result.uris.map((uri, i) => ( -
    + )) diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx index 270870a7..be327655 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx @@ -713,7 +713,7 @@ export const VoidCodeEditor = ({ initValue, language, maxHeight, showScrollbars onCreateInstance={useCallback((editor: CodeEditorWidget) => { const model = modelOfEditorId[id] ?? modelService.createModel( - initValueRef.current, { + initValueRef.current + '\n', { languageId: languageRef.current ? languageRef.current : 'typescript', onDidChange: (e) => { return { dispose: () => { } } } // no idea why they'd require this }) diff --git a/src/vs/workbench/contrib/void/browser/react/tailwind.config.js b/src/vs/workbench/contrib/void/browser/react/tailwind.config.js index f9fcaef6..bc57116b 100644 --- a/src/vs/workbench/contrib/void/browser/react/tailwind.config.js +++ b/src/vs/workbench/contrib/void/browser/react/tailwind.config.js @@ -28,6 +28,7 @@ module.exports = { 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)", From 2c2714273effb7b59be63e58271f5dfb17e1402d Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sun, 23 Feb 2025 01:28:48 -0800 Subject: [PATCH 34/41] partway through adding better support for more providers --- .vscode/settings.json | 2 +- .../void/browser/autocompleteService.ts | 33 +- .../contrib/void/common/llmMessageService.ts | 2 + .../contrib/void/common/llmMessageTypes.ts | 1 + .../contrib/void/common/voidSettingsTypes.ts | 435 +-------------- .../void/electron-main/llmMessage/MODELS.ts | 509 ++++++++++++++++++ .../void/electron-main/llmMessage/_old.ts | 387 +++++++++++++ .../electron-main/llmMessage/anthropic.ts | 114 ---- .../void/electron-main/llmMessage/ollama.ts | 124 ----- .../void/electron-main/llmMessage/openai.ts | 231 -------- .../llmMessage/postprocessToolCalls.ts | 8 - .../llmMessage/preprocessLLMMessages.ts | 418 +++++++------- .../llmMessage/sendLLMMessage.ts | 14 +- .../void/electron-main/llmMessageChannel.ts | 3 +- 14 files changed, 1173 insertions(+), 1108 deletions(-) create mode 100644 src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts delete mode 100644 src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts delete mode 100644 src/vs/workbench/contrib/void/electron-main/llmMessage/ollama.ts delete mode 100644 src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts delete mode 100644 src/vs/workbench/contrib/void/electron-main/llmMessage/postprocessToolCalls.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 910e0bec..d24ab6e5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -113,7 +113,7 @@ "files.insertFinalNewline": false }, "[typescript]": { - "editor.defaultFormatter": "vscode.typescript-language-features", + "editor.defaultFormatter": "ms-vsliveshare.vsliveshare", "editor.formatOnSave": true }, "[javascript]": { diff --git a/src/vs/workbench/contrib/void/browser/autocompleteService.ts b/src/vs/workbench/contrib/void/browser/autocompleteService.ts index 5fc8ac76..f9cbec7b 100644 --- a/src/vs/workbench/contrib/void/browser/autocompleteService.ts +++ b/src/vs/workbench/contrib/void/browser/autocompleteService.ts @@ -795,26 +795,27 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ }, useProviderFor: 'Autocomplete', logging: { loggingName: 'Autocomplete' }, - onText: async ({ fullText, newText }) => { + onText: () => { }, // unused in FIMMessage + // onText: async ({ fullText, newText }) => { - newAutocompletion.insertText = fullText + // newAutocompletion.insertText = fullText - // count newlines in newText - const numNewlines = newText.match(/\n|\r\n/g)?.length || 0 - newAutocompletion._newlineCount += numNewlines + // // count newlines in newText + // const numNewlines = newText.match(/\n|\r\n/g)?.length || 0 + // newAutocompletion._newlineCount += numNewlines - // if too many newlines, resolve up to last newline - if (newAutocompletion._newlineCount > 10) { - const lastNewlinePos = fullText.lastIndexOf('\n') - newAutocompletion.insertText = fullText.substring(0, lastNewlinePos) - resolve(newAutocompletion.insertText) - return - } + // // if too many newlines, resolve up to last newline + // if (newAutocompletion._newlineCount > 10) { + // const lastNewlinePos = fullText.lastIndexOf('\n') + // newAutocompletion.insertText = fullText.substring(0, lastNewlinePos) + // resolve(newAutocompletion.insertText) + // return + // } - // if (!getAutocompletionMatchup({ prefix: this._lastPrefix, autocompletion: newAutocompletion })) { - // reject('LLM response did not match user\'s text.') - // } - }, + // // if (!getAutocompletionMatchup({ prefix: this._lastPrefix, autocompletion: newAutocompletion })) { + // // reject('LLM response did not match user\'s text.') + // // } + // }, onFinalMessage: ({ fullText }) => { // console.log('____res: ', JSON.stringify(newAutocompletion.insertText)) diff --git a/src/vs/workbench/contrib/void/common/llmMessageService.ts b/src/vs/workbench/contrib/void/common/llmMessageService.ts index bb6cf09c..c8f9ea2c 100644 --- a/src/vs/workbench/contrib/void/common/llmMessageService.ts +++ b/src/vs/workbench/contrib/void/common/llmMessageService.ts @@ -157,6 +157,7 @@ export class LLMMessageService extends Disposable implements ILLMMessageService this.channel.call('ollamaList', { ...proxyParams, settingsOfProvider, + providerName: 'ollama', requestId: requestId_, } satisfies MainModelListParams) } @@ -175,6 +176,7 @@ export class LLMMessageService extends Disposable implements ILLMMessageService this.channel.call('openAICompatibleList', { ...proxyParams, settingsOfProvider, + providerName: 'openAICompatible', requestId: requestId_, } satisfies MainModelListParams) } diff --git a/src/vs/workbench/contrib/void/common/llmMessageTypes.ts b/src/vs/workbench/contrib/void/common/llmMessageTypes.ts index 0956b08b..58989cce 100644 --- a/src/vs/workbench/contrib/void/common/llmMessageTypes.ts +++ b/src/vs/workbench/contrib/void/common/llmMessageTypes.ts @@ -191,6 +191,7 @@ export type OpenaiCompatibleModelResponse = { // params to the true list fn export type ModelListParams = { + providerName: ProviderName; settingsOfProvider: SettingsOfProvider; onSuccess: (param: { models: modelResponse[] }) => void; onError: (param: { error: string }) => void; diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index 529a872c..fb387bc1 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -4,367 +4,13 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ +import { defaultModelsOfProvider } from '../electron-main/llmMessage/MODELS.js'; import { VoidSettingsState } from './voidSettingsService.js' - -// developer info used in sendLLMMessage -export type DeveloperInfoAtModel = { - // USED: - supportsSystemMessage: 'developer' | boolean, // if null, we will just do a string of system message. this is independent from separateSystemMessage, which takes priority and is passed directly in each provider's implementation. - supportsTools: boolean, // we will just do a string of tool use if it doesn't support - - // UNUSED (coming soon): - // TODO!!! think tokens - deepseek - _recognizedModelName: RecognizedModelName, // used to show user if model was auto-recognized - _supportsStreaming: boolean, // we will just dump the final result if doesn't support it - _supportsAutocompleteFIM: boolean, // we will just do a description of FIM if it doens't support <|fim_hole|> - _maxTokens: number, // required -} - -export type DeveloperInfoAtProvider = { - overrideSettingsForAllModels?: Partial; // any overrides for models that a provider might have (e.g. if a provider always supports tool use, even if we don't recognize the model we can set tools to true) -} - - - - - -export type VoidModelInfo = { // <-- STATEFUL - modelName: string, - isDefault: boolean, // whether or not it's a default for its provider - isHidden: boolean, // whether or not the user is hiding it (switched off) - isAutodetected?: boolean, // whether the model was autodetected by polling -} & DeveloperInfoAtModel - - - - - -export const recognizedModels = [ - // chat - 'OpenAI 4o', - 'Anthropic Claude', - 'Llama 3.x', - 'Deepseek Chat', // deepseek coder v2 is now merged into chat (V3) https://api-docs.deepseek.com/updates#deepseek-coder--deepseek-chat-upgraded-to-deepseek-v25-model - 'xAI Grok', - // 'xAI Grok', - // 'Google Gemini, Gemma', - // 'Microsoft Phi4', - - - // coding (autocomplete) - 'Alibaba Qwen2.5 Coder Instruct', // we recommend this over Qwen2.5 - 'Mistral Codestral', - - // thinking - 'OpenAI o1', - 'Deepseek R1', - - // general - // 'Mixtral 8x7b' - // 'Qwen2.5', - -] as const - -type RecognizedModelName = (typeof recognizedModels)[number] | '' - - -export function recognizedModelOfModelName(modelName: string): RecognizedModelName { - const lower = modelName.toLowerCase(); - - if (lower.includes('gpt-4o')) - return 'OpenAI 4o'; - if (lower.includes('claude')) - return 'Anthropic Claude'; - if (lower.includes('llama')) - return 'Llama 3.x'; - if (lower.includes('qwen2.5-coder')) - return 'Alibaba Qwen2.5 Coder Instruct'; - if (lower.includes('mistral')) - return 'Mistral Codestral'; - if (/\bo1\b/.test(lower) || /\bo3\b/.test(lower)) // o1, o3 - return 'OpenAI o1'; - if (lower.includes('deepseek-r1') || lower.includes('deepseek-reasoner')) - return 'Deepseek R1'; - if (lower.includes('deepseek')) - return 'Deepseek Chat' - if (lower.includes('grok')) - return 'xAI Grok' - - return ''; -} - - -const developerInfoAtProvider: { [providerName in ProviderName]: DeveloperInfoAtProvider } = { - 'anthropic': { - overrideSettingsForAllModels: { - supportsSystemMessage: true, - supportsTools: true, - _supportsAutocompleteFIM: false, - _supportsStreaming: true, - } - }, - 'deepseek': { - overrideSettingsForAllModels: { - } - }, - 'ollama': { - }, - 'openRouter': { - }, - 'openAICompatible': { - }, - 'openAI': { - }, - 'gemini': { - }, - 'mistral': { - }, - 'groq': { - }, - 'xAI': { - }, - 'vLLM': { - }, -} -export const developerInfoOfProviderName = (providerName: ProviderName): Partial => { - return developerInfoAtProvider[providerName] ?? {} -} - - - - -// providerName is optional, but gives some extra fallbacks if provided -const developerInfoOfRecognizedModelName: { [recognizedModel in RecognizedModelName]: Omit } = { - 'OpenAI 4o': { - supportsSystemMessage: true, - supportsTools: true, - _supportsAutocompleteFIM: false, - _supportsStreaming: true, - _maxTokens: 4096, - }, - - 'Anthropic Claude': { - supportsSystemMessage: true, - supportsTools: false, - _supportsAutocompleteFIM: false, - _supportsStreaming: false, - _maxTokens: 4096, - }, - - 'Llama 3.x': { - supportsSystemMessage: true, - supportsTools: true, - _supportsAutocompleteFIM: false, - _supportsStreaming: false, - _maxTokens: 4096, - }, - - 'xAI Grok': { - supportsSystemMessage: true, - supportsTools: true, - _supportsAutocompleteFIM: false, - _supportsStreaming: true, - _maxTokens: 4096, - - }, - - 'Deepseek Chat': { - supportsSystemMessage: true, - supportsTools: false, - _supportsAutocompleteFIM: false, - _supportsStreaming: false, - _maxTokens: 4096, - }, - - 'Alibaba Qwen2.5 Coder Instruct': { - supportsSystemMessage: true, - supportsTools: true, - _supportsAutocompleteFIM: false, - _supportsStreaming: false, - _maxTokens: 4096, - }, - - 'Mistral Codestral': { - supportsSystemMessage: true, - supportsTools: true, - _supportsAutocompleteFIM: false, - _supportsStreaming: false, - _maxTokens: 4096, - }, - - 'OpenAI o1': { - supportsSystemMessage: 'developer', - supportsTools: false, - _supportsAutocompleteFIM: false, - _supportsStreaming: true, - _maxTokens: 4096, - }, - - 'Deepseek R1': { - supportsSystemMessage: false, - supportsTools: false, - _supportsAutocompleteFIM: false, - _supportsStreaming: false, - _maxTokens: 4096, - }, - - - '': { - supportsSystemMessage: false, - supportsTools: false, - _supportsAutocompleteFIM: false, - _supportsStreaming: false, - _maxTokens: 4096, - }, -} -export const developerInfoOfModelName = (modelName: string, overrides?: Partial): DeveloperInfoAtModel => { - const recognizedModelName = recognizedModelOfModelName(modelName) - return { - _recognizedModelName: recognizedModelName, - ...developerInfoOfRecognizedModelName[recognizedModelName], - ...overrides - } -} - - - - - - -// creates `modelInfo` from `modelNames` -export const modelInfoOfDefaultModelNames = (defaultModelNames: string[]): VoidModelInfo[] => { - return defaultModelNames.map((modelName, i) => ({ - modelName, - isDefault: true, - isAutodetected: false, - isHidden: defaultModelNames.length >= 10, // hide all models if there are a ton of them, and make user enable them individually - ...developerInfoOfModelName(modelName), - })) -} - -export const modelInfoOfAutodetectedModelNames = (defaultModelNames: string[], options: { existingModels: VoidModelInfo[] }) => { - const { existingModels } = options - - const existingModelsMap: Record = {} - for (const existingModel of existingModels) { - existingModelsMap[existingModel.modelName] = existingModel - } - - return defaultModelNames.map((modelName, i) => ({ - modelName, - isDefault: true, - isAutodetected: true, - isHidden: !!existingModelsMap[modelName]?.isHidden, - ...developerInfoOfModelName(modelName) - })) -} - - - - - -// https://docs.anthropic.com/en/docs/about-claude/models -export const defaultAnthropicModels = modelInfoOfDefaultModelNames([ - 'claude-3-5-sonnet-20241022', - 'claude-3-5-haiku-20241022', - 'claude-3-opus-20240229', - 'claude-3-sonnet-20240229', - // 'claude-3-haiku-20240307', -]) - - -// https://platform.openai.com/docs/models/gp -export const defaultOpenAIModels = modelInfoOfDefaultModelNames([ - 'o1', - 'o1-mini', - 'o3-mini', - 'gpt-4o', - 'gpt-4o-mini', - // 'gpt-4o-2024-05-13', - // 'gpt-4o-2024-08-06', - // 'gpt-4o-mini-2024-07-18', - // 'gpt-4-turbo', - // 'gpt-4-turbo-2024-04-09', - // 'gpt-4-turbo-preview', - // 'gpt-4-0125-preview', - // 'gpt-4-1106-preview', - // 'gpt-4', - // 'gpt-4-0613', - // 'gpt-3.5-turbo-0125', - // 'gpt-3.5-turbo', - // 'gpt-3.5-turbo-1106', -]) - -// https://platform.openai.com/docs/models/gp -export const defaultDeepseekModels = modelInfoOfDefaultModelNames([ - 'deepseek-chat', - 'deepseek-reasoner', -]) - - -// https://console.groq.com/docs/models -export const defaultGroqModels = modelInfoOfDefaultModelNames([ - "llama3-70b-8192", - "llama-3.3-70b-versatile", - "llama-3.1-8b-instant", - "gemma2-9b-it", - "mixtral-8x7b-32768" -]) - - -export const defaultGeminiModels = modelInfoOfDefaultModelNames([ - 'gemini-1.5-flash', - 'gemini-1.5-pro', - 'gemini-1.5-flash-8b', - 'gemini-2.0-flash-exp', - 'gemini-2.0-flash-thinking-exp-1219', - 'learnlm-1.5-pro-experimental' -]) - -export const defaultMistralModels = modelInfoOfDefaultModelNames([ - "codestral-latest", - "open-codestral-mamba", - "open-mistral-nemo", - "mistral-large-latest", - "pixtral-large-latest", - "ministral-3b-latest", - "ministral-8b-latest", - "mistral-small-latest", -]) - -export const defaultXAIModels = modelInfoOfDefaultModelNames([ - 'grok-2-latest', - 'grok-3-latest', -]) -// export const parseMaxTokensStr = (maxTokensStr: string) => { -// // parse the string but only if the full string is a valid number, eg parseInt('100abc') should return NaN -// const int = isNaN(Number(maxTokensStr)) ? undefined : parseInt(maxTokensStr) -// if (Number.isNaN(int)) -// return undefined -// return int -// } - - - - -export const anthropicMaxPossibleTokens = (modelName: string) => { - if (modelName === 'claude-3-5-sonnet-20241022' - || modelName === 'claude-3-5-haiku-20241022') - return 8192 - if (modelName === 'claude-3-opus-20240229' - || modelName === 'claude-3-sonnet-20240229' - || modelName === 'claude-3-haiku-20240307') - return 4096 - return 1024 // return a reasonably small number if they're using a different model -} - - type UnionOfKeys = T extends T ? keyof T : never; - export const defaultProviderSettings = { anthropic: { apiKey: '', @@ -418,6 +64,14 @@ export const customSettingNamesOfProvider = (providerName: ProviderName) => { +export type VoidModelInfo = { // <-- STATEFUL + modelName: string, + isDefault: boolean, // whether or not it's a default for its provider + isHidden: boolean, // whether or not the user is hiding it (switched off) + isAutodetected?: boolean, // whether the model was autodetected by polling +} // TODO!!! eventually we'd want to let the user change supportsFIM, etc on the model themselves + + type CommonProviderSettings = { _didFillInProviderSettings: boolean | undefined, // undefined initially, computed when user types in all fields @@ -434,10 +88,6 @@ export type SettingsOfProvider = { export type SettingName = keyof SettingsAtProvider - - - - type DisplayInfoForProviderName = { title: string, desc?: string, @@ -584,110 +234,83 @@ const defaultCustomSettings: Record = { } - -export const voidInitModelOptions = { - anthropic: { - models: defaultAnthropicModels, - }, - openAI: { - models: defaultOpenAIModels, - }, - deepseek: { - models: defaultDeepseekModels, - }, - ollama: { - models: [], - }, - vLLM: { - models: [], - }, - openRouter: { - models: [], // any string - }, - openAICompatible: { - models: [], - }, - gemini: { - models: defaultGeminiModels, - }, - groq: { - models: defaultGroqModels, - }, - mistral: { - models: defaultMistralModels, - }, - xAI: { - models: defaultXAIModels, +const modelInfoOfDefaultModelNames = (defaultModelNames: string[]): { models: VoidModelInfo[] } => { + return { + models: defaultModelNames.map((modelName, i) => ({ + modelName, + isDefault: true, + isAutodetected: false, + isHidden: defaultModelNames.length >= 10, // hide all models if there are a ton of them, and make user enable them individually + })) } -} satisfies Record - +} // used when waiting and for a type reference export const defaultSettingsOfProvider: SettingsOfProvider = { anthropic: { ...defaultCustomSettings, ...defaultProviderSettings.anthropic, - ...voidInitModelOptions.anthropic, + ...modelInfoOfDefaultModelNames(defaultModelsOfProvider.anthropic), _didFillInProviderSettings: undefined, }, openAI: { ...defaultCustomSettings, ...defaultProviderSettings.openAI, - ...voidInitModelOptions.openAI, + ...modelInfoOfDefaultModelNames(defaultModelsOfProvider.openAI), _didFillInProviderSettings: undefined, }, deepseek: { ...defaultCustomSettings, ...defaultProviderSettings.deepseek, - ...voidInitModelOptions.deepseek, + ...modelInfoOfDefaultModelNames(defaultModelsOfProvider.deepseek), _didFillInProviderSettings: undefined, }, gemini: { ...defaultCustomSettings, ...defaultProviderSettings.gemini, - ...voidInitModelOptions.gemini, + ...modelInfoOfDefaultModelNames(defaultModelsOfProvider.gemini), _didFillInProviderSettings: undefined, }, mistral: { ...defaultCustomSettings, ...defaultProviderSettings.mistral, - ...voidInitModelOptions.mistral, + ...modelInfoOfDefaultModelNames(defaultModelsOfProvider.mistral), _didFillInProviderSettings: undefined, }, xAI: { ...defaultCustomSettings, ...defaultProviderSettings.xAI, - ...voidInitModelOptions.xAI, + ...modelInfoOfDefaultModelNames(defaultModelsOfProvider.xAI), _didFillInProviderSettings: undefined, }, groq: { // aggregator ...defaultCustomSettings, ...defaultProviderSettings.groq, - ...voidInitModelOptions.groq, + ...modelInfoOfDefaultModelNames(defaultModelsOfProvider.groq), _didFillInProviderSettings: undefined, }, openRouter: { // aggregator ...defaultCustomSettings, ...defaultProviderSettings.openRouter, - ...voidInitModelOptions.openRouter, + ...modelInfoOfDefaultModelNames(defaultModelsOfProvider.openRouter), _didFillInProviderSettings: undefined, }, openAICompatible: { // aggregator ...defaultCustomSettings, ...defaultProviderSettings.openAICompatible, - ...voidInitModelOptions.openAICompatible, + ...modelInfoOfDefaultModelNames(defaultModelsOfProvider.openAICompatible), _didFillInProviderSettings: undefined, }, ollama: { // aggregator ...defaultCustomSettings, ...defaultProviderSettings.ollama, - ...voidInitModelOptions.ollama, + ...modelInfoOfDefaultModelNames(defaultModelsOfProvider.ollama), _didFillInProviderSettings: undefined, }, vLLM: { // aggregator ...defaultCustomSettings, ...defaultProviderSettings.vLLM, - ...voidInitModelOptions.vLLM, + ...modelInfoOfDefaultModelNames(defaultModelsOfProvider.vLLM), _didFillInProviderSettings: undefined, }, } diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts new file mode 100644 index 00000000..3cefdfa9 --- /dev/null +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts @@ -0,0 +1,509 @@ +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ + +import OpenAI from 'openai'; +import { Model as OpenAIModel } from 'openai/resources/models.js'; +import { _InternalModelListFnType, _InternalSendLLMFIMMessageFnType, _InternalSendLLMChatMessageFnType, OllamaModelResponse } from '../../common/llmMessageTypes.js'; +import { InternalToolInfo, ToolName, toolNames } from '../../common/toolsService.js'; +import { defaultProviderSettings, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js'; +import { prepareMessages } from './preprocessLLMMessages.js'; +import Anthropic from '@anthropic-ai/sdk'; +import { Ollama } from 'ollama'; + + + +export const defaultModelsOfProvider = { + anthropic: [ // https://docs.anthropic.com/en/docs/about-claude/models + 'claude-3-5-sonnet-latest', + 'claude-3-5-haiku-latest', + 'claude-3-opus-latest', + ], + openAI: [ // https://platform.openai.com/docs/models/gp + 'o1', + 'o1-mini', + 'o3-mini', + 'gpt-4o', + 'gpt-4o-mini', + ], + deepseek: [ // https://platform.openai.com/docs/models/gp + 'deepseek-chat', + 'deepseek-reasoner', + ], + ollama: [], + vLLM: [], + openRouter: [], + openAICompatible: [], + gemini: [ + 'gemini-1.5-flash', + 'gemini-1.5-pro', + 'gemini-1.5-flash-8b', + 'gemini-2.0-flash-exp', + 'gemini-2.0-flash-thinking-exp-1219', + 'learnlm-1.5-pro-experimental' + ], + groq: [ // https://console.groq.com/docs/models + "llama3-70b-8192", + "llama-3.3-70b-versatile", + "llama-3.1-8b-instant", + "gemma2-9b-it", + "mixtral-8x7b-32768" + ], + mistral: [ // https://docs.mistral.ai/getting-started/models/models_overview/ + "codestral-latest", + "open-codestral-mamba", + "open-mistral-nemo", + "mistral-large-latest", + "pixtral-large-latest", + "ministral-3b-latest", + "ministral-8b-latest", + "mistral-small-latest", + ], + xAI: [ // https://docs.x.ai/docs/models?cluster=us-east-1 + 'grok-3-latest', + 'grok-2-latest', + ], +} satisfies Record + + + +type ProviderSettings = { + thinkingFormat: string; + toolsFormat: string; + FIMFormat: string; + modelOptions: { + [key: string]: { + contextWindow: number; + cost: { + input: number; + output: number; + cache_read?: number; + cache_write?: number; + }; + supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated'; + supportsTools: false | 'anthropic-style' | 'openai-style'; + }; + }; +}; + + +const openAIProviderSettings: ProviderSettings = { + + thinkingFormat: '', + + toolsFormat: '', + + FIMFormat: '', + + modelOptions: { + "o1": { + contextWindow: 128_000, + cost: { input: 15.00, cache_read: 7.50, output: 60.00, }, + supportsTools: false, + supportsSystemMessage: 'developer-role', + }, + "o3-mini": { + contextWindow: 200_000, + cost: { input: 1.10, cache_read: 0.55, output: 4.40, }, + supportsTools: false, + supportsSystemMessage: 'developer-role', + }, + "gpt-4o": { + contextWindow: 128_000, + cost: { input: 2.50, cache_read: 1.25, output: 10.00, }, + supportsTools: 'openai-style', + supportsSystemMessage: 'system-role', + }, + } + +} + + + + + +const anthropicProviderSettings: ProviderSettings = { + thinkingFormat: '', + + toolsFormat: '', + + FIMFormat: '', + + modelOptions: { + "claude-3-5-sonnet-20241022": { + contextWindow: 200_000, + cost: { input: 3.00, cache_read: 0.30, cache_write: 3.75, output: 15.00 }, + supportsSystemMessage: 'system-role', + supportsTools: 'anthropic-style', + + }, + "claude-3-5-haiku-20241022": { + contextWindow: 200_000, + cost: { input: 0.80, cache_read: 0.08, cache_write: 1.00, output: 4.00 }, + supportsSystemMessage: 'system-role', + supportsTools: 'anthropic-style', + }, + "claude-3-opus-20240229": { + contextWindow: 200_000, + cost: { input: 15.00, cache_read: 1.50, cache_write: 18.75, output: 75.00 }, + supportsSystemMessage: 'system-role', + supportsTools: 'anthropic-style', + }, + "claude-3-sonnet-20240229": { + contextWindow: 200_000, cost: { input: 3.00, output: 15.00 }, + supportsSystemMessage: 'system-role', + supportsTools: 'anthropic-style', + } + } +} + + + +const grokProviderSettings: ProviderSettings = { + thinkingFormat: '', + + toolsFormat: '', + + FIMFormat: '', + + modelOptions: { + "claude-3-5-sonnet-20241022": { + contextWindow: 200_000, + cost: { input: 3.00, cache_read: 0.30, cache_write: 3.75, output: 15.00 }, + supportsSystemMessage: 'system-role', + supportsTools: 'anthropic-style', + + }, + "claude-3-5-haiku-20241022": { + contextWindow: 200_000, + cost: { input: 0.80, cache_read: 0.08, cache_write: 1.00, output: 4.00 }, + supportsSystemMessage: 'system-role', + supportsTools: 'anthropic-style', + }, + "claude-3-opus-20240229": { + contextWindow: 200_000, + cost: { input: 15.00, cache_read: 1.50, cache_write: 18.75, output: 75.00 }, + supportsSystemMessage: 'system-role', + supportsTools: 'anthropic-style', + }, + "claude-3-sonnet-20240229": { + contextWindow: 200_000, cost: { input: 3.00, output: 15.00 }, + supportsSystemMessage: 'system-role', + supportsTools: 'anthropic-style', + } + } + +} + + + + +// helpers + +const toolNamesSet = new Set(toolNames) +const isAToolName = (toolName: string): toolName is ToolName => { + const isAToolName = toolNamesSet.has(toolName) + return isAToolName +} + + +// ------------ 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, params: string, id: string } } + +const toolCallsFrom_OpenAICompat = (toolCallOfIndex: ToolCallOfIndex) => { + return Object.keys(toolCallOfIndex).map(index => { + const tool = toolCallOfIndex[index] + return isAToolName(tool.name) ? { name: tool.name, id: tool.id, params: tool.params } : null + }).filter(t => !!t) +} + + +const newOpenAICompatibleSDK = ({ settingsOfProvider, providerName }: { settingsOfProvider: SettingsOfProvider, providerName: ProviderName }) => { + if (providerName === 'openAI') { + const thisConfig = settingsOfProvider[providerName] + return new OpenAI({ apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, }) + } + else if (providerName === 'ollama') { + const thisConfig = settingsOfProvider[providerName] + return new OpenAI({ baseURL: `${thisConfig.endpoint}/v1`, apiKey: 'noop', dangerouslyAllowBrowser: true, }) + } + else if (providerName === 'vLLM') { + const thisConfig = settingsOfProvider[providerName] + return new OpenAI({ baseURL: `${thisConfig.endpoint}/v1`, apiKey: 'noop', dangerouslyAllowBrowser: true, }) + } + else throw new Error(`Invalid providerName ${providerName}`) +} + +export const _sendOpenAICompatibleChat: _InternalSendLLMChatMessageFnType = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions, tools: tools_ }) => { + const { messages } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage: '', supportsTools: '', }) + const tools = (supportsTools && ((tools_?.length ?? 0) !== 0)) ? tools_?.map(tool => toOpenAICompatibleTool(tool)) : undefined + + const toolsObj = tools ? { tools: tools, tool_choice: 'auto', parallel_tool_calls: false, } as const : {} + const openai: OpenAI = newOpenAICompatibleSDK({ providerName, settingsOfProvider }) + const options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = { model: modelName, messages: messages, stream: true, ...toolsObj } + + let fullText = '' + 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: '', params: '', id: '' } + toolCallOfIndex[index].name += tool.function?.name ?? '' + toolCallOfIndex[index].params += tool.function?.arguments ?? ''; + toolCallOfIndex[index].id = tool.id ?? '' + } + // message + let newText = '' + newText += chunk.choices[0]?.delta?.content ?? '' + fullText += newText + onText({ newText, fullText }) + } + onFinalMessage({ fullText, toolCalls: toolCallsFrom_OpenAICompat(toolCallOfIndex) }); + }) + // 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: 'Invalid API key.', fullError: error }); } + else { onError({ message: error + '', fullError: error }); } + }) +} + + +export const _openaiCompatibleList: _InternalModelListFnType = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider, providerName }) => { + 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 + '' }) + } +} + + +// ------------ OPENAI ------------ +export const sendOpenAIChat: _InternalSendLLMChatMessageFnType = (params) => { + return _sendOpenAICompatibleChat(params) +} + +// ------------ 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 toolCallsFromAnthropicContent = (content: Anthropic.Messages.ContentBlock[]) => { + 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, params: JSON.stringify(c.input), id: c.id } : null + }).filter(t => !!t) +} + +export const sendAnthropicChat: _InternalSendLLMChatMessageFnType = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, aiInstructions, tools: tools_ }) => { + const { messages, separateSystemMessageStr } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage: 'separated', supportsTools: 'anthropic-style', }) + + 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 maxTokens = ; + const stream = anthropic.messages.stream({ + system: separateSystemMessageStr, + messages: messages, + model: modelName, + max_tokens: maxTokens, + tools: tools, + tool_choice: tools ? { type: 'auto', disable_parallel_tool_use: true } : undefined // one tool use at a time + }) + // when receive text + stream.on('text', (newText, fullText) => { + onText({ newText, fullText }) + }) + // when we get the final message on this stream (or when error/fail) + stream.on('finalMessage', (response) => { + const content = response.content.map(c => c.type === 'text' ? c.text : '').join('\n\n') + const toolCalls = toolCallsFromAnthropicContent(response.content) + onFinalMessage({ fullText: content, toolCalls }) + }) + // on error + stream.on('error', (error) => { + if (error instanceof Anthropic.APIError && error.status === 401) { onError({ message: 'Invalid API key.', 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 +} + +export const ollamaList: _InternalModelListFnType = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider }) => { + 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 + '' }) + } +} + +export const sendOllamaFIM: _InternalSendLLMFIMMessageFnType = ({ messages, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => { + const thisConfig = settingsOfProvider.ollama + const ollama = newOllamaSDK({ endpoint: thisConfig.endpoint }) + + let fullText = '' + ollama.generate({ + model: modelName, + prompt: messages.prefix, + suffix: messages.suffix, + options: { + stop: messages.stopTokens, + num_predict: 300, // 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 }) + }) +} + + +// ollama's implementation of openai-compatible SDK dumps all reasoning tokens out with message, and supports tools, so we can use it for chat! +export const sendOllamaMessage: _InternalSendLLMChatMessageFnType = (params) => { + return _sendOpenAICompatibleChat(params) + // TODO!!! filter out reasoning tags... +} + + + +// ------------ OPENROUTER ------------ +export const sendOpenRouterFIM: _InternalSendLLMFIMMessageFnType = ({ messages, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => { + // TODO!!! +} + +export const sendOpenRouterChat: _InternalSendLLMChatMessageFnType = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions, tools: tools_ }) => { + // payload should have {include_reasoning: true} https://openrouter.ai/announcements/reasoning-tokens-for-thinking-models + // response.choices[0].delta.reasoning +} + +// ------------ OPENAI-COMPATIBLE ------------ +export const openAICompatibleList: _InternalModelListFnType = async (params) => { + return _openaiCompatibleList(params) +} + +// TODO!!! FIM + +// using openai's SDK is not ideal (your implementation might not do tools, reasoning, FIM etc correctly), talk to us for a custom integration +export const sendOpenAICompatibleChat: _InternalSendLLMChatMessageFnType = (params) => { + return _sendOpenAICompatibleChat(params) +} + +// ------------ VLLM ------------ + +// TODO!!! FIM + +// using openai's SDK is not ideal (your implementation might not do tools, reasoning, FIM etc correctly), talk to us for a custom integration +export const sendVLLMChat: _InternalSendLLMChatMessageFnType = (params) => { + return _sendOpenAICompatibleChat(params) + // response.choices[0].delta.reasoning_content // https://docs.vllm.ai/en/stable/features/reasoning_outputs.html#streaming-chat-completions +} + + + + + + + diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/_old.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/_old.ts index e1e90245..8aa80bf4 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/_old.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/_old.ts @@ -94,3 +94,390 @@ + + + + + + + + + + + + + + + + + + + +// export const recognizedModels = [ +// // chat +// 'OpenAI 4o', +// 'Anthropic Claude', +// 'Llama 3.x', +// 'Deepseek Chat', // deepseek coder v2 is now merged into chat (V3) https://api-docs.deepseek.com/updates#deepseek-coder--deepseek-chat-upgraded-to-deepseek-v25-model +// 'xAI Grok', +// // 'xAI Grok', +// // 'Google Gemini, Gemma', +// // 'Microsoft Phi4', + + +// // coding (autocomplete) +// 'Alibaba Qwen2.5 Coder Instruct', // we recommend this over Qwen2.5 +// 'Mistral Codestral', + +// // thinking +// 'OpenAI o1', +// 'Deepseek R1', + +// // general +// // 'Mixtral 8x7b' +// // 'Qwen2.5', + +// ] as const + +// type RecognizedModelName = (typeof recognizedModels)[number] | '' + + +// export function recognizedModelOfModelName(modelName: string): RecognizedModelName { +// const lower = modelName.toLowerCase(); + +// if (lower.includes('gpt-4o')) +// return 'OpenAI 4o'; +// if (lower.includes('claude')) +// return 'Anthropic Claude'; +// if (lower.includes('llama')) +// return 'Llama 3.x'; +// if (lower.includes('qwen2.5-coder')) +// return 'Alibaba Qwen2.5 Coder Instruct'; +// if (lower.includes('mistral')) +// return 'Mistral Codestral'; +// if (/\bo1\b/.test(lower) || /\bo3\b/.test(lower)) // o1, o3 +// return 'OpenAI o1'; +// if (lower.includes('deepseek-r1') || lower.includes('deepseek-reasoner')) +// return 'Deepseek R1'; +// if (lower.includes('deepseek')) +// return 'Deepseek Chat' +// if (lower.includes('grok')) +// return 'xAI Grok' + +// return ''; +// } + + +// const developerInfoAtProvider: { [providerName in ProviderName]: DeveloperInfoAtProvider } = { +// 'anthropic': { +// overrideSettingsForAllModels: { +// supportsSystemMessage: true, +// supportsTools: true, +// _supportsAutocompleteFIM: false, +// _supportsStreaming: true, +// } +// }, +// 'deepseek': { +// overrideSettingsForAllModels: { +// } +// }, +// 'ollama': { +// }, +// 'openRouter': { +// }, +// 'openAICompatible': { +// }, +// 'openAI': { +// }, +// 'gemini': { +// }, +// 'mistral': { +// }, +// 'groq': { +// }, +// 'xAI': { +// }, +// 'vLLM': { +// }, +// } +// export const developerInfoOfProviderName = (providerName: ProviderName): Partial => { +// return developerInfoAtProvider[providerName] ?? {} +// } + + + + +// // providerName is optional, but gives some extra fallbacks if provided +// const developerInfoOfRecognizedModelName: { [recognizedModel in RecognizedModelName]: Omit } = { +// 'OpenAI 4o': { +// supportsSystemMessage: true, +// supportsTools: true, +// _supportsAutocompleteFIM: false, +// _supportsStreaming: true, +// _maxTokens: 4096, +// }, + +// 'Anthropic Claude': { +// supportsSystemMessage: true, +// supportsTools: false, +// _supportsAutocompleteFIM: false, +// _supportsStreaming: false, +// _maxTokens: 4096, +// }, + +// 'Llama 3.x': { +// supportsSystemMessage: true, +// supportsTools: true, +// _supportsAutocompleteFIM: false, +// _supportsStreaming: false, +// _maxTokens: 4096, +// }, + +// 'xAI Grok': { +// supportsSystemMessage: true, +// supportsTools: true, +// _supportsAutocompleteFIM: false, +// _supportsStreaming: true, +// _maxTokens: 4096, + +// }, + +// 'Deepseek Chat': { +// supportsSystemMessage: true, +// supportsTools: false, +// _supportsAutocompleteFIM: false, +// _supportsStreaming: false, +// _maxTokens: 4096, +// }, + +// 'Alibaba Qwen2.5 Coder Instruct': { +// supportsSystemMessage: true, +// supportsTools: true, +// _supportsAutocompleteFIM: false, +// _supportsStreaming: false, +// _maxTokens: 4096, +// }, + +// 'Mistral Codestral': { +// supportsSystemMessage: true, +// supportsTools: true, +// _supportsAutocompleteFIM: false, +// _supportsStreaming: false, +// _maxTokens: 4096, +// }, + +// 'OpenAI o1': { +// supportsSystemMessage: 'developer', +// supportsTools: false, +// _supportsAutocompleteFIM: false, +// _supportsStreaming: true, +// _maxTokens: 4096, +// }, + +// 'Deepseek R1': { +// supportsSystemMessage: false, +// supportsTools: false, +// _supportsAutocompleteFIM: false, +// _supportsStreaming: false, +// _maxTokens: 4096, +// }, + + +// '': { +// supportsSystemMessage: false, +// supportsTools: false, +// _supportsAutocompleteFIM: false, +// _supportsStreaming: false, +// _maxTokens: 4096, +// }, +// } +// export const developerInfoOfModelName = (modelName: string, overrides?: Partial): DeveloperInfoAtModel => { +// const recognizedModelName = recognizedModelOfModelName(modelName) +// return { +// _recognizedModelName: recognizedModelName, +// ...developerInfoOfRecognizedModelName[recognizedModelName], +// ...overrides +// } +// } + + + + + + +// // creates `modelInfo` from `modelNames` + + + + + +// export const modelInfoOfAutodetectedModelNames = (defaultModelNames: string[], options: { existingModels: VoidModelInfo[] }) => { +// const { existingModels } = options + +// const existingModelsMap: Record = {} +// for (const existingModel of existingModels) { +// existingModelsMap[existingModel.modelName] = existingModel +// } + +// return defaultModelNames.map((modelName, i) => ({ +// modelName, +// isDefault: true, +// isAutodetected: true, +// isHidden: !!existingModelsMap[modelName]?.isHidden, +// ...developerInfoOfModelName(modelName) +// })) +// } + + + + + + +// export const anthropicMaxPossibleTokens = (modelName: string) => { +// if (modelName === 'claude-3-5-sonnet-20241022' +// || modelName === 'claude-3-5-haiku-20241022') +// return 8192 +// if (modelName === 'claude-3-opus-20240229' +// || modelName === 'claude-3-sonnet-20240229' +// || modelName === 'claude-3-haiku-20240307') +// return 4096 +// return 1024 // return a reasonably small number if they're using a different model +// } + + + + + + + + + + + + + + + + + + + +// // Ollama chat +// export const sendOllamaChat: _InternalSendLLMChatMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => { + +// const thisConfig = settingsOfProvider.ollama +// // if endpoint is empty, normally ollama will send to 11434, but we want it to fail - the user should type it in +// if (!thisConfig.endpoint) throw new Error(`Ollama Endpoint was empty (please enter ${defaultProviderSettings.ollama.endpoint} if you want the default).`) + +// let fullText = '' + +// const ollama = new Ollama({ host: thisConfig.endpoint }) + +// ollama.chat({ +// model: modelName, +// messages: messages, +// stream: true, +// // options: { num_predict: parseMaxTokensStr(thisConfig.maxTokens) } // this is max_tokens +// }) +// .then(async stream => { +// _setAborter(() => stream.abort()) +// // iterate through the stream +// for await (const chunk of stream) { +// const newText = chunk.message.content; + +// // chunk.message.tool_calls[0].function.arguments + +// fullText += newText; +// onText({ newText, fullText }); +// } + +// onFinalMessage({ fullText, tools: [] }); + +// }) +// // when error/fail +// .catch((error) => { +// onError({ message: error + '', fullError: error }) +// }) + +// }; + + + + + + + +// type NewParams = Pick[0] & Parameters<_InternalSendLLMFIMMessageFnType>[0], 'settingsOfProvider' | 'providerName'> +// const newOpenAI = ({ settingsOfProvider, providerName }: NewParams) => { + +// if (providerName === 'openAI') { +// const thisConfig = settingsOfProvider[providerName] +// return new OpenAI({ +// apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true +// }) +// } +// else if (providerName === 'ollama') { +// const thisConfig = settingsOfProvider[providerName] +// return new OpenAI({ +// baseURL: `${thisConfig.endpoint}/v1`, apiKey: 'noop', dangerouslyAllowBrowser: true, +// }) +// } +// else if (providerName === 'vLLM') { +// const thisConfig = settingsOfProvider[providerName] +// return new OpenAI({ +// baseURL: `${thisConfig.endpoint}/v1`, apiKey: 'noop', dangerouslyAllowBrowser: true, +// }) +// } +// else if (providerName === 'openRouter') { +// const thisConfig = settingsOfProvider[providerName] +// return new OpenAI({ +// baseURL: 'https://openrouter.ai/api/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, +// defaultHeaders: { +// 'HTTP-Referer': 'https://voideditor.com', // Optional, for including your app on openrouter.ai rankings. +// 'X-Title': 'Void Editor', // Optional. Shows in rankings on openrouter.ai. +// }, +// }) +// } +// else if (providerName === 'gemini') { +// const thisConfig = settingsOfProvider[providerName] +// return new OpenAI({ +// baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, +// }) +// } +// else if (providerName === 'deepseek') { +// const thisConfig = settingsOfProvider[providerName] +// return new OpenAI({ +// baseURL: 'https://api.deepseek.com/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, +// }) +// } +// else if (providerName === 'openAICompatible') { +// const thisConfig = settingsOfProvider[providerName] +// return new OpenAI({ +// baseURL: thisConfig.endpoint, apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, +// }) +// } +// else if (providerName === 'mistral') { +// const thisConfig = settingsOfProvider[providerName] +// return new OpenAI({ +// baseURL: 'https://api.mistral.ai/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, +// }) +// } +// else if (providerName === 'groq') { +// const thisConfig = settingsOfProvider[providerName] +// return new OpenAI({ +// baseURL: 'https://api.groq.com/openai/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, +// }) +// } +// else if (providerName === 'xAI') { +// const thisConfig = settingsOfProvider[providerName] +// return new OpenAI({ +// baseURL: 'https://api.x.ai/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, +// }) +// } +// else { +// console.error(`sendOpenAICompatibleMsg: invalid providerName: ${providerName}`) +// throw new Error(`Void providerName was invalid: ${providerName}`) +// } +// } + + diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts deleted file mode 100644 index c4338ebb..00000000 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/anthropic.ts +++ /dev/null @@ -1,114 +0,0 @@ -/*-------------------------------------------------------------------------------------- - * Copyright 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. - *--------------------------------------------------------------------------------------*/ - -import Anthropic from '@anthropic-ai/sdk'; -import { _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes.js'; -import { anthropicMaxPossibleTokens, developerInfoOfModelName, developerInfoOfProviderName } from '../../common/voidSettingsTypes.js'; -import { InternalToolInfo } from '../../common/toolsService.js'; -import { addSystemMessageAndToolSupport } from './preprocessLLMMessages.js'; -import { isAToolName } from './postprocessToolCalls.js'; - - - - -export 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 -} - - - - - -export const sendAnthropicChat: _InternalSendLLMChatMessageFnType = ({ messages: messages_, providerName, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, aiInstructions, tools: tools_ }) => { - - const thisConfig = settingsOfProvider.anthropic - - const maxTokens = anthropicMaxPossibleTokens(modelName) - if (maxTokens === undefined) { - onError({ message: `Please set a value for Max Tokens.`, fullError: null }) - return - } - - const { messages, separateSystemMessageStr } = addSystemMessageAndToolSupport(modelName, providerName, messages_, aiInstructions, { separateSystemMessage: true }) - - const { overrideSettingsForAllModels } = developerInfoOfProviderName(providerName) - const { supportsTools } = developerInfoOfModelName(modelName, overrideSettingsForAllModels) - - const anthropic = new Anthropic({ apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true }); - - const tools = (supportsTools && ((tools_?.length ?? 0) !== 0)) ? tools_?.map(tool => toAnthropicTool(tool)) : undefined - - const stream = anthropic.messages.stream({ - system: separateSystemMessageStr, - messages: messages, - model: modelName, - max_tokens: maxTokens, - tools: tools, - tool_choice: tools ? { type: 'auto', disable_parallel_tool_use: true } : undefined // one tool use at a time - }) - - - // when receive text - stream.on('text', (newText, fullText) => { - onText({ newText, fullText }) - }) - - - // // can do tool use 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 - // } - // // TODO!!!!! - // // onText({}) - // }) - - // when we get the final message on this stream (or when error/fail) - stream.on('finalMessage', (response) => { - // stringify the response's content - const content = response.content.map(c => c.type === 'text' ? c.text : '').join('\n\n') - const toolCalls = response.content - .map(c => { - if (c.type !== 'tool_use') return null - if (!isAToolName(c.name)) return null - return c.type === 'tool_use' ? { name: c.name, params: JSON.stringify(c.input), id: c.id } : null - }) - .filter(t => !!t) - - onFinalMessage({ fullText: content, toolCalls }) - }) - - stream.on('error', (error) => { - // the most common error will be invalid API key (401), so we handle this with a nice message - if (error instanceof Anthropic.APIError && error.status === 401) { - onError({ message: 'Invalid API key.', fullError: error }) - } - else { - onError({ message: error + '', fullError: error }) // anthropic errors can be stringified nicely like this - } - }) - - // TODO need to test this to make sure it works, it might throw an error - _setAborter(() => stream.controller.abort()) - -}; diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/ollama.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/ollama.ts deleted file mode 100644 index da6715c0..00000000 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/ollama.ts +++ /dev/null @@ -1,124 +0,0 @@ - -/*-------------------------------------------------------------------------------------- - * Copyright 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. - *--------------------------------------------------------------------------------------*/ - -import { Ollama } from 'ollama'; -import { _InternalModelListFnType, _InternalSendLLMFIMMessageFnType, _InternalSendLLMChatMessageFnType, OllamaModelResponse } from '../../common/llmMessageTypes.js'; -import { defaultProviderSettings } from '../../common/voidSettingsTypes.js'; - -export const ollamaList: _InternalModelListFnType = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider }) => { - - const onSuccess = ({ models }: { models: OllamaModelResponse[] }) => { - onSuccess_({ models }) - } - - const onError = ({ error }: { error: string }) => { - onError_({ error }) - } - - try { - const thisConfig = settingsOfProvider.ollama - // if endpoint is empty, normally ollama will send to 11434, but we want it to fail - the user should type it in - if (!thisConfig.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: thisConfig.endpoint }) - ollama.list() - .then((response) => { - const { models } = response - onSuccess({ models }) - }) - .catch((error) => { - onError({ error: error + '' }) - }) - } - catch (error) { - onError({ error: error + '' }) - } -} - - - - -// export const sendOllamaFIM: _InternalSendLLMFIMMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => { - -// const thisConfig = settingsOfProvider.ollama -// // if endpoint is empty, normally ollama will send to 11434, but we want it to fail - the user should type it in -// if (!thisConfig.endpoint) throw new Error(`Ollama Endpoint was empty (please enter ${defaultProviderSettings.ollama.endpoint} if you want the default).`) - -// let fullText = '' - -// const ollama = new Ollama({ host: thisConfig.endpoint }) - -// ollama.generate({ -// model: modelName, -// prompt: messages.prefix, -// suffix: messages.suffix, -// options: { -// stop: messages.stopTokens, -// num_predict: 300, // max tokens -// // repeat_penalty: 1, -// }, -// raw: true, -// stream: true, -// }) -// .then(async stream => { -// _setAborter(() => stream.abort()) -// // iterate through the stream -// for await (const chunk of stream) { -// const newText = chunk.response; -// fullText += newText; -// onText({ newText, fullText }); -// } -// onFinalMessage({ fullText, tools: [] }); -// }) -// // when error/fail -// .catch((error) => { -// onError({ message: error + '', fullError: error }) -// }) -// }; - - -// // Ollama -// export const sendOllamaChat: _InternalSendLLMChatMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => { - -// const thisConfig = settingsOfProvider.ollama -// // if endpoint is empty, normally ollama will send to 11434, but we want it to fail - the user should type it in -// if (!thisConfig.endpoint) throw new Error(`Ollama Endpoint was empty (please enter ${defaultProviderSettings.ollama.endpoint} if you want the default).`) - -// let fullText = '' - -// const ollama = new Ollama({ host: thisConfig.endpoint }) - -// ollama.chat({ -// model: modelName, -// messages: messages, -// stream: true, -// // options: { num_predict: parseMaxTokensStr(thisConfig.maxTokens) } // this is max_tokens -// }) -// .then(async stream => { -// _setAborter(() => stream.abort()) -// // iterate through the stream -// for await (const chunk of stream) { -// const newText = chunk.message.content; - -// // chunk.message.tool_calls[0].function.arguments - -// fullText += newText; -// onText({ newText, fullText }); -// } - -// onFinalMessage({ fullText, tools: [] }); - -// }) -// // when error/fail -// .catch((error) => { -// onError({ message: error + '', fullError: error }) -// }) - -// }; - - - -// // ['codestral', 'qwen2.5-coder', 'qwen2.5-coder:0.5b', 'qwen2.5-coder:1.5b', 'qwen2.5-coder:3b', 'qwen2.5-coder:7b', 'qwen2.5-coder:14b', 'qwen2.5-coder:32b', 'codegemma', 'codegemma:2b', 'codegemma:7b', 'codellama', 'codellama:7b', 'codellama:13b', 'codellama:34b', 'codellama:70b', 'codellama:code', 'codellama:python', 'command-r', 'command-r:35b', 'command-r-plus', 'command-r-plus:104b', 'deepseek-coder-v2', 'deepseek-coder-v2:16b', 'deepseek-coder-v2:236b', 'falcon2', 'falcon2:11b', 'firefunction-v2', 'firefunction-v2:70b', 'gemma', 'gemma:2b', 'gemma:7b', 'gemma2', 'gemma2:2b', 'gemma2:9b', 'gemma2:27b', 'llama2', 'llama2:7b', 'llama2:13b', 'llama2:70b', 'llama3', 'llama3:8b', 'llama3:70b', 'llama3-chatqa', 'llama3-chatqa:8b', 'llama3-chatqa:70b', 'llama3-gradient', 'llama3-gradient:8b', 'llama3-gradient:70b', 'llama3.1', 'llama3.1:8b', 'llama3.1:70b', 'llama3.1:405b', 'llava', 'llava:7b', 'llava:13b', 'llava:34b', 'llava-llama3', 'llava-llama3:8b', 'llava-phi3', 'llava-phi3:3.8b', 'mistral', 'mistral:7b', 'mistral-large', 'mistral-large:123b', 'mistral-nemo', 'mistral-nemo:12b', 'mixtral', 'mixtral:8x7b', 'mixtral:8x22b', 'moondream', 'moondream:1.8b', 'openhermes', 'openhermes:v2.5', 'phi3', 'phi3:3.8b', 'phi3:14b', 'phi3.5', 'phi3.5:3.8b', 'qwen', 'qwen:7b', 'qwen:14b', 'qwen:32b', 'qwen:72b', 'qwen:110b', 'qwen2', 'qwen2:0.5b', 'qwen2:1.5b', 'qwen2:7b', 'qwen2:72b', 'smollm', 'smollm:135m', 'smollm:360m', 'smollm:1.7b',] diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts deleted file mode 100644 index 7769a983..00000000 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/openai.ts +++ /dev/null @@ -1,231 +0,0 @@ -/*-------------------------------------------------------------------------------------- - * Copyright 2025 Glass Devtools, Inc. All rights reserved. - * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. - *--------------------------------------------------------------------------------------*/ - -import OpenAI from 'openai'; -import { _InternalModelListFnType, _InternalSendLLMFIMMessageFnType, _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes.js'; -import { Model } from 'openai/resources/models.js'; -import { InternalToolInfo } from '../../common/toolsService.js'; -import { addSystemMessageAndToolSupport } from './preprocessLLMMessages.js'; -import { developerInfoOfModelName, developerInfoOfProviderName } from '../../common/voidSettingsTypes.js'; -import { isAToolName } from './postprocessToolCalls.js'; - - -// developer command - https://cdn.openai.com/spec/model-spec-2024-05-08.html#follow-the-chain-of-command -// prompting - https://platform.openai.com/docs/guides/reasoning#advice-on-prompting - -// npm i @openrouter/ai-sdk-provider ai ollama-ai-provider - -export const toOpenAITool = (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 NewParams = Pick[0] & Parameters<_InternalSendLLMFIMMessageFnType>[0], 'settingsOfProvider' | 'providerName'> -const newOpenAI = ({ settingsOfProvider, providerName }: NewParams) => { - - if (providerName === 'openAI') { - const thisConfig = settingsOfProvider[providerName] - return new OpenAI({ - apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true - }) - } - else if (providerName === 'ollama') { - const thisConfig = settingsOfProvider[providerName] - return new OpenAI({ - baseURL: `${thisConfig.endpoint}/v1`, apiKey: 'noop', dangerouslyAllowBrowser: true, - }) - } - else if (providerName === 'vLLM') { - const thisConfig = settingsOfProvider[providerName] - return new OpenAI({ - baseURL: `${thisConfig.endpoint}/v1`, apiKey: 'noop', dangerouslyAllowBrowser: true, - }) - } - else if (providerName === 'openRouter') { - const thisConfig = settingsOfProvider[providerName] - return new OpenAI({ - baseURL: 'https://openrouter.ai/api/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, - defaultHeaders: { - 'HTTP-Referer': 'https://voideditor.com', // Optional, for including your app on openrouter.ai rankings. - 'X-Title': 'Void Editor', // Optional. Shows in rankings on openrouter.ai. - }, - }) - } - else if (providerName === 'gemini') { - const thisConfig = settingsOfProvider[providerName] - return new OpenAI({ - baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, - }) - } - else if (providerName === 'deepseek') { - const thisConfig = settingsOfProvider[providerName] - return new OpenAI({ - baseURL: 'https://api.deepseek.com/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, - }) - } - else if (providerName === 'openAICompatible') { - const thisConfig = settingsOfProvider[providerName] - return new OpenAI({ - baseURL: thisConfig.endpoint, apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, - }) - } - else if (providerName === 'mistral') { - const thisConfig = settingsOfProvider[providerName] - return new OpenAI({ - baseURL: 'https://api.mistral.ai/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, - }) - } - else if (providerName === 'groq') { - const thisConfig = settingsOfProvider[providerName] - return new OpenAI({ - baseURL: 'https://api.groq.com/openai/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, - }) - } - else if (providerName === 'xAI') { - const thisConfig = settingsOfProvider[providerName] - return new OpenAI({ - baseURL: 'https://api.x.ai/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, - }) - } - else { - console.error(`sendOpenAICompatibleMsg: invalid providerName: ${providerName}`) - throw new Error(`Void providerName was invalid: ${providerName}`) - } -} - - - -// might not currently be used in the code -export const openaiCompatibleList: _InternalModelListFnType = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider }) => { - const onSuccess = ({ models }: { models: Model[] }) => { - onSuccess_({ models }) - } - - const onError = ({ error }: { error: string }) => { - onError_({ error }) - } - - try { - const openai = newOpenAI({ providerName: 'openAICompatible', settingsOfProvider }) - - openai.models.list() - .then(async (response) => { - const models: Model[] = [] - 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 + '' }) - } -} - - - - -export const sendOpenAIFIM: _InternalSendLLMFIMMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }) => { - - - // openai.completions has a FIM parameter called `suffix`, but it's deprecated and only works for ~GPT 3 era models - - - -} - - - -// OpenAI, OpenRouter, OpenAICompatible -export const sendOpenAIChat: _InternalSendLLMChatMessageFnType = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions, tools: tools_ }) => { - - let fullText = '' - const toolCallOfIndex: { [index: string]: { name: string, params: string, id: string } } = {} - - const { overrideSettingsForAllModels } = developerInfoOfProviderName(providerName) - const { supportsTools } = developerInfoOfModelName(modelName, overrideSettingsForAllModels) - - const { messages } = addSystemMessageAndToolSupport(modelName, providerName, messages_, aiInstructions, { separateSystemMessage: false }) - - const tools = (supportsTools && ((tools_?.length ?? 0) !== 0)) ? tools_?.map(tool => toOpenAITool(tool)) : undefined - - const openai: OpenAI = newOpenAI({ providerName, settingsOfProvider }) - const options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = { - model: modelName, - messages: messages, - stream: true, - tools: tools, - tool_choice: tools ? 'auto' : undefined, - parallel_tool_calls: tools ? false : undefined, - } - - 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: '', params: '', id: '' } - toolCallOfIndex[index].name += tool.function?.name ?? '' - toolCallOfIndex[index].params += tool.function?.arguments ?? ''; - toolCallOfIndex[index].id = tool.id ?? '' - - } - - // message - let newText = '' - newText += chunk.choices[0]?.delta?.content ?? '' - console.log('!!!!', JSON.stringify(chunk, null, 2)) - fullText += newText; - - onText({ newText, fullText }); - } - onFinalMessage({ - fullText, - toolCalls: Object.keys(toolCallOfIndex) - .map(index => { - const tool = toolCallOfIndex[index] - if (isAToolName(tool.name)) - return { name: tool.name, id: tool.id, params: tool.params } - return null - }) - .filter(t => !!t) - }); - }) - // 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: 'Invalid API key.', fullError: error }); - } - else { - onError({ message: error + '', fullError: error }); - } - }) - -} diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/postprocessToolCalls.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/postprocessToolCalls.ts deleted file mode 100644 index aee52dcb..00000000 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/postprocessToolCalls.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { ToolName, toolNames } from '../../common/toolsService.js'; - -const toolNamesSet = new Set(toolNames) - -export const isAToolName = (toolName: string): toolName is ToolName => { - const isAToolName = toolNamesSet.has(toolName) - return isAToolName -} diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts index 689e44de..3cef5327 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts @@ -1,7 +1,6 @@ import { LLMChatMessage } from '../../common/llmMessageTypes.js'; -import { developerInfoOfModelName, developerInfoOfProviderName, ProviderName } from '../../common/voidSettingsTypes.js'; import { deepClone } from '../../../../../base/common/objects.js'; @@ -14,16 +13,24 @@ export const parseObject = (args: unknown) => { return {} } -// no matter whether the model supports a system message or not (or what format it supports), add it in some way -// also take into account tools if the model doesn't support tool use -export const addSystemMessageAndToolSupport = (modelName: string, providerName: ProviderName, messages_: LLMChatMessage[], aiInstructions: string, { separateSystemMessage }: { separateSystemMessage: boolean }): { separateSystemMessageStr?: string, messages: any[] } => { +const prepareMessages_cloneAndTrim = ({ messages: messages_ }: { messages: LLMChatMessage[] }) => { const messages = deepClone(messages_).map(m => ({ ...m, content: m.content.trim(), })) + return { messages } +} - const { overrideSettingsForAllModels } = developerInfoOfProviderName(providerName) - const { supportsSystemMessage, supportsTools } = developerInfoOfModelName(modelName, overrideSettingsForAllModels) +// 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[], + aiInstructions: string, + supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated', +}) + : { separateSystemMessageStr?: string, messages: any[] } => { - // 1. SYSTEM MESSAGE // find system messages and concatenate them let systemMessageStr = messages .filter(msg => msg.role === 'system') @@ -33,7 +40,6 @@ export const addSystemMessageAndToolSupport = (modelName: string, providerName: if (aiInstructions) systemMessageStr = `${(systemMessageStr ? `${systemMessageStr}\n\n` : '')}GUIDELINES\n${aiInstructions}` - let separateSystemMessageStr: string | undefined = undefined // remove all system messages @@ -49,11 +55,12 @@ export const addSystemMessageAndToolSupport = (modelName: string, providerName: if (systemMessageStr) { // if supports system message if (supportsSystemMessage) { - if (separateSystemMessage) + if (supportsSystemMessage === 'separated') separateSystemMessageStr = systemMessageStr - else { - newMessages.unshift({ role: supportsSystemMessage === 'developer' ? 'developer' : 'system', content: systemMessageStr }) // add new first message - } + else if (supportsSystemMessage === 'system-role') + newMessages.unshift({ role: 'system', content: systemMessageStr }) // add new first message + else if (supportsSystemMessage === 'developer-role') + newMessages.unshift({ role: 'developer', content: systemMessageStr }) // add new first message } // if does not support system message else { @@ -79,181 +86,179 @@ export const addSystemMessageAndToolSupport = (modelName: string, providerName: } } + return { messages: newMessages, separateSystemMessageStr } +} - // 2. MAKE TOOLS FORMAT CORRECT in messages - let finalMessages: any[] - if (!supportsTools) { - // do nothing - finalMessages = newMessages - } +// convert messages as if about to send to openai +/* +reference - https://platform.openai.com/docs/guides/function-calling#function-calling-steps +openai MESSAGE (role=assistant): +"tool_calls":[{ + "type": "function", + "id": "call_12345xyz", + "function": { + "name": "get_weather", + "arguments": "{\"latitude\":48.8566,\"longitude\":2.3522}" +}] - // anthropic assistant message will have: https://docs.anthropic.com/en/docs/build-with-claude/tool-use#tool-use-examples - // "content": [ - // { - // "type": "text", - // "text": "I need to call the get_weather function, and the user wants SF, which is likely San Francisco, CA." - // }, - // { - // "type": "tool_use", - // "id": "toolu_01A09q90qw90lq917835lq9", - // "name": "get_weather", - // "input": { "location": "San Francisco, CA", "unit": "celsius" } - // } - // ] +openai RESPONSE (role=user): +{ "role": "tool", + "tool_call_id": tool_call.id, + "content": str(result) } - // anthropic user message response will be: - // "content": [ - // { - // "type": "tool_result", - // "tool_use_id": "toolu_01A09q90qw90lq917835lq9", - // "content": "15 degrees" - // } - // ] +also see +openai on prompting - https://platform.openai.com/docs/guides/reasoning#advice-on-prompting +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[], }) => { - else if (providerName === 'anthropic') { // convert role:'tool' to anthropic's type - const newMessagesTools: ( - Exclude | { - role: 'assistant', - content: string | ({ - type: 'text'; - text: string; - } | { - type: 'tool_use'; + const newMessages: ( + Exclude | { + role: 'assistant', + content: string; + tool_calls?: { + type: 'function'; + id: string; + function: { name: string; - input: Record; - id: string; - })[] - } | { - role: 'user', - content: string | ({ - type: 'text'; - text: string; - } | { - type: 'tool_result'; - tool_use_id: string; - content: string; - })[] - } - )[] = newMessages; + arguments: string; + } + }[] + } | { + role: 'tool', + id: string; // old val + tool_call_id: string; // new val + content: string; + } + )[] = []; + for (let i = 0; i < messages.length; i += 1) { + const currMsg = messages[i] - for (let i = 0; i < newMessagesTools.length; i += 1) { - const currMsg = newMessagesTools[i] - - if (currMsg.role !== 'tool') continue - - const prevMsg = 0 <= i - 1 && i - 1 <= newMessagesTools.length ? newMessagesTools[i - 1] : undefined - - if (prevMsg?.role === 'assistant') { - if (typeof prevMsg.content === 'string') prevMsg.content = [{ type: 'text', text: prevMsg.content }] - prevMsg.content.push({ type: 'tool_use', id: currMsg.id, name: currMsg.name, input: parseObject(currMsg.params) }) - } - - // turn each tool into a user message with tool results at the end - newMessagesTools[i] = { - role: 'user', - content: [ - ...[{ type: 'tool_result', tool_use_id: currMsg.id, content: currMsg.content }] as const, - ...currMsg.content ? [{ type: 'text', text: currMsg.content }] as const : [], - ] - } + if (currMsg.role !== 'tool') { + newMessages.push(currMsg) + continue } - finalMessages = newMessagesTools - } - - // openai assistant message will have: https://platform.openai.com/docs/guides/function-calling#function-calling-steps - // "tool_calls":[ - // { - // "type": "function", - // "id": "call_12345xyz", - // "function": { - // "name": "get_weather", - // "arguments": "{\"latitude\":48.8566,\"longitude\":2.3522}" - // } - // }] - - // openai user response will be: - // { - // "role": "tool", - // "tool_call_id": tool_call.id, - // "content": str(result) - // } - - // treat all other providers like openai tool message for now - else { - - const newMessagesTools: ( - Exclude | { - role: 'assistant', - content: string; - tool_calls?: { - type: 'function'; - id: string; - function: { - name: string; - arguments: string; - } - }[] - } | { - role: 'tool', - id: string; // old val - tool_call_id: string; // new val - content: string; - } - )[] = []; - - for (let i = 0; i < newMessages.length; i += 1) { - const currMsg = newMessages[i] - - if (currMsg.role !== 'tool') { - newMessagesTools.push(currMsg) - continue - } - - // edit previous assistant message to have called the tool - const prevMsg = 0 <= i - 1 && i - 1 <= newMessagesTools.length ? newMessagesTools[i - 1] : undefined - if (prevMsg?.role === 'assistant') { - prevMsg.tool_calls = [{ - type: 'function', - id: currMsg.id, - function: { - name: currMsg.name, - arguments: JSON.stringify(currMsg.params) - } - }] - } - - // add the tool - newMessagesTools.push({ - role: 'tool', + // edit previous assistant message to have called the tool + const prevMsg = 0 <= i - 1 && i - 1 <= newMessages.length ? newMessages[i - 1] : undefined + if (prevMsg?.role === 'assistant') { + prevMsg.tool_calls = [{ + type: 'function', id: currMsg.id, - content: currMsg.content, - tool_call_id: currMsg.id, - }) + function: { + name: currMsg.name, + arguments: JSON.stringify(currMsg.params) + } + }] } - finalMessages = newMessagesTools + + // add the tool + newMessages.push({ + role: 'tool', + id: currMsg.id, + content: currMsg.content, + tool_call_id: currMsg.id, + }) } + return { messages: newMessages } + +} + + +// convert messages as if about to send to anthropic +/* +https://docs.anthropic.com/en/docs/build-with-claude/tool-use#tool-use-examples +anthropic MESSAGE (role=assistant): +"content": [{ + "type": "text", + "text": "I need to call the get_weather function, and the user wants SF, which is likely San Francisco, CA." +}, { + "type": "tool_use", + "id": "toolu_01A09q90qw90lq917835lq9", + "name": "get_weather", + "input": { "location": "San Francisco, CA", "unit": "celsius" } +}] +anthropic RESPONSE (role=user): +"content": [{ + "type": "tool_result", + "tool_use_id": "toolu_01A09q90qw90lq917835lq9", + "content": "15 degrees" +}] +*/ + +const prepareMessages_tools_anthropic = ({ messages }: { messages: LLMChatMessage[], }) => { + const newMessages: ( + Exclude | { + role: 'assistant', + content: string | ({ + type: 'text'; + text: string; + } | { + type: 'tool_use'; + name: string; + input: Record; + id: string; + })[] + } | { + role: 'user', + content: string | ({ + type: 'text'; + text: string; + } | { + type: 'tool_result'; + tool_use_id: string; + content: string; + })[] + } + )[] = messages; + + + for (let i = 0; i < newMessages.length; i += 1) { + const currMsg = newMessages[i] + + if (currMsg.role !== 'tool') continue + + const prevMsg = 0 <= i - 1 && i - 1 <= newMessages.length ? newMessages[i - 1] : undefined + + if (prevMsg?.role === 'assistant') { + if (typeof prevMsg.content === 'string') prevMsg.content = [{ type: 'text', text: prevMsg.content }] + prevMsg.content.push({ type: 'tool_use', id: currMsg.id, name: currMsg.name, input: parseObject(currMsg.params) }) + } + + // turn each tool into a user message with tool results at the end + newMessages[i] = { + role: 'user', + content: [ + ...[{ type: 'tool_result', tool_use_id: currMsg.id, content: currMsg.content }] as const, + ...currMsg.content ? [{ type: 'text', text: currMsg.content }] as const : [], + ] + } + } + return { messages: newMessages } +} - // 3. CROP MESSAGES SO EVERYTHING FITS IN CONTEXT - // TODO!!! - - console.log('SYSMG', separateSystemMessage) - console.log('FINAL MESSAGES', JSON.stringify(finalMessages, null, 2)) - - - return { - separateSystemMessageStr, - messages: finalMessages, +const prepareMessages_tools = ({ messages, supportsTools }: { messages: LLMChatMessage[], supportsTools: false | 'anthropic-style' | 'openai-style' }) => { + if (!supportsTools) { + return { messages: messages } + } + else if (supportsTools === 'anthropic-style') { + return prepareMessages_tools_anthropic({ messages }) + } + else if (supportsTools === 'openai-style') { + return prepareMessages_tools_openai({ messages }) + } + else { + throw 1 } } @@ -262,42 +267,59 @@ export const addSystemMessageAndToolSupport = (modelName: string, providerName: + + + + + + + + + + +export const prepareMessages = ({ + messages, + aiInstructions, + supportsSystemMessage, + supportsTools, +}: { + messages: LLMChatMessage[], + aiInstructions: string, + supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated', + supportsTools: false | 'anthropic-style' | 'openai-style', +}) => { + const { messages: messages1 } = prepareMessages_cloneAndTrim({ messages }) + const { messages: messages2, separateSystemMessageStr } = prepareMessages_systemMessage({ messages: messages1, aiInstructions, supportsSystemMessage }) + const { messages: messages3 } = prepareMessages_tools({ messages: messages2, supportsTools }) + return { + messages: messages3 as any, + separateSystemMessageStr + } as const +} + + /* - - ACCORDING TO 4o: gemini: similar to openai, but function_call, and only 1 call per message (no id because only 1 message) -gemini request: { -"role": "assistant", -"content": null, -"function_call": { -"name": "get_weather", -"arguments": { -"latitude": 48.8566, -"longitude": 2.3522 -} -} +gemini request: +{ "role": "assistant", + "content": null, + "function_call": { + "name": "get_weather", + "arguments": { + "latitude": 48.8566, + "longitude": 2.3522 + } + } } + gemini response: -{ -"role": "assistant", -"function_response": { -"name": "get_weather", -"response": { -"temperature": "15°C", -"condition": "Cloudy" +{ "role": "assistant", + "function_response": { + "name": "get_weather", + "response": { + "temperature": "15°C", + "condition": "Cloudy" + } + } } -} -} - - -+ anthropic - -+ openai-compat (4) -+ gemini - -ollama - - -mistral: same as openai - */ diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts index 7bb4e141..1c9ac21d 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts @@ -6,9 +6,7 @@ import { SendLLMMessageParams, OnText, OnFinalMessage, OnError } from '../../common/llmMessageTypes.js'; import { IMetricsService } from '../../common/metricsService.js'; import { displayInfoOfProviderName } from '../../common/voidSettingsTypes.js'; - -import { sendAnthropicChat } from './anthropic.js'; -import { sendOpenAIChat } from './openai.js'; +import { sendAnthropicChat, sendOpenAIChat } from './MODELS.js'; export const sendLLMMessage = ({ @@ -97,6 +95,10 @@ export const sendLLMMessage = ({ try { switch (providerName) { + case 'anthropic': + if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - Anthropic FIM' }) + else /* */ sendAnthropicChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions, tools }); + break; case 'openAI': case 'openRouter': case 'deepseek': @@ -107,13 +109,9 @@ export const sendLLMMessage = ({ case 'groq': case 'gemini': case 'xAI': - if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - OpenAI API FIM', toolCalls: [] }) + if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - OpenAI API FIM' }) else /* */ sendOpenAIChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions, tools }); break; - case 'anthropic': - if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - Anthropic FIM', toolCalls: [] }) - else /* */ sendAnthropicChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions, tools }); - break; default: onError({ message: `Error: Void provider was "${providerName}", which is not recognized.`, fullError: null }) break; diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessageChannel.ts b/src/vs/workbench/contrib/void/electron-main/llmMessageChannel.ts index b00ade9c..929c85e4 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessageChannel.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessageChannel.ts @@ -11,8 +11,7 @@ import { Emitter, Event } from '../../../../base/common/event.js'; import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, MainSendLLMMessageParams, AbortRef, SendLLMMessageParams, MainLLMMessageAbortParams, MainModelListParams, ModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, OllamaModelResponse, OpenaiCompatibleModelResponse, } from '../common/llmMessageTypes.js'; import { sendLLMMessage } from './llmMessage/sendLLMMessage.js' import { IMetricsService } from '../common/metricsService.js'; -import { ollamaList } from './llmMessage/ollama.js'; -import { openaiCompatibleList } from './llmMessage/openai.js'; +import { ollamaList } from './llmMessage/MODELS.js'; // NODE IMPLEMENTATION - calls actual sendLLMMessage() and returns listeners to it From fd5e5234348e6ccb2db44234d5ecd398ef74f4e4 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sun, 23 Feb 2025 16:58:43 -0800 Subject: [PATCH 35/41] models --- .vscode/settings.json | 2 +- .../contrib/void/common/llmMessageTypes.ts | 41 +- .../contrib/void/common/toolsService.ts | 7 + .../void/electron-main/llmMessage/MODELS.ts | 113 ++-- .../void/electron-main/llmMessage/_old.ts | 483 ------------------ 5 files changed, 73 insertions(+), 573 deletions(-) delete mode 100644 src/vs/workbench/contrib/void/electron-main/llmMessage/_old.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index d24ab6e5..910e0bec 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -113,7 +113,7 @@ "files.insertFinalNewline": false }, "[typescript]": { - "editor.defaultFormatter": "ms-vsliveshare.vsliveshare", + "editor.defaultFormatter": "vscode.typescript-language-features", "editor.formatOnSave": true }, "[javascript]": { diff --git a/src/vs/workbench/contrib/void/common/llmMessageTypes.ts b/src/vs/workbench/contrib/void/common/llmMessageTypes.ts index 58989cce..01e03ad4 100644 --- a/src/vs/workbench/contrib/void/common/llmMessageTypes.ts +++ b/src/vs/workbench/contrib/void/common/llmMessageTypes.ts @@ -65,7 +65,7 @@ export const toLLMChatMessage = (c: ChatMessage): LLMChatMessage => { } -type _InternalSendFIMMessage = { +export type LLMFIMMessage = { prefix: string; suffix: string; stopTokens: string[]; @@ -77,7 +77,7 @@ type SendLLMType = { tools?: InternalToolInfo[]; } | { messagesType: 'FIMMessage'; - messages: _InternalSendFIMMessage; + messages: LLMFIMMessage; tools?: undefined; } @@ -118,38 +118,6 @@ export type EventLLMMessageOnFinalMessageParams = Parameters[0] export type EventLLMMessageOnErrorParams = Parameters[0] & { requestId: string } -export type _InternalSendLLMChatMessageFnType = ( - params: { - aiInstructions: string; - - onText: OnText; - onFinalMessage: OnFinalMessage; - onError: OnError; - providerName: ProviderName; - settingsOfProvider: SettingsOfProvider; - modelName: string; - _setAborter: (aborter: () => void) => void; - - tools?: InternalToolInfo[], - - messages: LLMChatMessage[]; - } -) => void - -export type _InternalSendLLMFIMMessageFnType = ( - params: { - onText: OnText; - onFinalMessage: OnFinalMessage; - onError: OnError; - providerName: ProviderName; - settingsOfProvider: SettingsOfProvider; - modelName: string; - _setAborter: (aborter: () => void) => void; - - messages: _InternalSendFIMMessage; - } -) => void - // service -> main -> internal -> event (back to main) // (browser) @@ -190,10 +158,10 @@ export type OpenaiCompatibleModelResponse = { // params to the true list fn -export type ModelListParams = { +export type ModelListParams = { providerName: ProviderName; settingsOfProvider: SettingsOfProvider; - onSuccess: (param: { models: modelResponse[] }) => void; + onSuccess: (param: { models: ModelResponse[] }) => void; onError: (param: { error: string }) => void; } @@ -212,4 +180,3 @@ export type EventModelListOnErrorParams = Parameters = (params: ModelListParams) => void diff --git a/src/vs/workbench/contrib/void/common/toolsService.ts b/src/vs/workbench/contrib/void/common/toolsService.ts index 09ce82a2..4e92696c 100644 --- a/src/vs/workbench/contrib/void/common/toolsService.ts +++ b/src/vs/workbench/contrib/void/common/toolsService.ts @@ -89,6 +89,13 @@ export const voidTools = { export type ToolName = keyof typeof voidTools export const toolNames = Object.keys(voidTools) as ToolName[] +const toolNamesSet = new Set(toolNames) +export const isAToolName = (toolName: string): toolName is ToolName => { + const isAToolName = toolNamesSet.has(toolName) + return isAToolName +} + + export type ToolParamNames = keyof typeof voidTools[T]['params'] export type ToolParamsObj = { [paramName in ToolParamNames]: unknown } diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts index 3cefdfa9..e6e035fe 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts @@ -5,8 +5,8 @@ import OpenAI from 'openai'; import { Model as OpenAIModel } from 'openai/resources/models.js'; -import { _InternalModelListFnType, _InternalSendLLMFIMMessageFnType, _InternalSendLLMChatMessageFnType, OllamaModelResponse } from '../../common/llmMessageTypes.js'; -import { InternalToolInfo, ToolName, toolNames } from '../../common/toolsService.js'; +import { OllamaModelResponse, OnText, OnFinalMessage, OnError, LLMChatMessage, LLMFIMMessage, ModelListParams } from '../../common/llmMessageTypes.js'; +import { InternalToolInfo, isAToolName } from '../../common/toolsService.js'; import { defaultProviderSettings, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js'; import { prepareMessages } from './preprocessLLMMessages.js'; import Anthropic from '@anthropic-ai/sdk'; @@ -80,12 +80,13 @@ type ProviderSettings = { output: number; cache_read?: number; cache_write?: number; - }; + } supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated'; supportsTools: false | 'anthropic-style' | 'openai-style'; - }; - }; -}; + supportsFIM: false | 'TODO_FIM_FORMAT' + } + } +} const openAIProviderSettings: ProviderSettings = { @@ -97,21 +98,24 @@ const openAIProviderSettings: ProviderSettings = { FIMFormat: '', modelOptions: { - "o1": { + 'o1': { contextWindow: 128_000, cost: { input: 15.00, cache_read: 7.50, output: 60.00, }, + supportsFIM: false, supportsTools: false, supportsSystemMessage: 'developer-role', }, - "o3-mini": { + 'o3-mini': { contextWindow: 200_000, cost: { input: 1.10, cache_read: 0.55, output: 4.40, }, + supportsFIM: false, supportsTools: false, supportsSystemMessage: 'developer-role', }, - "gpt-4o": { + 'gpt-4o': { contextWindow: 128_000, cost: { input: 2.50, cache_read: 1.25, output: 10.00, }, + supportsFIM: false, supportsTools: 'openai-style', supportsSystemMessage: 'system-role', }, @@ -134,6 +138,7 @@ const anthropicProviderSettings: ProviderSettings = { "claude-3-5-sonnet-20241022": { contextWindow: 200_000, cost: { input: 3.00, cache_read: 0.30, cache_write: 3.75, output: 15.00 }, + supportsFIM: false, supportsSystemMessage: 'system-role', supportsTools: 'anthropic-style', @@ -141,17 +146,20 @@ const anthropicProviderSettings: ProviderSettings = { "claude-3-5-haiku-20241022": { contextWindow: 200_000, cost: { input: 0.80, cache_read: 0.08, cache_write: 1.00, output: 4.00 }, + supportsFIM: false, supportsSystemMessage: 'system-role', supportsTools: 'anthropic-style', }, "claude-3-opus-20240229": { contextWindow: 200_000, cost: { input: 15.00, cache_read: 1.50, cache_write: 18.75, output: 75.00 }, + supportsFIM: false, supportsSystemMessage: 'system-role', supportsTools: 'anthropic-style', }, "claude-3-sonnet-20240229": { contextWindow: 200_000, cost: { input: 3.00, output: 15.00 }, + supportsFIM: false, supportsSystemMessage: 'system-role', supportsTools: 'anthropic-style', } @@ -168,30 +176,13 @@ const grokProviderSettings: ProviderSettings = { FIMFormat: '', modelOptions: { - "claude-3-5-sonnet-20241022": { - contextWindow: 200_000, - cost: { input: 3.00, cache_read: 0.30, cache_write: 3.75, output: 15.00 }, + "grok-2-latest": { + contextWindow: 131_072, + cost: { input: 2.00, output: 10.00 }, + supportsFIM: false, supportsSystemMessage: 'system-role', - supportsTools: 'anthropic-style', - + supportsTools: 'openai-style', }, - "claude-3-5-haiku-20241022": { - contextWindow: 200_000, - cost: { input: 0.80, cache_read: 0.08, cache_write: 1.00, output: 4.00 }, - supportsSystemMessage: 'system-role', - supportsTools: 'anthropic-style', - }, - "claude-3-opus-20240229": { - contextWindow: 200_000, - cost: { input: 15.00, cache_read: 1.50, cache_write: 18.75, output: 75.00 }, - supportsSystemMessage: 'system-role', - supportsTools: 'anthropic-style', - }, - "claude-3-sonnet-20240229": { - contextWindow: 200_000, cost: { input: 3.00, output: 15.00 }, - supportsSystemMessage: 'system-role', - supportsTools: 'anthropic-style', - } } } @@ -199,14 +190,22 @@ const grokProviderSettings: ProviderSettings = { -// helpers -const toolNamesSet = new Set(toolNames) -const isAToolName = (toolName: string): toolName is ToolName => { - const isAToolName = toolNamesSet.has(toolName) - return isAToolName +type InternalCommonMessageParams = { + aiInstructions: string; + onText: OnText; + onFinalMessage: OnFinalMessage; + onError: OnError; + providerName: ProviderName; + settingsOfProvider: SettingsOfProvider; + modelName: string; + _setAborter: (aborter: () => void) => void; } +type SendChatParams_Internal = InternalCommonMessageParams & { messages: LLMChatMessage[]; tools?: InternalToolInfo[] } +type SendFIMParams_Internal = InternalCommonMessageParams & { messages: LLMFIMMessage; } +export type ListParams_Internal = ModelListParams + // ------------ OPENAI-COMPATIBLE (HELPERS) ------------ const toOpenAICompatibleTool = (toolInfo: InternalToolInfo) => { @@ -251,7 +250,7 @@ const newOpenAICompatibleSDK = ({ settingsOfProvider, providerName }: { settings else throw new Error(`Invalid providerName ${providerName}`) } -export const _sendOpenAICompatibleChat: _InternalSendLLMChatMessageFnType = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions, tools: tools_ }) => { +export const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions, tools: tools_ }: SendChatParams_Internal) => { const { messages } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage: '', supportsTools: '', }) const tools = (supportsTools && ((tools_?.length ?? 0) !== 0)) ? tools_?.map(tool => toOpenAICompatibleTool(tool)) : undefined @@ -291,7 +290,7 @@ export const _sendOpenAICompatibleChat: _InternalSendLLMChatMessageFnType = ({ m } -export const _openaiCompatibleList: _InternalModelListFnType = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider, providerName }) => { +export const _openaiCompatibleList = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider, providerName }: ListParams_Internal) => { const onSuccess = ({ models }: { models: OpenAIModel[] }) => { onSuccess_({ models }) } @@ -320,7 +319,7 @@ export const _openaiCompatibleList: _InternalModelListFnType = asyn // ------------ OPENAI ------------ -export const sendOpenAIChat: _InternalSendLLMChatMessageFnType = (params) => { +export const sendOpenAIChat = (params: SendChatParams_Internal) => { return _sendOpenAICompatibleChat(params) } @@ -346,7 +345,7 @@ const toolCallsFromAnthropicContent = (content: Anthropic.Messages.ContentBlock[ }).filter(t => !!t) } -export const sendAnthropicChat: _InternalSendLLMChatMessageFnType = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, aiInstructions, tools: tools_ }) => { +export const sendAnthropicChat = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, aiInstructions, tools: tools_ }: SendChatParams_Internal) => { const { messages, separateSystemMessageStr } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage: 'separated', supportsTools: 'anthropic-style', }) const thisConfig = settingsOfProvider.anthropic @@ -405,7 +404,7 @@ const newOllamaSDK = ({ endpoint }: { endpoint: string }) => { return ollama } -export const ollamaList: _InternalModelListFnType = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider }) => { +export const ollamaList = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider }: ListParams_Internal) => { const onSuccess = ({ models }: { models: OllamaModelResponse[] }) => { onSuccess_({ models }) } @@ -429,7 +428,7 @@ export const ollamaList: _InternalModelListFnType = async ( } } -export const sendOllamaFIM: _InternalSendLLMFIMMessageFnType = ({ messages, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => { +export const sendOllamaFIM = ({ messages, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }: SendFIMParams_Internal) => { const thisConfig = settingsOfProvider.ollama const ollama = newOllamaSDK({ endpoint: thisConfig.endpoint }) @@ -462,7 +461,7 @@ export const sendOllamaFIM: _InternalSendLLMFIMMessageFnType = ({ messages, onFi // ollama's implementation of openai-compatible SDK dumps all reasoning tokens out with message, and supports tools, so we can use it for chat! -export const sendOllamaMessage: _InternalSendLLMChatMessageFnType = (params) => { +export const sendOllamaChat = (params: SendChatParams_Internal) => { return _sendOpenAICompatibleChat(params) // TODO!!! filter out reasoning tags... } @@ -470,24 +469,24 @@ export const sendOllamaMessage: _InternalSendLLMChatMessageFnType = (params) => // ------------ OPENROUTER ------------ -export const sendOpenRouterFIM: _InternalSendLLMFIMMessageFnType = ({ messages, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => { +export const sendOpenRouterFIM = ({ messages, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }: SendFIMParams_Internal) => { // TODO!!! } -export const sendOpenRouterChat: _InternalSendLLMChatMessageFnType = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions, tools: tools_ }) => { - // payload should have {include_reasoning: true} https://openrouter.ai/announcements/reasoning-tokens-for-thinking-models - // response.choices[0].delta.reasoning +export const sendOpenRouterChat = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions, tools: tools_ }: SendChatParams_Internal) => { + // reasoning: response.choices[0].delta.reasoning : payload should have {include_reasoning: true} https://openrouter.ai/announcements/reasoning-tokens-for-thinking-models + // } // ------------ OPENAI-COMPATIBLE ------------ -export const openAICompatibleList: _InternalModelListFnType = async (params) => { +export const openAICompatibleList = async (params: ListParams_Internal) => { return _openaiCompatibleList(params) } // TODO!!! FIM // using openai's SDK is not ideal (your implementation might not do tools, reasoning, FIM etc correctly), talk to us for a custom integration -export const sendOpenAICompatibleChat: _InternalSendLLMChatMessageFnType = (params) => { +export const sendOpenAICompatibleChat = (params: SendChatParams_Internal) => { return _sendOpenAICompatibleChat(params) } @@ -496,13 +495,23 @@ export const sendOpenAICompatibleChat: _InternalSendLLMChatMessageFnType = (para // TODO!!! FIM // using openai's SDK is not ideal (your implementation might not do tools, reasoning, FIM etc correctly), talk to us for a custom integration -export const sendVLLMChat: _InternalSendLLMChatMessageFnType = (params) => { +export const sendVLLMChat = (params: SendChatParams_Internal) => { return _sendOpenAICompatibleChat(params) - // response.choices[0].delta.reasoning_content // https://docs.vllm.ai/en/stable/features/reasoning_outputs.html#streaming-chat-completions + // reasoning: response.choices[0].delta.reasoning_content // https://docs.vllm.ai/en/stable/features/reasoning_outputs.html#streaming-chat-completions } - +// ------------ DEEPSEEK API ------------ +export const sendDeepSeekAPIChat = (params: SendChatParams_Internal) => { + return _sendOpenAICompatibleChat(params) + // reasoning: response.choices[0].delta.reasoning_content // https://api-docs.deepseek.com/guides/reasoning_model +} + + +// ------------ GEMINI ------------ +// ------------ MISTRAL ------------ +// ------------ GROQ ------------ +// ------------ GROK ------------ diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/_old.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/_old.ts deleted file mode 100644 index 8aa80bf4..00000000 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/_old.ts +++ /dev/null @@ -1,483 +0,0 @@ -// /*-------------------------------------------------------------------------------------- -// * Copyright 2025 Glass Devtools, Inc. All rights reserved. -// * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. -// *--------------------------------------------------------------------------------------*/ - -// import Groq from 'groq-sdk'; -// import { _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes.js'; - -// // Groq -// export const sendGroqChat: _InternalSendLLMChatMessageFnType = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => { -// let fullText = ''; - -// const thisConfig = settingsOfProvider.groq - -// const groq = new Groq({ -// apiKey: thisConfig.apiKey, -// dangerouslyAllowBrowser: true -// }); - -// await groq.chat.completions -// .create({ -// messages: messages, -// model: modelName, -// stream: true, -// }) -// .then(async response => { -// _setAborter(() => response.controller.abort()) -// // when receive text -// for await (const chunk of response) { -// const newText = chunk.choices[0]?.delta?.content || ''; -// fullText += newText; -// onText({ newText, fullText }); -// } - -// onFinalMessage({ fullText, tools: [] }); -// }) -// .catch(error => { -// onError({ message: error + '', fullError: error }); -// }) - - -// }; - - - -// /*-------------------------------------------------------------------------------------- -// * Copyright 2025 Glass Devtools, Inc. All rights reserved. -// * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. -// *--------------------------------------------------------------------------------------*/ - -// import { Mistral } from '@mistralai/mistralai'; -// import { _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes.js'; - -// // Mistral -// export const sendMistralChat: _InternalSendLLMChatMessageFnType = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => { -// let fullText = ''; - -// const thisConfig = settingsOfProvider.mistral; - -// const mistral = new Mistral({ -// apiKey: thisConfig.apiKey, -// }) - -// await mistral.chat -// .stream({ -// messages: messages, -// model: modelName, -// stream: true, -// }) -// .then(async response => { -// // Mistral has a really nonstandard API - no interrupt and weird stream types -// _setAborter(() => { console.log('Mistral does not support interrupts! Further messages will just be ignored.') }); -// // when receive text -// for await (const chunk of response) { -// const c = chunk.data.choices[0].delta.content || '' -// const newText = ( -// typeof c === 'string' ? c -// : c?.map(c => c.type === 'text' ? c.text : c.type).join('\n') -// ) -// fullText += newText; -// onText({ newText, fullText }); -// } - -// onFinalMessage({ fullText, tools: [] }); -// }) -// .catch(error => { -// onError({ message: error + '', fullError: error }); -// }) -// } - - - - - - - - - - - - - - - - - - - - - - - - - - -// export const recognizedModels = [ -// // chat -// 'OpenAI 4o', -// 'Anthropic Claude', -// 'Llama 3.x', -// 'Deepseek Chat', // deepseek coder v2 is now merged into chat (V3) https://api-docs.deepseek.com/updates#deepseek-coder--deepseek-chat-upgraded-to-deepseek-v25-model -// 'xAI Grok', -// // 'xAI Grok', -// // 'Google Gemini, Gemma', -// // 'Microsoft Phi4', - - -// // coding (autocomplete) -// 'Alibaba Qwen2.5 Coder Instruct', // we recommend this over Qwen2.5 -// 'Mistral Codestral', - -// // thinking -// 'OpenAI o1', -// 'Deepseek R1', - -// // general -// // 'Mixtral 8x7b' -// // 'Qwen2.5', - -// ] as const - -// type RecognizedModelName = (typeof recognizedModels)[number] | '' - - -// export function recognizedModelOfModelName(modelName: string): RecognizedModelName { -// const lower = modelName.toLowerCase(); - -// if (lower.includes('gpt-4o')) -// return 'OpenAI 4o'; -// if (lower.includes('claude')) -// return 'Anthropic Claude'; -// if (lower.includes('llama')) -// return 'Llama 3.x'; -// if (lower.includes('qwen2.5-coder')) -// return 'Alibaba Qwen2.5 Coder Instruct'; -// if (lower.includes('mistral')) -// return 'Mistral Codestral'; -// if (/\bo1\b/.test(lower) || /\bo3\b/.test(lower)) // o1, o3 -// return 'OpenAI o1'; -// if (lower.includes('deepseek-r1') || lower.includes('deepseek-reasoner')) -// return 'Deepseek R1'; -// if (lower.includes('deepseek')) -// return 'Deepseek Chat' -// if (lower.includes('grok')) -// return 'xAI Grok' - -// return ''; -// } - - -// const developerInfoAtProvider: { [providerName in ProviderName]: DeveloperInfoAtProvider } = { -// 'anthropic': { -// overrideSettingsForAllModels: { -// supportsSystemMessage: true, -// supportsTools: true, -// _supportsAutocompleteFIM: false, -// _supportsStreaming: true, -// } -// }, -// 'deepseek': { -// overrideSettingsForAllModels: { -// } -// }, -// 'ollama': { -// }, -// 'openRouter': { -// }, -// 'openAICompatible': { -// }, -// 'openAI': { -// }, -// 'gemini': { -// }, -// 'mistral': { -// }, -// 'groq': { -// }, -// 'xAI': { -// }, -// 'vLLM': { -// }, -// } -// export const developerInfoOfProviderName = (providerName: ProviderName): Partial => { -// return developerInfoAtProvider[providerName] ?? {} -// } - - - - -// // providerName is optional, but gives some extra fallbacks if provided -// const developerInfoOfRecognizedModelName: { [recognizedModel in RecognizedModelName]: Omit } = { -// 'OpenAI 4o': { -// supportsSystemMessage: true, -// supportsTools: true, -// _supportsAutocompleteFIM: false, -// _supportsStreaming: true, -// _maxTokens: 4096, -// }, - -// 'Anthropic Claude': { -// supportsSystemMessage: true, -// supportsTools: false, -// _supportsAutocompleteFIM: false, -// _supportsStreaming: false, -// _maxTokens: 4096, -// }, - -// 'Llama 3.x': { -// supportsSystemMessage: true, -// supportsTools: true, -// _supportsAutocompleteFIM: false, -// _supportsStreaming: false, -// _maxTokens: 4096, -// }, - -// 'xAI Grok': { -// supportsSystemMessage: true, -// supportsTools: true, -// _supportsAutocompleteFIM: false, -// _supportsStreaming: true, -// _maxTokens: 4096, - -// }, - -// 'Deepseek Chat': { -// supportsSystemMessage: true, -// supportsTools: false, -// _supportsAutocompleteFIM: false, -// _supportsStreaming: false, -// _maxTokens: 4096, -// }, - -// 'Alibaba Qwen2.5 Coder Instruct': { -// supportsSystemMessage: true, -// supportsTools: true, -// _supportsAutocompleteFIM: false, -// _supportsStreaming: false, -// _maxTokens: 4096, -// }, - -// 'Mistral Codestral': { -// supportsSystemMessage: true, -// supportsTools: true, -// _supportsAutocompleteFIM: false, -// _supportsStreaming: false, -// _maxTokens: 4096, -// }, - -// 'OpenAI o1': { -// supportsSystemMessage: 'developer', -// supportsTools: false, -// _supportsAutocompleteFIM: false, -// _supportsStreaming: true, -// _maxTokens: 4096, -// }, - -// 'Deepseek R1': { -// supportsSystemMessage: false, -// supportsTools: false, -// _supportsAutocompleteFIM: false, -// _supportsStreaming: false, -// _maxTokens: 4096, -// }, - - -// '': { -// supportsSystemMessage: false, -// supportsTools: false, -// _supportsAutocompleteFIM: false, -// _supportsStreaming: false, -// _maxTokens: 4096, -// }, -// } -// export const developerInfoOfModelName = (modelName: string, overrides?: Partial): DeveloperInfoAtModel => { -// const recognizedModelName = recognizedModelOfModelName(modelName) -// return { -// _recognizedModelName: recognizedModelName, -// ...developerInfoOfRecognizedModelName[recognizedModelName], -// ...overrides -// } -// } - - - - - - -// // creates `modelInfo` from `modelNames` - - - - - -// export const modelInfoOfAutodetectedModelNames = (defaultModelNames: string[], options: { existingModels: VoidModelInfo[] }) => { -// const { existingModels } = options - -// const existingModelsMap: Record = {} -// for (const existingModel of existingModels) { -// existingModelsMap[existingModel.modelName] = existingModel -// } - -// return defaultModelNames.map((modelName, i) => ({ -// modelName, -// isDefault: true, -// isAutodetected: true, -// isHidden: !!existingModelsMap[modelName]?.isHidden, -// ...developerInfoOfModelName(modelName) -// })) -// } - - - - - - -// export const anthropicMaxPossibleTokens = (modelName: string) => { -// if (modelName === 'claude-3-5-sonnet-20241022' -// || modelName === 'claude-3-5-haiku-20241022') -// return 8192 -// if (modelName === 'claude-3-opus-20240229' -// || modelName === 'claude-3-sonnet-20240229' -// || modelName === 'claude-3-haiku-20240307') -// return 4096 -// return 1024 // return a reasonably small number if they're using a different model -// } - - - - - - - - - - - - - - - - - - - -// // Ollama chat -// export const sendOllamaChat: _InternalSendLLMChatMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => { - -// const thisConfig = settingsOfProvider.ollama -// // if endpoint is empty, normally ollama will send to 11434, but we want it to fail - the user should type it in -// if (!thisConfig.endpoint) throw new Error(`Ollama Endpoint was empty (please enter ${defaultProviderSettings.ollama.endpoint} if you want the default).`) - -// let fullText = '' - -// const ollama = new Ollama({ host: thisConfig.endpoint }) - -// ollama.chat({ -// model: modelName, -// messages: messages, -// stream: true, -// // options: { num_predict: parseMaxTokensStr(thisConfig.maxTokens) } // this is max_tokens -// }) -// .then(async stream => { -// _setAborter(() => stream.abort()) -// // iterate through the stream -// for await (const chunk of stream) { -// const newText = chunk.message.content; - -// // chunk.message.tool_calls[0].function.arguments - -// fullText += newText; -// onText({ newText, fullText }); -// } - -// onFinalMessage({ fullText, tools: [] }); - -// }) -// // when error/fail -// .catch((error) => { -// onError({ message: error + '', fullError: error }) -// }) - -// }; - - - - - - - -// type NewParams = Pick[0] & Parameters<_InternalSendLLMFIMMessageFnType>[0], 'settingsOfProvider' | 'providerName'> -// const newOpenAI = ({ settingsOfProvider, providerName }: NewParams) => { - -// if (providerName === 'openAI') { -// const thisConfig = settingsOfProvider[providerName] -// return new OpenAI({ -// apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true -// }) -// } -// else if (providerName === 'ollama') { -// const thisConfig = settingsOfProvider[providerName] -// return new OpenAI({ -// baseURL: `${thisConfig.endpoint}/v1`, apiKey: 'noop', dangerouslyAllowBrowser: true, -// }) -// } -// else if (providerName === 'vLLM') { -// const thisConfig = settingsOfProvider[providerName] -// return new OpenAI({ -// baseURL: `${thisConfig.endpoint}/v1`, apiKey: 'noop', dangerouslyAllowBrowser: true, -// }) -// } -// else if (providerName === 'openRouter') { -// const thisConfig = settingsOfProvider[providerName] -// return new OpenAI({ -// baseURL: 'https://openrouter.ai/api/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, -// defaultHeaders: { -// 'HTTP-Referer': 'https://voideditor.com', // Optional, for including your app on openrouter.ai rankings. -// 'X-Title': 'Void Editor', // Optional. Shows in rankings on openrouter.ai. -// }, -// }) -// } -// else if (providerName === 'gemini') { -// const thisConfig = settingsOfProvider[providerName] -// return new OpenAI({ -// baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, -// }) -// } -// else if (providerName === 'deepseek') { -// const thisConfig = settingsOfProvider[providerName] -// return new OpenAI({ -// baseURL: 'https://api.deepseek.com/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, -// }) -// } -// else if (providerName === 'openAICompatible') { -// const thisConfig = settingsOfProvider[providerName] -// return new OpenAI({ -// baseURL: thisConfig.endpoint, apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, -// }) -// } -// else if (providerName === 'mistral') { -// const thisConfig = settingsOfProvider[providerName] -// return new OpenAI({ -// baseURL: 'https://api.mistral.ai/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, -// }) -// } -// else if (providerName === 'groq') { -// const thisConfig = settingsOfProvider[providerName] -// return new OpenAI({ -// baseURL: 'https://api.groq.com/openai/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, -// }) -// } -// else if (providerName === 'xAI') { -// const thisConfig = settingsOfProvider[providerName] -// return new OpenAI({ -// baseURL: 'https://api.x.ai/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, -// }) -// } -// else { -// console.error(`sendOpenAICompatibleMsg: invalid providerName: ${providerName}`) -// throw new Error(`Void providerName was invalid: ${providerName}`) -// } -// } - - From 9f20476eea682cdab5e9757b7844604b8eb5252b Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sun, 23 Feb 2025 21:37:34 -0800 Subject: [PATCH 36/41] provider support progress Co-authored-by: Mathew Pareles --- .../browser/helpers/extractCodeFromResult.ts | 21 +- .../contrib/void/common/llmMessageService.ts | 109 ++-- .../contrib/void/common/llmMessageTypes.ts | 7 +- .../void/common/refreshModelService.ts | 10 +- .../void/electron-main/llmMessage/MODELS.ts | 588 +++++++++++++----- .../llmMessage/preprocessLLMMessages.ts | 51 +- .../llmMessage/sendLLMMessage.ts | 47 +- .../void/electron-main/llmMessageChannel.ts | 133 ++-- 8 files changed, 602 insertions(+), 364 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts b/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts index cd3276ff..806676da 100644 --- a/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts +++ b/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts @@ -59,7 +59,7 @@ class SurroundingsRemover { // return offset === suffix.length // } - removeFromStartUntil = (until: string, alsoRemoveUntilStr: boolean) => { + removeFromStartUntilFullMatch = (until: string, alsoRemoveUntilStr: boolean) => { const index = this.originalS.indexOf(until, this.i) if (index === -1) { @@ -86,7 +86,7 @@ class SurroundingsRemover { const foundCodeBlock = pm.removePrefix('```') if (!foundCodeBlock) return false - pm.removeFromStartUntil('\n', true) // language + pm.removeFromStartUntilFullMatch('\n', true) // language const j = pm.j let foundCodeBlockEnd = pm.removeSuffix('```') @@ -159,27 +159,10 @@ export const extractCodeFromFIM = ({ text, recentlyAddedTextLen, midTag, }: { te const [delta, ignoredSuffix] = pm.deltaInfo(recentlyAddedTextLen) return [s, delta, ignoredSuffix] - - - // // const regex = /[\s\S]*?(?:`{1,3}\s*([a-zA-Z_]+[\w]*)?[\s\S]*?)?([\s\S]*?)(?:<\/MID>|`{1,3}|$)/; - // const regex = new RegExp( - // `[\\s\\S]*?(?:\`{1,3}\\s*([a-zA-Z_]+[\\w]*)?[\\s\\S]*?)?<${midTag}>([\\s\\S]*?)(?:|\`{1,3}|$)`, - // '' - // ); - // const match = text.match(regex); - // if (match) { - // const [_, languageName, codeBetweenMidTags] = match; - // return [languageName, codeBetweenMidTags] as const - - // } else { - // return [undefined, extractCodeFromRegular(text)] as const - // } - } - export type ExtractedSearchReplaceBlock = { state: 'writingOriginal' | 'writingFinal' | 'done', orig: string, diff --git a/src/vs/workbench/contrib/void/common/llmMessageService.ts b/src/vs/workbench/contrib/void/common/llmMessageService.ts index c8f9ea2c..b2266213 100644 --- a/src/vs/workbench/contrib/void/common/llmMessageService.ts +++ b/src/vs/workbench/contrib/void/common/llmMessageService.ts @@ -3,7 +3,7 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, ServiceSendLLMMessageParams, MainSendLLMMessageParams, MainLLMMessageAbortParams, ServiceModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, MainModelListParams, OllamaModelResponse, OpenaiCompatibleModelResponse, } from './llmMessageTypes.js'; +import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, ServiceSendLLMMessageParams, MainSendLLMMessageParams, MainLLMMessageAbortParams, ServiceModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, MainModelListParams, OllamaModelResponse, VLLMModelResponse, } from './llmMessageTypes.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js'; @@ -24,27 +24,39 @@ export interface ILLMMessageService { sendLLMMessage: (params: ServiceSendLLMMessageParams) => string | null; abort: (requestId: string) => void; ollamaList: (params: ServiceModelListParams) => void; - openAICompatibleList: (params: ServiceModelListParams) => void; + vLLMList: (params: ServiceModelListParams) => void; } + +// open this file side by side with llmMessageChannel export class LLMMessageService extends Disposable implements ILLMMessageService { readonly _serviceBrand: undefined; private readonly channel: IChannel // LLMMessageChannel - // llmMessage - private readonly onTextHooks_llm: { [eventId: string]: ((params: EventLLMMessageOnTextParams) => void) } = {} - private readonly onFinalMessageHooks_llm: { [eventId: string]: ((params: EventLLMMessageOnFinalMessageParams) => void) } = {} - private readonly onErrorHooks_llm: { [eventId: string]: ((params: EventLLMMessageOnErrorParams) => void) } = {} + // sendLLMMessage + private readonly llmMessageHooks = { + onText: {} as { [eventId: string]: ((params: EventLLMMessageOnTextParams) => void) }, + onFinalMessage: {} as { [eventId: string]: ((params: EventLLMMessageOnFinalMessageParams) => void) }, + onError: {} as { [eventId: string]: ((params: EventLLMMessageOnErrorParams) => void) }, + } - - // ollamaList - private readonly onSuccess_ollama: { [eventId: string]: ((params: EventModelListOnSuccessParams) => void) } = {} - private readonly onError_ollama: { [eventId: string]: ((params: EventModelListOnErrorParams) => void) } = {} - - // openAICompatibleList - private readonly onSuccess_openAICompatible: { [eventId: string]: ((params: EventModelListOnSuccessParams) => void) } = {} - private readonly onError_openAICompatible: { [eventId: string]: ((params: EventModelListOnErrorParams) => void) } = {} + // list hooks + private readonly listHooks = { + ollama: { + success: {} as { [eventId: string]: ((params: EventModelListOnSuccessParams) => void) }, + error: {} as { [eventId: string]: ((params: EventModelListOnErrorParams) => void) }, + }, + vLLM: { + success: {} as { [eventId: string]: ((params: EventModelListOnSuccessParams) => void) }, + error: {} as { [eventId: string]: ((params: EventModelListOnErrorParams) => void) }, + } + } satisfies { + [providerName: string]: { + success: { [eventId: string]: ((params: EventModelListOnSuccessParams) => void) }, + error: { [eventId: string]: ((params: EventModelListOnErrorParams) => void) }, + } + } constructor( @IMainProcessService private readonly mainProcessService: IMainProcessService, // used as a renderer (only usable on client side) @@ -59,32 +71,14 @@ export class LLMMessageService extends Disposable implements ILLMMessageService // .listen sets up an IPC channel and takes a few ms, so we set up listeners immediately and add hooks to them instead // llm - this._register((this.channel.listen('onText_llm') satisfies Event)(e => { - this.onTextHooks_llm[e.requestId]?.(e) - })) - this._register((this.channel.listen('onFinalMessage_llm') satisfies Event)(e => { - this.onFinalMessageHooks_llm[e.requestId]?.(e) - this._onRequestIdDone(e.requestId) - })) - this._register((this.channel.listen('onError_llm') satisfies Event)(e => { - console.error('Error in LLMMessageService:', JSON.stringify(e)) - this.onErrorHooks_llm[e.requestId]?.(e) - this._onRequestIdDone(e.requestId) - })) + this._register((this.channel.listen('onText_sendLLMMessage') satisfies Event)(e => { this.llmMessageHooks.onText[e.requestId]?.(e) })) + this._register((this.channel.listen('onFinalMessage_sendLLMMessage') satisfies Event)(e => { this.llmMessageHooks.onFinalMessage[e.requestId]?.(e); this._onRequestIdDone(e.requestId) })) + this._register((this.channel.listen('onError_sendLLMMessage') satisfies Event)(e => { this.llmMessageHooks.onError[e.requestId]?.(e); this._onRequestIdDone(e.requestId); console.error('Error in LLMMessageService:', JSON.stringify(e)) })) // ollama .list() - this._register((this.channel.listen('onSuccess_ollama') satisfies Event>)(e => { - this.onSuccess_ollama[e.requestId]?.(e) - })) - this._register((this.channel.listen('onError_ollama') satisfies Event>)(e => { - this.onError_ollama[e.requestId]?.(e) - })) - // openaiCompatible .list() - this._register((this.channel.listen('onSuccess_openAICompatible') satisfies Event>)(e => { - this.onSuccess_openAICompatible[e.requestId]?.(e) - })) - this._register((this.channel.listen('onError_openAICompatible') satisfies Event>)(e => { - this.onError_openAICompatible[e.requestId]?.(e) - })) + this._register((this.channel.listen('onSuccess_list_ollama') satisfies Event>)(e => { this.listHooks.ollama.success[e.requestId]?.(e) })) + this._register((this.channel.listen('onError_list_ollama') satisfies Event>)(e => { this.listHooks.ollama.error[e.requestId]?.(e) })) + this._register((this.channel.listen('onSuccess_list_vLLM') satisfies Event>)(e => { this.listHooks.vLLM.success[e.requestId]?.(e) })) + this._register((this.channel.listen('onError_list_vLLM') satisfies Event>)(e => { this.listHooks.vLLM.error[e.requestId]?.(e) })) } @@ -117,9 +111,9 @@ export class LLMMessageService extends Disposable implements ILLMMessageService // add state for request id const requestId = generateUuid(); - this.onTextHooks_llm[requestId] = onText - this.onFinalMessageHooks_llm[requestId] = onFinalMessage - this.onErrorHooks_llm[requestId] = onError + this.llmMessageHooks.onText[requestId] = onText + this.llmMessageHooks.onFinalMessage[requestId] = onFinalMessage + this.llmMessageHooks.onError[requestId] = onError const { aiInstructions } = this.voidSettingsService.state.globalSettings const { settingsOfProvider } = this.voidSettingsService.state @@ -151,8 +145,8 @@ export class LLMMessageService extends Disposable implements ILLMMessageService // add state for request id const requestId_ = generateUuid(); - this.onSuccess_ollama[requestId_] = onSuccess - this.onError_ollama[requestId_] = onError + this.listHooks.ollama.success[requestId_] = onSuccess + this.listHooks.ollama.error[requestId_] = onError this.channel.call('ollamaList', { ...proxyParams, @@ -163,33 +157,34 @@ export class LLMMessageService extends Disposable implements ILLMMessageService } - openAICompatibleList = (params: ServiceModelListParams) => { + vLLMList = (params: ServiceModelListParams) => { const { onSuccess, onError, ...proxyParams } = params const { settingsOfProvider } = this.voidSettingsService.state // add state for request id const requestId_ = generateUuid(); - this.onSuccess_openAICompatible[requestId_] = onSuccess - this.onError_openAICompatible[requestId_] = onError + this.listHooks.vLLM.success[requestId_] = onSuccess + this.listHooks.vLLM.error[requestId_] = onError - this.channel.call('openAICompatibleList', { + this.channel.call('vLLMList', { ...proxyParams, settingsOfProvider, - providerName: 'openAICompatible', + providerName: 'vLLM', requestId: requestId_, - } satisfies MainModelListParams) + } satisfies MainModelListParams) } - - _onRequestIdDone(requestId: string) { - delete this.onTextHooks_llm[requestId] - delete this.onFinalMessageHooks_llm[requestId] - delete this.onErrorHooks_llm[requestId] + delete this.llmMessageHooks.onText[requestId] + delete this.llmMessageHooks.onFinalMessage[requestId] + delete this.llmMessageHooks.onError[requestId] - delete this.onSuccess_ollama[requestId] - delete this.onError_ollama[requestId] + delete this.listHooks.ollama.success[requestId] + delete this.listHooks.ollama.error[requestId] + + delete this.listHooks.vLLM.success[requestId] + delete this.listHooks.vLLM.error[requestId] } } diff --git a/src/vs/workbench/contrib/void/common/llmMessageTypes.ts b/src/vs/workbench/contrib/void/common/llmMessageTypes.ts index 01e03ad4..abe88970 100644 --- a/src/vs/workbench/contrib/void/common/llmMessageTypes.ts +++ b/src/vs/workbench/contrib/void/common/llmMessageTypes.ts @@ -45,7 +45,7 @@ export type ToolCallType = { } -export type OnText = (p: { newText: string, fullText: string }) => void +export type OnText = (p: { newText: string, fullText: string; newReasoning: string; fullReasoning: string }) => void export type OnFinalMessage = (p: { fullText: string, toolCalls?: ToolCallType[] }) => void // id is tool_use_id export type OnError = (p: { message: string, fullError: Error | null }) => void export type AbortRef = { current: (() => void) | null } @@ -149,13 +149,16 @@ export type OllamaModelResponse = { size_vram: number; } -export type OpenaiCompatibleModelResponse = { +type OpenaiCompatibleModelResponse = { id: string; created: number; object: 'model'; owned_by: string; } +export type VLLMModelResponse = OpenaiCompatibleModelResponse + + // params to the true list fn export type ModelListParams = { diff --git a/src/vs/workbench/contrib/void/common/refreshModelService.ts b/src/vs/workbench/contrib/void/common/refreshModelService.ts index 1c95a4ad..1d68b304 100644 --- a/src/vs/workbench/contrib/void/common/refreshModelService.ts +++ b/src/vs/workbench/contrib/void/common/refreshModelService.ts @@ -8,7 +8,7 @@ import { ILLMMessageService } from './llmMessageService.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js'; import { RefreshableProviderName, refreshableProviderNames, SettingsOfProvider } from './voidSettingsTypes.js'; -import { OllamaModelResponse, OpenaiCompatibleModelResponse } from './llmMessageTypes.js'; +import { OllamaModelResponse, VLLMModelResponse } from './llmMessageTypes.js'; import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; @@ -160,9 +160,8 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ } } const listFn = providerName === 'ollama' ? this.llmMessageService.ollamaList - : providerName === 'vLLM' ? this.llmMessageService.openAICompatibleList - : providerName === 'openAICompatible' ? this.llmMessageService.openAICompatibleList - : () => { } + : providerName === 'vLLM' ? this.llmMessageService.vLLMList + : () => { } listFn({ onSuccess: ({ models }) => { @@ -172,8 +171,7 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ providerName, models.map(model => { if (providerName === 'ollama') return (model as OllamaModelResponse).name; - else if (providerName === 'vLLM') return (model as OpenaiCompatibleModelResponse).id; - else if (providerName === 'openAICompatible') return (model as OpenaiCompatibleModelResponse).id; + else if (providerName === 'vLLM') return (model as VLLMModelResponse).id; else throw new Error('refreshMode fn: unknown provider', providerName); }), { enableProviderOnSuccess: options.enableProviderOnSuccess, hideRefresh: options.doNotFire } diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts index e6e035fe..ce0d0537 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts @@ -3,11 +3,11 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import OpenAI from 'openai'; +import OpenAI, { ClientOptions } from 'openai'; import { Model as OpenAIModel } from 'openai/resources/models.js'; import { OllamaModelResponse, OnText, OnFinalMessage, OnError, LLMChatMessage, LLMFIMMessage, ModelListParams } from '../../common/llmMessageTypes.js'; import { InternalToolInfo, isAToolName } from '../../common/toolsService.js'; -import { defaultProviderSettings, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js'; +import { defaultProviderSettings, displayInfoOfProviderName, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js'; import { prepareMessages } from './preprocessLLMMessages.js'; import Anthropic from '@anthropic-ai/sdk'; import { Ollama } from 'ollama'; @@ -68,129 +68,215 @@ export const defaultModelsOfProvider = { +type ModelOptions = { + contextWindow: number; + cost: { + input: number; + output: number; + cache_read?: number; + cache_write?: number; + } + supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated'; + supportsTools: false | 'anthropic-style' | 'openai-style'; + supportsFIM: false | 'TODO_FIM_FORMAT'; + + supportsReasoning: boolean; // not whether it reasons, but whether it outputs reasoning tokens + manualMatchReasoningTokens?: [string, string]; // reasoning tokens if it's an OSS model +} + +type ProviderReasoningOptions = { + // 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 tags + output?: + | { nameOfFieldInDelta?: string, needsManualParse?: undefined, } + | { nameOfFieldInDelta?: undefined, needsManualParse?: true, }; +} + type ProviderSettings = { - thinkingFormat: string; - toolsFormat: string; - FIMFormat: string; - modelOptions: { - [key: string]: { - contextWindow: number; - cost: { - input: number; - output: number; - cache_read?: number; - cache_write?: number; - } - supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated'; - supportsTools: false | 'anthropic-style' | 'openai-style'; - supportsFIM: false | 'TODO_FIM_FORMAT' + providerReasoningOptions?: ProviderReasoningOptions; + modelOptions: { [key: string]: ModelOptions }; + modelOptionsFallback: (modelName: string) => ModelOptions; // allowed to throw error if modeName is totally invalid +} + + +type ModelSettingsOfProvider = { + [providerName in ProviderName]: ProviderSettings +} + + + + + +const modelNotRecognizedErrorMessage = (modelName: string, providerName: ProviderName) => `Void could not find a model matching ${modelName} for ${displayInfoOfProviderName(providerName).title}.` + + + +// ---------------- OPENAI ---------------- +const openAIModelOptions = { + "o1": { + contextWindow: 128_000, + cost: { input: 15.00, cache_read: 7.50, output: 60.00, }, + supportsFIM: false, + supportsTools: false, + supportsSystemMessage: 'developer-role', + supportsReasoning: false, + }, + "o3-mini": { + contextWindow: 200_000, + cost: { input: 1.10, cache_read: 0.55, output: 4.40, }, + supportsFIM: false, + supportsTools: false, + supportsSystemMessage: 'developer-role', + supportsReasoning: false, + }, + "gpt-4o": { + contextWindow: 128_000, + cost: { input: 2.50, cache_read: 1.25, output: 10.00, }, + supportsFIM: false, + supportsTools: 'openai-style', + supportsSystemMessage: 'system-role', + supportsReasoning: false, + }, +} as const + +const openAISettings: ProviderSettings = { + modelOptions: openAIModelOptions, + modelOptionsFallback: (modelName) => { + if (modelName.includes('o1')) return openAIModelOptions['o1'] + if (modelName.includes('o3-mini')) return openAIModelOptions['o3-mini'] + if (modelName.includes('gpt-4o')) return openAIModelOptions['gpt-4o'] + throw new Error(modelNotRecognizedErrorMessage(modelName, 'openAI')) + } +} + +// ---------------- ANTHROPIC ---------------- +const anthropicModelOptions = { + "claude-3-5-sonnet-20241022": { + contextWindow: 200_000, + 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, + 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, + 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": { + contextWindow: 200_000, cost: { input: 3.00, output: 15.00 }, + supportsFIM: false, + supportsSystemMessage: 'separated', + supportsTools: 'anthropic-style', + supportsReasoning: false, + } +} as const + +const anthropicSettings: ProviderSettings = { + modelOptions: anthropicModelOptions, + modelOptionsFallback: (modelName) => { + throw new Error(modelNotRecognizedErrorMessage(modelName, 'anthropic')) + } +} + + +// ---------------- XAI ---------------- +const XAIModelOptions = { + "grok-2-latest": { + contextWindow: 131_072, + cost: { input: 2.00, output: 10.00 }, + supportsFIM: false, + supportsSystemMessage: 'system-role', + supportsTools: 'openai-style', + supportsReasoning: false, + }, +} as const + +const XAISettings: ProviderSettings = { + modelOptions: XAIModelOptions, + modelOptionsFallback: (modelName) => { + throw new Error(modelNotRecognizedErrorMessage(modelName, 'xAI')) + } +} + + + +const modelSettingsOfProvider: ModelSettingsOfProvider = { + openAI: openAISettings, + anthropic: anthropicSettings, + xAI: XAISettings, + gemini: { + modelOptions: { + } - } -} + }, + googleVertex: { + }, + microsoftAzure: { -const openAIProviderSettings: ProviderSettings = { - - thinkingFormat: '', - - toolsFormat: '', - - FIMFormat: '', - - modelOptions: { - 'o1': { - contextWindow: 128_000, - cost: { input: 15.00, cache_read: 7.50, output: 60.00, }, - supportsFIM: false, - supportsTools: false, - supportsSystemMessage: 'developer-role', - }, - 'o3-mini': { - contextWindow: 200_000, - cost: { input: 1.10, cache_read: 0.55, output: 4.40, }, - supportsFIM: false, - supportsTools: false, - supportsSystemMessage: 'developer-role', - }, - 'gpt-4o': { - contextWindow: 128_000, - cost: { input: 2.50, cache_read: 1.25, output: 10.00, }, - supportsFIM: false, - supportsTools: 'openai-style', - supportsSystemMessage: 'system-role', - }, - } - -} - - - - - -const anthropicProviderSettings: ProviderSettings = { - thinkingFormat: '', - - toolsFormat: '', - - FIMFormat: '', - - modelOptions: { - "claude-3-5-sonnet-20241022": { - contextWindow: 200_000, - cost: { input: 3.00, cache_read: 0.30, cache_write: 3.75, output: 15.00 }, - supportsFIM: false, - supportsSystemMessage: 'system-role', - supportsTools: 'anthropic-style', - - }, - "claude-3-5-haiku-20241022": { - contextWindow: 200_000, - cost: { input: 0.80, cache_read: 0.08, cache_write: 1.00, output: 4.00 }, - supportsFIM: false, - supportsSystemMessage: 'system-role', - supportsTools: 'anthropic-style', - }, - "claude-3-opus-20240229": { - contextWindow: 200_000, - cost: { input: 15.00, cache_read: 1.50, cache_write: 18.75, output: 75.00 }, - supportsFIM: false, - supportsSystemMessage: 'system-role', - supportsTools: 'anthropic-style', - }, - "claude-3-sonnet-20240229": { - contextWindow: 200_000, cost: { input: 3.00, output: 15.00 }, - supportsFIM: false, - supportsSystemMessage: 'system-role', - supportsTools: 'anthropic-style', + }, + openRouter: { + providerReasoningOptions: { + // reasoning: OAICompat + response.choices[0].delta.reasoning : payload should have {include_reasoning: true} https://openrouter.ai/announcements/reasoning-tokens-for-thinking-models + input: { includeInPayload: { include_reasoning: true } }, + output: { nameOfFieldInDelta: 'reasoning' }, } - } -} - - - -const grokProviderSettings: ProviderSettings = { - thinkingFormat: '', - - toolsFormat: '', - - FIMFormat: '', - - modelOptions: { - "grok-2-latest": { - contextWindow: 131_072, - cost: { input: 2.00, output: 10.00 }, - supportsFIM: false, - supportsSystemMessage: 'system-role', - supportsTools: 'openai-style', + }, + vLLM: { + providerReasoningOptions: { + // reasoning: OAICompat + response.choices[0].delta.reasoning_content // https://docs.vllm.ai/en/stable/features/reasoning_outputs.html#streaming-chat-completions + output: { nameOfFieldInDelta: 'reasoning_content' }, + } + }, + deepseek: { + providerReasoningOptions: { + // reasoning: OAICompat + response.choices[0].delta.reasoning_content // https://api-docs.deepseek.com/guides/reasoning_model + output: { nameOfFieldInDelta: 'reasoning_content' }, }, - } + }, + ollama: { + providerReasoningOptions: { + // reasoning: we need to filter out reasoning tags manually + output: { needsManualParse: true }, + }, + }, + openAICompatible: { + }, + mistral: { + }, + groq: { + }, + + + +} as const satisfies ModelSettingsOfProvider + + +const modelOptionsOfProvider = (providerName: ProviderName, modelName: string) => { + const { modelOptions, modelOptionsFallback } = modelSettingsOfProvider[providerName] + if (modelName in modelOptions) return modelOptions[modelName] + return modelOptionsFallback(modelName) } - - type InternalCommonMessageParams = { aiInstructions: string; onText: OnText; @@ -234,30 +320,94 @@ const toolCallsFrom_OpenAICompat = (toolCallOfIndex: ToolCallOfIndex) => { } -const newOpenAICompatibleSDK = ({ settingsOfProvider, providerName }: { settingsOfProvider: SettingsOfProvider, providerName: ProviderName }) => { +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, dangerouslyAllowBrowser: true, }) + return new OpenAI({ apiKey: thisConfig.apiKey, ...commonPayloadOpts }) } else if (providerName === 'ollama') { const thisConfig = settingsOfProvider[providerName] - return new OpenAI({ baseURL: `${thisConfig.endpoint}/v1`, apiKey: 'noop', dangerouslyAllowBrowser: true, }) + 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', dangerouslyAllowBrowser: true, }) + return new OpenAI({ baseURL: `${thisConfig.endpoint}/v1`, apiKey: 'noop', ...commonPayloadOpts }) } - else throw new Error(`Invalid providerName ${providerName}`) + 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 === 'mistral') { + const thisConfig = settingsOfProvider[providerName] + return new OpenAI({ baseURL: 'https://api.mistral.ai/v1', 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}.`) } -export const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions, tools: tools_ }: SendChatParams_Internal) => { - const { messages } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage: '', supportsTools: '', }) + + +const manualParseOnText = ( + providerName: ProviderName, + modelName: string, + onText_: OnText +): OnText => { + return onText_ +} + + +const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions, tools: tools_ }: SendChatParams_Internal) => { + const { + supportsReasoning: modelSupportsReasoning, + supportsSystemMessage, + supportsTools, + } = modelOptionsOfProvider(providerName, modelName) + + const { messages } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsTools, }) const tools = (supportsTools && ((tools_?.length ?? 0) !== 0)) ? tools_?.map(tool => toOpenAICompatibleTool(tool)) : undefined + const includeInPayload = modelSupportsReasoning ? {} : modelSettingsOfProvider[providerName].providerReasoningOptions?.input?.includeInPayload || {} + const toolsObj = tools ? { tools: tools, tool_choice: 'auto', parallel_tool_calls: false, } as const : {} - const openai: OpenAI = newOpenAICompatibleSDK({ providerName, settingsOfProvider }) + const openai: OpenAI = newOpenAICompatibleSDK({ providerName, settingsOfProvider, includeInPayload }) const options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = { model: modelName, messages: messages, stream: true, ...toolsObj } + const { nameOfFieldInDelta: nameOfReasoningFieldInDelta, needsManualParse: needsManualReasoningParse } = modelSettingsOfProvider[providerName].providerReasoningOptions?.output ?? {} + if (needsManualReasoningParse) onText = manualParseOnText(providerName, modelName, onText) + + let fullReasoning = '' let fullText = '' const toolCallOfIndex: ToolCallOfIndex = {} openai.chat.completions @@ -275,10 +425,18 @@ export const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinal toolCallOfIndex[index].id = tool.id ?? '' } // message - let newText = '' - newText += chunk.choices[0]?.delta?.content ?? '' + const newText = chunk.choices[0]?.delta?.content ?? '' fullText += newText - onText({ newText, fullText }) + + // reasoning + let newReasoning = '' + if (nameOfReasoningFieldInDelta) { + // @ts-ignore + newReasoning = (chunk.choices[0]?.delta?.[nameOfFieldInDelta] || '') + '' + fullReasoning += newReasoning + } + + onText({ newText, fullText, newReasoning, fullReasoning }) } onFinalMessage({ fullText, toolCalls: toolCallsFrom_OpenAICompat(toolCallOfIndex) }); }) @@ -290,7 +448,7 @@ export const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinal } -export const _openaiCompatibleList = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider, providerName }: ListParams_Internal) => { +const _openaiCompatibleList = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider, providerName }: ListParams_Internal) => { const onSuccess = ({ models }: { models: OpenAIModel[] }) => { onSuccess_({ models }) } @@ -318,8 +476,9 @@ export const _openaiCompatibleList = async ({ onSuccess: onSuccess_, onError: on } + // ------------ OPENAI ------------ -export const sendOpenAIChat = (params: SendChatParams_Internal) => { +const sendOpenAIChat = (params: SendChatParams_Internal) => { return _sendOpenAICompatibleChat(params) } @@ -345,25 +504,31 @@ const toolCallsFromAnthropicContent = (content: Anthropic.Messages.ContentBlock[ }).filter(t => !!t) } -export const sendAnthropicChat = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, aiInstructions, tools: tools_ }: SendChatParams_Internal) => { - const { messages, separateSystemMessageStr } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage: 'separated', supportsTools: 'anthropic-style', }) +const sendAnthropicChat = ({ messages: messages_, onText, providerName, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, aiInstructions, tools: tools_ }: SendChatParams_Internal) => { + const { + // supportsReasoning: modelSupportsReasoning, + supportsSystemMessage, + supportsTools, + contextWindow, + } = modelOptionsOfProvider(providerName, modelName) + + const { messages, separateSystemMessageStr } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsTools, }) 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 maxTokens = ; const stream = anthropic.messages.stream({ system: separateSystemMessageStr, messages: messages, model: modelName, - max_tokens: maxTokens, + max_tokens: contextWindow, tools: tools, tool_choice: tools ? { type: 'auto', disable_parallel_tool_use: true } : undefined // one tool use at a time }) // when receive text stream.on('text', (newText, fullText) => { - onText({ newText, fullText }) + onText({ newText, fullText, newReasoning: '', fullReasoning: '' }) }) // when we get the final message on this stream (or when error/fail) stream.on('finalMessage', (response) => { @@ -377,7 +542,7 @@ export const sendAnthropicChat = ({ messages: messages_, onText, onFinalMessage, 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 } } = {} @@ -396,6 +561,16 @@ export const sendAnthropicChat = ({ messages: messages_, onText, onFinalMessage, // }) +// ------------ XAI ------------ +const sendXAIChat = (params: SendChatParams_Internal) => { + return _sendOpenAICompatibleChat(params) +} + +// ------------ GEMINI ------------ +const sendGeminiAPIChat = (params: SendChatParams_Internal) => { + return _sendOpenAICompatibleChat(params) +} + // ------------ 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 @@ -404,7 +579,7 @@ const newOllamaSDK = ({ endpoint }: { endpoint: string }) => { return ollama } -export const ollamaList = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider }: ListParams_Internal) => { +const ollamaList = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider }: ListParams_Internal) => { const onSuccess = ({ models }: { models: OllamaModelResponse[] }) => { onSuccess_({ models }) } @@ -428,7 +603,7 @@ export const ollamaList = async ({ onSuccess: onSuccess_, onError: onError_, set } } -export const sendOllamaFIM = ({ messages, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }: SendFIMParams_Internal) => { +const sendOllamaFIM = ({ messages, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }: SendFIMParams_Internal) => { const thisConfig = settingsOfProvider.ollama const ollama = newOllamaSDK({ endpoint: thisConfig.endpoint }) @@ -461,58 +636,141 @@ export const sendOllamaFIM = ({ messages, onFinalMessage, onError, settingsOfPro // ollama's implementation of openai-compatible SDK dumps all reasoning tokens out with message, and supports tools, so we can use it for chat! -export const sendOllamaChat = (params: SendChatParams_Internal) => { +const sendOllamaChat = (params: SendChatParams_Internal) => { return _sendOpenAICompatibleChat(params) - // TODO!!! filter out reasoning tags... -} - - - -// ------------ OPENROUTER ------------ -export const sendOpenRouterFIM = ({ messages, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }: SendFIMParams_Internal) => { - // TODO!!! -} - -export const sendOpenRouterChat = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions, tools: tools_ }: SendChatParams_Internal) => { - // reasoning: response.choices[0].delta.reasoning : payload should have {include_reasoning: true} https://openrouter.ai/announcements/reasoning-tokens-for-thinking-models - // } // ------------ OPENAI-COMPATIBLE ------------ -export const openAICompatibleList = async (params: ListParams_Internal) => { - return _openaiCompatibleList(params) -} - // TODO!!! FIM // using openai's SDK is not ideal (your implementation might not do tools, reasoning, FIM etc correctly), talk to us for a custom integration -export const sendOpenAICompatibleChat = (params: SendChatParams_Internal) => { +const sendOpenAICompatibleChat = (params: SendChatParams_Internal) => { return _sendOpenAICompatibleChat(params) } +// ------------ OPENROUTER ------------ +const sendOpenRouterChat = (params: SendChatParams_Internal) => { + _sendOpenAICompatibleChat(params) +} + // ------------ VLLM ------------ - -// TODO!!! FIM +const vLLMList = async (params: ListParams_Internal) => { + return _openaiCompatibleList(params) +} +const sendVLLMFIM = (params: SendFIMParams_Internal) => { + // TODO!!! +} // using openai's SDK is not ideal (your implementation might not do tools, reasoning, FIM etc correctly), talk to us for a custom integration -export const sendVLLMChat = (params: SendChatParams_Internal) => { +const sendVLLMChat = (params: SendChatParams_Internal) => { return _sendOpenAICompatibleChat(params) - // reasoning: response.choices[0].delta.reasoning_content // https://docs.vllm.ai/en/stable/features/reasoning_outputs.html#streaming-chat-completions } - // ------------ DEEPSEEK API ------------ -export const sendDeepSeekAPIChat = (params: SendChatParams_Internal) => { +const sendDeepSeekAPIChat = (params: SendChatParams_Internal) => { + return _sendOpenAICompatibleChat(params) +} + +// ------------ MISTRAL ------------ +const sendMistralAPIChat = (params: SendChatParams_Internal) => { + return _sendOpenAICompatibleChat(params) +} + +// ------------ GROQ ------------ +const sendGroqAPIChat = (params: SendChatParams_Internal) => { return _sendOpenAICompatibleChat(params) - // reasoning: response.choices[0].delta.reasoning_content // https://api-docs.deepseek.com/guides/reasoning_model } -// ------------ GEMINI ------------ -// ------------ MISTRAL ------------ -// ------------ GROQ ------------ -// ------------ GROK ------------ + + +/* +FIM: + +qwen2.5-coder https://ollama.com/library/qwen2.5-coder/blobs/e94a8ecb9327 +<|fim_prefix|>{{ .Prompt }}<|fim_suffix|>{{ .Suffix }}<|fim_middle|> + +codestral https://ollama.com/library/codestral/blobs/51707752a87c +[SUFFIX]{{ .Suffix }}[PREFIX] {{ .Prompt }} + +deepseek-coder-v2 https://ollama.com/library/deepseek-coder-v2/blobs/22091531faf0 +<|fim▁begin|>{{ .Prompt }}<|fim▁hole|>{{ .Suffix }}<|fim▁end|> + +starcoder2 https://ollama.com/library/starcoder2/blobs/3b190e68fefe + + +{{ .Prompt }}{{ .Suffix }} +<|end_of_text|> + +codegemma https://ollama.com/library/codegemma:2b/blobs/48d9a8140749 +<|fim_prefix|>{{ .Prompt }}<|fim_suffix|>{{ .Suffix }}<|fim_middle|> + +*/ +type CallFnOfProvider = { + [providerName in ProviderName]: { + sendChat: (params: SendChatParams_Internal) => void; + sendFIM: ((params: SendFIMParams_Internal) => void) | null; + list: ((params: ListParams_Internal) => void) | null; + } +} +export const sendLLMMessageToProviderImplementation = { + openAI: { + sendChat: sendOpenAIChat, + sendFIM: null, + list: null, + }, + anthropic: { + sendChat: sendAnthropicChat, + sendFIM: null, + list: null, + }, + xAI: { + sendChat: sendXAIChat, + sendFIM: null, + list: null, + }, + gemini: { + sendChat: sendGeminiAPIChat, + sendFIM: null, + list: null, + }, + ollama: { + sendChat: sendOllamaChat, + sendFIM: sendOllamaFIM, + list: ollamaList, + }, + openAICompatible: { + sendChat: sendOpenAICompatibleChat, + sendFIM: null, + list: null, + }, + openRouter: { + sendChat: sendOpenRouterChat, + sendFIM: null, + list: null, + }, + vLLM: { + sendChat: sendVLLMChat, + sendFIM: sendVLLMFIM, + list: vLLMList, + }, + deepseek: { + sendChat: sendDeepSeekAPIChat, + sendFIM: null, + list: null, + }, + groq: { + sendChat: sendGroqAPIChat, + sendFIM: null, + list: null, + }, + mistral: { + sendChat: sendMistralAPIChat, + sendFIM: null, + list: null, + }, +} satisfies CallFnOfProvider diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts index 3cef5327..1d388338 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts @@ -266,6 +266,31 @@ const prepareMessages_tools = ({ messages, supportsTools }: { messages: LLMChatM +/* +Gemini has this, but they're openai-compat so we don't need to implement this +gemini request: +{ "role": "assistant", + "content": null, + "function_call": { + "name": "get_weather", + "arguments": { + "latitude": 48.8566, + "longitude": 2.3522 + } + } +} + +gemini response: +{ "role": "assistant", + "function_response": { + "name": "get_weather", + "response": { + "temperature": "15°C", + "condition": "Cloudy" + } + } +} +*/ @@ -297,29 +322,3 @@ export const prepareMessages = ({ } as const } - -/* -ACCORDING TO 4o: gemini: similar to openai, but function_call, and only 1 call per message (no id because only 1 message) -gemini request: -{ "role": "assistant", - "content": null, - "function_call": { - "name": "get_weather", - "arguments": { - "latitude": 48.8566, - "longitude": 2.3522 - } - } -} - -gemini response: -{ "role": "assistant", - "function_response": { - "name": "get_weather", - "response": { - "temperature": "15°C", - "condition": "Cloudy" - } - } -} -*/ diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts index 1c9ac21d..90deffe2 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts @@ -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 { sendAnthropicChat, sendOpenAIChat } from './MODELS.js'; +import { sendLLMMessageToProviderImplementation } from './MODELS.js'; export const sendLLMMessage = ({ @@ -56,9 +56,10 @@ export const sendLLMMessage = ({ let _setAborter = (fn: () => void) => { _aborter = fn } let _didAbort = false - const onText: OnText = ({ newText, fullText }) => { + const onText: OnText = (params) => { + const { fullText } = params if (_didAbort) return - onText_({ newText, fullText }) + onText_(params) _fullTextSoFar = fullText } @@ -93,29 +94,27 @@ export const sendLLMMessage = ({ else if (messagesType === 'FIMMessage') captureLLMEvent(`${loggingName} - Sending FIM`, {}) // TODO!!! add more metrics + try { - switch (providerName) { - case 'anthropic': - if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - Anthropic FIM' }) - else /* */ sendAnthropicChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions, tools }); - break; - case 'openAI': - case 'openRouter': - case 'deepseek': - case 'openAICompatible': - case 'mistral': - case 'ollama': - case 'vLLM': - case 'groq': - case 'gemini': - case 'xAI': - if (messagesType === 'FIMMessage') onFinalMessage({ fullText: 'TODO - OpenAI API FIM' }) - else /* */ sendOpenAIChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions, tools }); - break; - default: - onError({ message: `Error: Void provider was "${providerName}", which is not recognized.`, fullError: null }) - break; + const implementation = sendLLMMessageToProviderImplementation[providerName] + if (!implementation) { + onError({ message: `Error: Provider "${providerName}" not recognized.`, fullError: null }) + return } + const { sendFIM, sendChat } = implementation + if (messagesType === 'chatMessages') { + sendChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions, tools }) + return + } + if (messagesType === 'FIMMessage') { + if (sendFIM) { + sendFIM({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions }) + return + } + onError({ message: `Error: This provider does not support Autocomplete yet.`, fullError: null }) + return + } + onError({ message: `Error: Message type "${messagesType}" not recognized.`, fullError: null }) } catch (error) { diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessageChannel.ts b/src/vs/workbench/contrib/void/electron-main/llmMessageChannel.ts index 929c85e4..d2bceb4c 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessageChannel.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessageChannel.ts @@ -8,29 +8,42 @@ import { IServerChannel } from '../../../../base/parts/ipc/common/ipc.js'; import { Emitter, Event } from '../../../../base/common/event.js'; -import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, MainSendLLMMessageParams, AbortRef, SendLLMMessageParams, MainLLMMessageAbortParams, MainModelListParams, ModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, OllamaModelResponse, OpenaiCompatibleModelResponse, } from '../common/llmMessageTypes.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 { ollamaList } from './llmMessage/MODELS.js'; +import { sendLLMMessageToProviderImplementation } from './llmMessage/MODELS.js'; // NODE IMPLEMENTATION - calls actual sendLLMMessage() and returns listeners to it export class LLMMessageChannel implements IServerChannel { + // sendLLMMessage - private readonly _onText_llm = new Emitter(); - private readonly _onFinalMessage_llm = new Emitter(); - private readonly _onError_llm = new Emitter(); + private readonly llmMessageEmitters = { + onText: new Emitter(), + onFinalMessage: new Emitter(), + onError: new Emitter(), + } - // abort - private readonly _abortRefOfRequestId_llm: Record = {} + // aborters for above + private readonly abortRefOfRequestId: Record = {} - // ollamaList - private readonly _onSuccess_ollama = new Emitter>(); - private readonly _onError_ollama = new Emitter>(); - // openaiCompatibleList - private readonly _onSuccess_openAICompatible = new Emitter>(); - private readonly _onError_openAICompatible = new Emitter>(); + // list + private readonly listEmitters = { + ollama: { + success: new Emitter>(), + error: new Emitter>(), + }, + vLLM: { + success: new Emitter>(), + error: new Emitter>(), + } + } satisfies { + [providerName: string]: { + success: Emitter>, + error: Emitter>, + } + } // stupidly, channels can't take in @IService constructor( @@ -39,30 +52,17 @@ export class LLMMessageChannel implements IServerChannel { // browser uses this to listen for changes listen(_: unknown, event: string): Event { - if (event === 'onText_llm') { - return this._onText_llm.event; - } - else if (event === 'onFinalMessage_llm') { - return this._onFinalMessage_llm.event; - } - else if (event === 'onError_llm') { - return this._onError_llm.event; - } - else if (event === 'onSuccess_ollama') { - return this._onSuccess_ollama.event; - } - else if (event === 'onError_ollama') { - return this._onError_ollama.event; - } - else if (event === 'onSuccess_openAICompatible') { - return this._onSuccess_openAICompatible.event; - } - else if (event === 'onError_openAICompatible') { - return this._onError_openAICompatible.event; - } - else { - throw new Error(`Event not found: ${event}`); - } + // text + if (event === 'onText_sendLLMMessage') return this.llmMessageEmitters.onText.event; + else if (event === 'onFinalMessage_sendLLMMessage') return this.llmMessageEmitters.onFinalMessage.event; + else if (event === 'onError_sendLLMMessage') return this.llmMessageEmitters.onError.event; + // list + else if (event === 'onSuccess_list_ollama') return this.listEmitters.ollama.success.event; + else if (event === 'onError_list_ollama') return this.listEmitters.ollama.error.event; + else if (event === 'onSuccess_list_vLLM') return this.listEmitters.vLLM.success.event; + else if (event === 'onError_list_vLLM') return this.listEmitters.vLLM.error.event; + + else throw new Error(`Event not found: ${event}`); } // browser uses this to call (see this.channel.call() in llmMessageService.ts for all usages) @@ -77,8 +77,8 @@ export class LLMMessageChannel implements IServerChannel { else if (command === 'ollamaList') { this._callOllamaList(params) } - else if (command === 'openAICompatibleList') { - this._callOpenAICompatibleList(params) + else if (command === 'vLLMList') { + this._callVLLMList(params) } else { throw new Error(`Void sendLLM: command "${command}" not recognized.`) @@ -93,47 +93,50 @@ export class LLMMessageChannel implements IServerChannel { private async _callSendLLMMessage(params: MainSendLLMMessageParams) { const { requestId } = params; - if (!(requestId in this._abortRefOfRequestId_llm)) - this._abortRefOfRequestId_llm[requestId] = { current: null } + if (!(requestId in this.abortRefOfRequestId)) + this.abortRefOfRequestId[requestId] = { current: null } const mainThreadParams: SendLLMMessageParams = { ...params, - onText: ({ newText, fullText }) => { this._onText_llm.fire({ requestId, newText, fullText }); }, - onFinalMessage: ({ fullText, toolCalls }) => { this._onFinalMessage_llm.fire({ requestId, fullText, toolCalls }); }, - onError: ({ message: error, fullError }) => { console.log('sendLLM: firing err'); this._onError_llm.fire({ requestId, message: error, fullError }); }, - abortRef: this._abortRefOfRequestId_llm[requestId], + onText: (p) => { this.llmMessageEmitters.onText.fire({ requestId, ...p }); }, + onFinalMessage: (p) => { this.llmMessageEmitters.onFinalMessage.fire({ requestId, ...p }); }, + onError: (p) => { console.log('sendLLM: firing err'); this.llmMessageEmitters.onError.fire({ requestId, ...p }); }, + abortRef: this.abortRefOfRequestId[requestId], } sendLLMMessage(mainThreadParams, this.metricsService); } - private _callAbort(params: MainLLMMessageAbortParams) { - const { requestId } = params; - if (!(requestId in this._abortRefOfRequestId_llm)) return - this._abortRefOfRequestId_llm[requestId].current?.() - delete this._abortRefOfRequestId_llm[requestId] - } - - private _callOllamaList(params: MainModelListParams) { - const { requestId } = params; - + _callOllamaList = (params: MainModelListParams) => { + const { requestId } = params + const emitters = this.listEmitters.ollama const mainThreadParams: ModelListParams = { ...params, - onSuccess: ({ models }) => { this._onSuccess_ollama.fire({ requestId, models }); }, - onError: ({ error }) => { this._onError_ollama.fire({ requestId, error }); }, + onSuccess: (p) => { emitters.success.fire({ requestId, ...p }); }, + onError: (p) => { emitters.error.fire({ requestId, ...p }); }, } - ollamaList(mainThreadParams) + sendLLMMessageToProviderImplementation.ollama.list(mainThreadParams) } - private _callOpenAICompatibleList(params: MainModelListParams) { - const { requestId } = params; - - const mainThreadParams: ModelListParams = { + _callVLLMList = (params: MainModelListParams) => { + const { requestId } = params + const emitters = this.listEmitters.vLLM + const mainThreadParams: ModelListParams = { ...params, - onSuccess: ({ models }) => { this._onSuccess_openAICompatible.fire({ requestId, models }); }, - onError: ({ error }) => { this._onError_openAICompatible.fire({ requestId, error }); }, + onSuccess: (p) => { emitters.success.fire({ requestId, ...p }); }, + onError: (p) => { emitters.error.fire({ requestId, ...p }); }, } - openaiCompatibleList(mainThreadParams) + sendLLMMessageToProviderImplementation.vLLM.list(mainThreadParams) } + + + + private _callAbort(params: MainLLMMessageAbortParams) { + const { requestId } = params; + if (!(requestId in this.abortRefOfRequestId)) return + this.abortRefOfRequestId[requestId].current?.() + delete this.abortRefOfRequestId[requestId] + } + } From 3ae8f756410f5cc56054f196b9e2193f1e616425 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Mon, 24 Feb 2025 04:01:00 -0800 Subject: [PATCH 37/41] remove mistral, finish(?) models! --- package-lock.json | 13 +- package.json | 2 +- .../browser/helpers/extractCodeFromResult.ts | 94 +++ .../react/src/void-settings-tsx/Settings.tsx | 2 +- .../void/common/voidSettingsService.ts | 39 +- .../contrib/void/common/voidSettingsTypes.ts | 88 +- .../void/electron-main/llmMessage/MODELS.ts | 755 +++++++++++------- .../llmMessage/preprocessLLMMessages.ts | 26 +- 8 files changed, 686 insertions(+), 333 deletions(-) diff --git a/package-lock.json b/package-lock.json index bc045891..d29248c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -56,7 +56,7 @@ "node-pty": "1.1.0-beta21", "ollama": "^0.5.11", "open": "^8.4.2", - "openai": "^4.76.1", + "openai": "^4.85.4", "posthog-node": "^4.3.1", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -17079,9 +17079,10 @@ } }, "node_modules/openai": { - "version": "4.77.0", - "resolved": "https://registry.npmjs.org/openai/-/openai-4.77.0.tgz", - "integrity": "sha512-WWacavtns/7pCUkOWvQIjyOfcdr9X+9n9Vvb0zFeKVDAqwCMDHB+iSr24SVaBAhplvSG6JrRXFpcNM9gWhOGIw==", + "version": "4.85.4", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.85.4.tgz", + "integrity": "sha512-Nki51PBSu+Aryo7WKbdXvfm0X/iKkQS2fq3O0Uqb/O3b4exOZFid2te1BZ52bbO5UwxQZ5eeHJDCTqtrJLPw0w==", + "license": "Apache-2.0", "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", @@ -17095,9 +17096,13 @@ "openai": "bin/cli" }, "peerDependencies": { + "ws": "^8.18.0", "zod": "^3.23.8" }, "peerDependenciesMeta": { + "ws": { + "optional": true + }, "zod": { "optional": true } diff --git a/package.json b/package.json index a4ee38bb..b99ca9dc 100644 --- a/package.json +++ b/package.json @@ -124,7 +124,7 @@ "node-pty": "1.1.0-beta21", "ollama": "^0.5.11", "open": "^8.4.2", - "openai": "^4.76.1", + "openai": "^4.85.4", "posthog-node": "^4.3.1", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts b/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts index 806676da..297d82b6 100644 --- a/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts +++ b/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts @@ -3,6 +3,7 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ +import { OnText } from '../../common/llmMessageTypes.js' import { DIVIDER, FINAL, ORIGINAL } from '../prompt/prompts.js' class SurroundingsRemover { @@ -240,3 +241,96 @@ export const extractSearchReplaceBlocks = (str: string) => { }) } } + + + + + + + + + + +export const extractReasoningFromText = ( + onText_: OnText, + thinkTags: [string, string], +): OnText => { + + let latestAddIdx = 0 // exclusive + let foundTag1 = false + let foundTag2 = false + + let fullText = '' + let fullReasoning = '' + + const onText: OnText = ({ newText: newText_, fullText: fullText_ }) => { + // abcdefghi + // | + // until found the first think tag, keep adding to fullText + if (!foundTag1) { + const endsWithTag1 = endsWithAnyPrefixOf(fullText_, thinkTags[0]) + if (endsWithTag1) { + // wait until we get the full tag or know more + return + } + // if found the first tag + const tag1Index = fullText_.lastIndexOf(thinkTags[0]) + if (tag1Index !== -1) { + foundTag1 = true + const newText = fullText.substring(latestAddIdx, tag1Index) + const newReasoning = fullText.substring(tag1Index + thinkTags[0].length, Infinity) + + fullText += newText + fullReasoning += newReasoning + latestAddIdx += newText.length + newReasoning.length + onText_({ newText, fullText, newReasoning: newReasoning, fullReasoning }) + return + } + + // add the text to fullText + const newText = fullText.substring(latestAddIdx, Infinity) + fullText += newText + latestAddIdx += newText.length + onText_({ newText, fullText, newReasoning: '', fullReasoning }) + return + } + // at this point, we found + + // until found the second think tag, keep adding to fullReasoning + if (!foundTag2) { + const endsWithTag2 = endsWithAnyPrefixOf(fullText_, thinkTags[1]) + if (endsWithTag2) { + // wait until we get the full tag or know more + return + } + // if found the second tag + const tag2Index = fullText_.lastIndexOf(thinkTags[1]) + if (tag2Index !== -1) { + foundTag2 = true + const newReasoning = fullText.substring(latestAddIdx, tag2Index) + const newText = fullText.substring(tag2Index + thinkTags[1].length, Infinity) + + fullText += newText + fullReasoning += newReasoning + latestAddIdx += newText.length + newReasoning.length + onText_({ newText, fullText, newReasoning: newReasoning, fullReasoning }) + return + } + + // add the text to fullReasoning + const newReasoning = fullText.substring(latestAddIdx, Infinity) + fullReasoning += newReasoning + latestAddIdx += newReasoning.length + onText_({ newText: '', fullText, newReasoning, fullReasoning }) + return + } + // at this point, we found + + fullText += newText_ + const newText = fullText.substring(latestAddIdx, Infinity) + latestAddIdx += newText.length + onText_({ newText, fullText, newReasoning: '', fullReasoning }) + } + + return onText +} diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx index a6aec380..e2056fbf 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx @@ -415,7 +415,7 @@ export const FeaturesTab = () => {
    - + {/* TODO we should create UI for downloading models without user going into terminal */} diff --git a/src/vs/workbench/contrib/void/common/voidSettingsService.ts b/src/vs/workbench/contrib/void/common/voidSettingsService.ts index 7a35c678..be3f6689 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsService.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsService.ts @@ -11,7 +11,7 @@ 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, developerInfoOfModelName, modelInfoOfAutodetectedModelNames } from './voidSettingsTypes.js'; +import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, VoidModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, defaultProviderSettings } from './voidSettingsTypes.js'; const STORAGE_KEY = 'void.settingsServiceStorage' @@ -32,8 +32,6 @@ type SetGlobalSettingFn = (settingName: T, newVal export type ModelOption = { name: string, selection: ModelSelection } - - export type VoidSettingsState = { readonly settingsOfProvider: SettingsOfProvider; // optionsOfProvider readonly modelSelectionOfFeature: ModelSelectionOfFeature; // stateOfFeature @@ -172,9 +170,6 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { // A HACK BECAUSE WE ADDED DEEPSEEK (did not exist before, comes before readS) ...{ deepseek: defaultSettingsOfProvider.deepseek }, - // A HACK BECAUSE WE ADDED MISTRAL (did not exist before, comes before readS) - ...{ mistral: defaultSettingsOfProvider.mistral }, - // A HACK BECAUSE WE ADDED XAI (did not exist before, comes before readS) ...{ xAI: defaultSettingsOfProvider.xAI }, @@ -295,19 +290,35 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { } + private _updatedModelsAfterAutodetection = (defaultModelNames: string[], options: { existingModels: VoidModelInfo[] }) => { + const { existingModels } = options + + const existingModelsMap: Record = {} + for (const existingModel of existingModels) { + existingModelsMap[existingModel.modelName] = existingModel + } + + const newDefaultModels = defaultModelNames.map((modelName, i) => ({ + modelName, + isDefault: true, + isAutodetected: true, + isHidden: !!existingModelsMap[modelName]?.isHidden, + })) + + return [ + ...newDefaultModels, // swap out all the default models for the new default models + ...existingModels.filter(m => !m.isDefault), // keep any non-default (custom) models + ] + } + + setAutodetectedModels(providerName: ProviderName, autodetectedModelNames: string[], logging: object) { const { models } = this.state.settingsOfProvider[providerName] const oldModelNames = models.map(m => m.modelName) - - const newDefaultModels = modelInfoOfAutodetectedModelNames(autodetectedModelNames, { existingModels: models }) - const newModels = [ - ...newDefaultModels, // swap out all the default models for the new default models - ...models.filter(m => !m.isDefault), // keep any non-default (custom) models - ] - + const newModels = this._updatedModelsAfterAutodetection(autodetectedModelNames, { existingModels: models }) this.setSettingOfProvider(providerName, 'models', newModels) // if the models changed, log it @@ -341,7 +352,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { if (existingIdx !== -1) return // if exists, do nothing const newModels = [ ...models, - { ...developerInfoOfModelName(modelName), modelName, isDefault: false, isHidden: false } + { modelName, isDefault: false, isHidden: false } ] this.setSettingOfProvider(providerName, 'models', newModels) diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index fb387bc1..4111b53b 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -4,7 +4,6 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import { defaultModelsOfProvider } from '../electron-main/llmMessage/MODELS.js'; import { VoidSettingsState } from './voidSettingsService.js' @@ -40,14 +39,70 @@ export const defaultProviderSettings = { groq: { apiKey: '', }, - mistral: { - apiKey: '' - }, xAI: { apiKey: '' }, } as const + + + +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-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/qwen2.5-vl-72b-instruct:free', + ], + 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 + + + + export type ProviderName = keyof typeof defaultProviderSettings export const providerNames = Object.keys(defaultProviderSettings) as ProviderName[] @@ -139,11 +194,6 @@ export const displayInfoOfProviderName = (providerName: ProviderName): DisplayIn title: 'Groq.com API', } } - else if (providerName === 'mistral') { - return { - title: 'Mistral API', - } - } else if (providerName === 'xAI') { return { title: 'xAI API', @@ -173,10 +223,9 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName providerName === 'openRouter' ? 'sk-or-key...' : // sk-or-v1-key providerName === 'gemini' ? 'key...' : providerName === 'groq' ? 'gsk_key...' : - providerName === 'mistral' ? 'key...' : - providerName === 'openAICompatible' ? 'sk-key...' : - providerName === 'xAI' ? 'xai-key...' : - '', + providerName === 'openAICompatible' ? 'sk-key...' : + providerName === 'xAI' ? 'xai-key...' : + '', subTextMd: providerName === 'anthropic' ? 'Get your [API Key here](https://console.anthropic.com/settings/keys).' : providerName === 'openAI' ? 'Get your [API Key here](https://platform.openai.com/api-keys).' : @@ -184,10 +233,9 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName providerName === 'openRouter' ? 'Get your [API Key here](https://openrouter.ai/settings/keys).' : providerName === 'gemini' ? 'Get your [API Key here](https://aistudio.google.com/apikey).' : providerName === 'groq' ? 'Get your [API Key here](https://console.groq.com/keys).' : - providerName === 'mistral' ? 'Get your [API Key here](https://console.mistral.ai/api-keys/).' : - providerName === 'xAI' ? 'Get your [API Key here](https://console.x.ai).' : - providerName === 'openAICompatible' ? undefined : - '', + providerName === 'xAI' ? 'Get your [API Key here](https://console.x.ai).' : + providerName === 'openAICompatible' ? undefined : + '', isPasswordField: true, } } @@ -271,12 +319,6 @@ export const defaultSettingsOfProvider: SettingsOfProvider = { ...modelInfoOfDefaultModelNames(defaultModelsOfProvider.gemini), _didFillInProviderSettings: undefined, }, - mistral: { - ...defaultCustomSettings, - ...defaultProviderSettings.mistral, - ...modelInfoOfDefaultModelNames(defaultModelsOfProvider.mistral), - _didFillInProviderSettings: undefined, - }, xAI: { ...defaultCustomSettings, ...defaultProviderSettings.xAI, diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts index ce0d0537..d68408cd 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts @@ -4,67 +4,15 @@ *--------------------------------------------------------------------------------------*/ import OpenAI, { ClientOptions } from 'openai'; -import { Model as OpenAIModel } from 'openai/resources/models.js'; -import { OllamaModelResponse, OnText, OnFinalMessage, OnError, LLMChatMessage, LLMFIMMessage, ModelListParams } from '../../common/llmMessageTypes.js'; -import { InternalToolInfo, isAToolName } from '../../common/toolsService.js'; -import { defaultProviderSettings, displayInfoOfProviderName, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js'; -import { prepareMessages } from './preprocessLLMMessages.js'; import Anthropic from '@anthropic-ai/sdk'; import { Ollama } from 'ollama'; - - -export const defaultModelsOfProvider = { - anthropic: [ // https://docs.anthropic.com/en/docs/about-claude/models - 'claude-3-5-sonnet-latest', - 'claude-3-5-haiku-latest', - 'claude-3-opus-latest', - ], - openAI: [ // https://platform.openai.com/docs/models/gp - 'o1', - 'o1-mini', - 'o3-mini', - 'gpt-4o', - 'gpt-4o-mini', - ], - deepseek: [ // https://platform.openai.com/docs/models/gp - 'deepseek-chat', - 'deepseek-reasoner', - ], - ollama: [], - vLLM: [], - openRouter: [], - openAICompatible: [], - gemini: [ - 'gemini-1.5-flash', - 'gemini-1.5-pro', - 'gemini-1.5-flash-8b', - 'gemini-2.0-flash-exp', - 'gemini-2.0-flash-thinking-exp-1219', - 'learnlm-1.5-pro-experimental' - ], - groq: [ // https://console.groq.com/docs/models - "llama3-70b-8192", - "llama-3.3-70b-versatile", - "llama-3.1-8b-instant", - "gemma2-9b-it", - "mixtral-8x7b-32768" - ], - mistral: [ // https://docs.mistral.ai/getting-started/models/models_overview/ - "codestral-latest", - "open-codestral-mamba", - "open-mistral-nemo", - "mistral-large-latest", - "pixtral-large-latest", - "ministral-3b-latest", - "ministral-8b-latest", - "mistral-small-latest", - ], - xAI: [ // https://docs.x.ai/docs/models?cluster=us-east-1 - 'grok-3-latest', - 'grok-2-latest', - ], -} satisfies Record +import { Model as OpenAIModel } from 'openai/resources/models.js'; +import { OllamaModelResponse, OnText, OnFinalMessage, OnError, LLMChatMessage, LLMFIMMessage, ModelListParams } from '../../common/llmMessageTypes.js'; +import { InternalToolInfo, isAToolName } from '../../common/toolsService.js'; +import { defaultProviderSettings, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js'; +import { prepareFIMMessage, prepareMessages } from './preprocessLLMMessages.js'; +import { extractReasoningFromText } from '../../browser/helpers/extractCodeFromResult.js'; @@ -78,10 +26,13 @@ type ModelOptions = { } supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated'; supportsTools: false | 'anthropic-style' | 'openai-style'; - supportsFIM: false | 'TODO_FIM_FORMAT'; + supportsFIM: boolean; - supportsReasoning: boolean; // not whether it reasons, but whether it outputs reasoning tokens - manualMatchReasoningTokens?: [string, string]; // reasoning tokens if it's an OSS model + supportsReasoningOutput: false | { + // 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 + openSourceThinkTags?: [string, string] + }; } type ProviderReasoningOptions = { @@ -95,9 +46,9 @@ type ProviderReasoningOptions = { } type ProviderSettings = { - providerReasoningOptions?: ProviderReasoningOptions; + ifSupportsReasoningOutput?: ProviderReasoningOptions; modelOptions: { [key: string]: ModelOptions }; - modelOptionsFallback: (modelName: string) => ModelOptions; // allowed to throw error if modeName is totally invalid + modelOptionsFallback: (modelName: string) => (ModelOptions & { modelName: string }) | null; } @@ -107,172 +58,446 @@ type ModelSettingsOfProvider = { +// type DefaultModels = typeof defaultModelsOfProvider[T][number] +// type AssertModelsIncluded< +// T extends ProviderName, +// Options extends Record +// > = Exclude, keyof Options> extends never +// ? true +// : ["Missing models for", T, Exclude, keyof Options>]; +// const assertOpenAI: AssertModelsIncluded<'openAI', typeof openAIModelOptions> = true; -const modelNotRecognizedErrorMessage = (modelName: string, providerName: ProviderName) => `Void could not find a model matching ${modelName} for ${displayInfoOfProviderName(providerName).title}.` - +const modelOptionDefaults: ModelOptions = { + contextWindow: 32_000, + cost: { input: 0, output: 0 }, + supportsSystemMessage: false, + supportsTools: false, + supportsFIM: false, + supportsReasoningOutput: false, +} // ---------------- OPENAI ---------------- -const openAIModelOptions = { - "o1": { +const openAIModelOptions = { // https://platform.openai.com/docs/pricing + 'o1': { contextWindow: 128_000, cost: { input: 15.00, cache_read: 7.50, output: 60.00, }, supportsFIM: false, supportsTools: false, supportsSystemMessage: 'developer-role', - supportsReasoning: false, + supportsReasoningOutput: false, }, - "o3-mini": { + 'o3-mini': { contextWindow: 200_000, cost: { input: 1.10, cache_read: 0.55, output: 4.40, }, supportsFIM: false, supportsTools: false, supportsSystemMessage: 'developer-role', - supportsReasoning: false, + supportsReasoningOutput: false, }, - "gpt-4o": { + 'gpt-4o': { contextWindow: 128_000, cost: { input: 2.50, cache_read: 1.25, output: 10.00, }, supportsFIM: false, supportsTools: 'openai-style', supportsSystemMessage: 'system-role', - supportsReasoning: false, + supportsReasoningOutput: false, }, -} as const + 'o1-mini': { + contextWindow: 128_000, + cost: { input: 1.10, cache_read: 0.55, output: 4.40, }, + supportsFIM: false, + supportsTools: false, + supportsSystemMessage: false, // does not support any system + supportsReasoningOutput: false, + }, + 'gpt-4o-mini': { + contextWindow: 128_000, + cost: { input: 0.15, cache_read: 0.075, output: 0.60, }, + supportsFIM: false, + supportsTools: 'openai-style', + supportsSystemMessage: 'system-role', // ?? + supportsReasoningOutput: false, + }, +} as const satisfies { [s: string]: ModelOptions } + const openAISettings: ProviderSettings = { modelOptions: openAIModelOptions, modelOptionsFallback: (modelName) => { - if (modelName.includes('o1')) return openAIModelOptions['o1'] - if (modelName.includes('o3-mini')) return openAIModelOptions['o3-mini'] - if (modelName.includes('gpt-4o')) return openAIModelOptions['gpt-4o'] - throw new Error(modelNotRecognizedErrorMessage(modelName, 'openAI')) + 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 } } // ---------------- ANTHROPIC ---------------- const anthropicModelOptions = { - "claude-3-5-sonnet-20241022": { + 'claude-3-5-sonnet-20241022': { contextWindow: 200_000, cost: { input: 3.00, cache_read: 0.30, cache_write: 3.75, output: 15.00 }, supportsFIM: false, supportsSystemMessage: 'separated', supportsTools: 'anthropic-style', - supportsReasoning: false, - + supportsReasoningOutput: false, }, - "claude-3-5-haiku-20241022": { + 'claude-3-5-haiku-20241022': { contextWindow: 200_000, cost: { input: 0.80, cache_read: 0.08, cache_write: 1.00, output: 4.00 }, supportsFIM: false, supportsSystemMessage: 'separated', supportsTools: 'anthropic-style', - supportsReasoning: false, + supportsReasoningOutput: false, }, - "claude-3-opus-20240229": { + 'claude-3-opus-20240229': { contextWindow: 200_000, cost: { input: 15.00, cache_read: 1.50, cache_write: 18.75, output: 75.00 }, supportsFIM: false, supportsSystemMessage: 'separated', supportsTools: 'anthropic-style', - supportsReasoning: false, + supportsReasoningOutput: false, }, - "claude-3-sonnet-20240229": { + '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 }, supportsFIM: false, supportsSystemMessage: 'separated', supportsTools: 'anthropic-style', - supportsReasoning: false, + supportsReasoningOutput: false, } -} as const +} as const satisfies { [s: string]: ModelOptions } const anthropicSettings: ProviderSettings = { modelOptions: anthropicModelOptions, modelOptionsFallback: (modelName) => { - throw new Error(modelNotRecognizedErrorMessage(modelName, 'anthropic')) + let fallbackName: keyof typeof anthropicModelOptions | null = null + 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 (fallbackName) return { modelName: fallbackName, ...anthropicModelOptions[fallbackName] } + return null } } // ---------------- XAI ---------------- -const XAIModelOptions = { - "grok-2-latest": { +const xAIModelOptions = { + 'grok-2-latest': { contextWindow: 131_072, cost: { input: 2.00, output: 10.00 }, supportsFIM: false, supportsSystemMessage: 'system-role', supportsTools: 'openai-style', - supportsReasoning: false, + supportsReasoningOutput: false, }, -} as const +} as const satisfies { [s: string]: ModelOptions } -const XAISettings: ProviderSettings = { - modelOptions: XAIModelOptions, +const xAISettings: ProviderSettings = { + modelOptions: xAIModelOptions, modelOptionsFallback: (modelName) => { - throw new Error(modelNotRecognizedErrorMessage(modelName, 'xAI')) + let fallbackName: keyof typeof xAIModelOptions | null = null + if (modelName.includes('grok-2')) fallbackName = 'grok-2-latest' + 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, + cost: { input: 0.10, output: 0.40 }, + supportsFIM: false, + supportsSystemMessage: 'system-role', + supportsTools: 'openai-style', // we are assuming OpenAI SDK when calling gemini + supportsReasoningOutput: false, + }, + 'gemini-2.0-flash-lite-preview-02-05': { + contextWindow: 1_048_576, + cost: { input: 0.075, output: 0.30 }, + supportsFIM: false, + supportsSystemMessage: 'system-role', + supportsTools: 'openai-style', + supportsReasoningOutput: false, + }, + 'gemini-1.5-flash': { + contextWindow: 1_048_576, + 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', + supportsReasoningOutput: false, + }, + 'gemini-1.5-pro': { + contextWindow: 2_097_152, + 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', + supportsReasoningOutput: false, + }, + 'gemini-1.5-flash-8b': { + contextWindow: 1_048_576, + 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', + supportsReasoningOutput: false, + }, +} as const satisfies { [s: string]: ModelOptions } + +const geminiSettings: ProviderSettings = { + modelOptions: geminiModelOptions, + modelOptionsFallback: (modelName) => { + return null + } +} + + +// ---------------- OPEN SOURCE MODELS ---------------- + +const openSourceModelDefaultOptionsAssumingOAICompat = { + 'deepseekR1': { + supportsFIM: false, + supportsSystemMessage: false, + supportsTools: false, + supportsReasoningOutput: { openSourceThinkTags: ['', ''] }, + }, + 'deepseekCoderV2': { + supportsFIM: false, + supportsSystemMessage: false, // unstable + supportsTools: false, + supportsReasoningOutput: false, + }, + 'codestral': { + supportsFIM: true, + supportsSystemMessage: 'system-role', + supportsTools: 'openai-style', + supportsReasoningOutput: false, + }, + // llama + 'llama3': { + supportsFIM: false, + supportsSystemMessage: 'system-role', + supportsTools: 'openai-style', + supportsReasoningOutput: false, + }, + 'llama3.1': { + supportsFIM: false, + supportsSystemMessage: 'system-role', + supportsTools: 'openai-style', + supportsReasoningOutput: false, + }, + 'llama3.2': { + supportsFIM: false, + supportsSystemMessage: 'system-role', + supportsTools: 'openai-style', + supportsReasoningOutput: false, + }, + 'llama3.3': { + supportsFIM: false, + supportsSystemMessage: 'system-role', + supportsTools: 'openai-style', + supportsReasoningOutput: false, + }, + 'qwen2.5coder': { + supportsFIM: true, + supportsSystemMessage: 'system-role', + supportsTools: 'openai-style', + supportsReasoningOutput: false, + }, + // FIM only + 'starcoder2': { + supportsFIM: true, + supportsSystemMessage: false, + supportsTools: false, + supportsReasoningOutput: false, + }, + 'codegemma:2b': { + supportsFIM: true, + supportsSystemMessage: false, + supportsTools: false, + supportsReasoningOutput: false, + }, +} as const satisfies { [s: string]: Partial } + + + +// ---------------- DEEPSEEK API ---------------- +const deepseekModelOptions = { + 'deepseek-chat': { + ...openSourceModelDefaultOptionsAssumingOAICompat.deepseekR1, + contextWindow: 64_000, // https://api-docs.deepseek.com/quick_start/pricing + cost: { cache_read: .07, input: .27, output: 1.10, }, + }, + 'deepseek-reasoner': { + ...openSourceModelDefaultOptionsAssumingOAICompat.deepseekCoderV2, + contextWindow: 64_000, + cost: { cache_read: .14, input: .55, output: 2.19, }, + }, +} as const satisfies { [s: string]: ModelOptions } + + +const deepseekSettings: ProviderSettings = { + modelOptions: deepseekModelOptions, + ifSupportsReasoningOutput: { + // 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 = { + 'llama-3.3-70b-versatile': { + contextWindow: 128_000, + cost: { input: 0.59, output: 0.79 }, + supportsFIM: false, + supportsSystemMessage: 'system-role', + supportsTools: 'openai-style', + supportsReasoningOutput: false, + }, + 'llama-3.1-8b-instant': { + contextWindow: 128_000, + cost: { input: 0.05, output: 0.08 }, + supportsFIM: false, + supportsSystemMessage: 'system-role', + supportsTools: 'openai-style', + supportsReasoningOutput: false, + }, + 'qwen-2.5-coder-32b': { + contextWindow: 128_000, + cost: { input: 0.79, output: 0.79 }, + supportsFIM: false, // unfortunately looks like no FIM support on groq + supportsSystemMessage: 'system-role', + supportsTools: 'openai-style', + supportsReasoningOutput: false, + }, +} as const satisfies { [s: string]: ModelOptions } +const groqSettings: ProviderSettings = { + modelOptions: groqModelOptions, + modelOptionsFallback: (modelName) => { return null } +} + + +// ---------------- anything self-hosted/local: VLLM, OLLAMA, OPENAICOMPAT ---------------- + +// fallback to any model (anything openai-compatible) +const extensiveModelFallback: ProviderSettings['modelOptionsFallback'] = (modelName) => { + const toFallback = (opts: Omit): 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')) return toFallback(anthropicModelOptions['claude-3-5-sonnet-20241022']) + if (modelName.includes('grok')) return toFallback(xAIModelOptions['grok-2-latest']) + if (modelName.includes('deepseek-r1') || modelName.includes('deepseek-reasoner')) return toFallback({ ...openSourceModelDefaultOptionsAssumingOAICompat.deepseekR1, contextWindow: 32_000, }) + if (modelName.includes('deepseek')) return toFallback({ ...openSourceModelDefaultOptionsAssumingOAICompat.deepseekCoderV2, contextWindow: 32_000, }) + if (modelName.includes('llama3')) return toFallback({ ...openSourceModelDefaultOptionsAssumingOAICompat.llama3, contextWindow: 32_000, }) + if (modelName.includes('qwen2.5-coder')) return toFallback({ ...openSourceModelDefaultOptionsAssumingOAICompat['qwen2.5coder'], contextWindow: 32_000, }) + if (modelName.includes('codestral')) return toFallback({ ...openSourceModelDefaultOptionsAssumingOAICompat.codestral, contextWindow: 32_000, }) + if (/\bo1\b/.test(modelName) || /\bo3\b/.test(modelName)) return toFallback(openAIModelOptions['o1']) + return toFallback(modelOptionDefaults) +} + + +const vLLMSettings: ProviderSettings = { + // reasoning: OAICompat + response.choices[0].delta.reasoning_content // https://docs.vllm.ai/en/stable/features/reasoning_outputs.html#streaming-chat-completions + ifSupportsReasoningOutput: { output: { nameOfFieldInDelta: 'reasoning_content' }, }, + modelOptionsFallback: (modelName) => extensiveModelFallback(modelName), + modelOptions: {}, +} + +const ollamaSettings: ProviderSettings = { + // reasoning: we need to filter out reasoning tags manually + ifSupportsReasoningOutput: { 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 = { + 'deepseek/deepseek-r1': { + ...openSourceModelDefaultOptionsAssumingOAICompat.deepseekR1, + contextWindow: 128_000, + cost: { input: 0.8, output: 2.4 }, + }, + 'anthropic/claude-3.5-sonnet': { + contextWindow: 200_000, + cost: { input: 3.00, output: 15.00 }, + supportsFIM: false, + supportsSystemMessage: 'system-role', + supportsTools: 'openai-style', + supportsReasoningOutput: false, + }, + 'mistralai/codestral-2501': { + ...openSourceModelDefaultOptionsAssumingOAICompat.codestral, + contextWindow: 256_000, + cost: { input: 0.3, output: 0.9 }, + supportsTools: 'openai-style', + supportsReasoningOutput: false, + }, +} 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 + ifSupportsReasoningOutput: { + input: { includeInPayload: { include_reasoning: true } }, + output: { nameOfFieldInDelta: 'reasoning' }, + }, + modelOptions: openRouterModelOptions, + // TODO!!! send a query to openrouter to get the price, isFIM, etc. + modelOptionsFallback: (modelName) => extensiveModelFallback(modelName), +} + +// ---------------- model settings of everything above ---------------- const modelSettingsOfProvider: ModelSettingsOfProvider = { openAI: openAISettings, anthropic: anthropicSettings, - xAI: XAISettings, - gemini: { - modelOptions: { - - } - }, - googleVertex: { - - }, - microsoftAzure: { - - }, - openRouter: { - providerReasoningOptions: { - // reasoning: OAICompat + response.choices[0].delta.reasoning : payload should have {include_reasoning: true} https://openrouter.ai/announcements/reasoning-tokens-for-thinking-models - input: { includeInPayload: { include_reasoning: true } }, - output: { nameOfFieldInDelta: 'reasoning' }, - } - }, - vLLM: { - providerReasoningOptions: { - // reasoning: OAICompat + response.choices[0].delta.reasoning_content // https://docs.vllm.ai/en/stable/features/reasoning_outputs.html#streaming-chat-completions - output: { nameOfFieldInDelta: 'reasoning_content' }, - } - }, - deepseek: { - providerReasoningOptions: { - // reasoning: OAICompat + response.choices[0].delta.reasoning_content // https://api-docs.deepseek.com/guides/reasoning_model - output: { nameOfFieldInDelta: 'reasoning_content' }, - }, - }, - ollama: { - providerReasoningOptions: { - // reasoning: we need to filter out reasoning tags manually - output: { needsManualParse: true }, - }, - }, - - openAICompatible: { - }, - mistral: { - }, - groq: { - }, + 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 satisfies ModelSettingsOfProvider -const modelOptionsOfProvider = (providerName: ProviderName, modelName: string) => { + + +export const modelOptionsOfProvider = (providerName: ProviderName, modelName: string): ModelOptions & { modelName: string } => { const { modelOptions, modelOptionsFallback } = modelSettingsOfProvider[providerName] - if (modelName in modelOptions) return modelOptions[modelName] - return modelOptionsFallback(modelName) + if (modelName in modelOptions) return { modelName, ...modelOptions[modelName] } + const result = modelOptionsFallback(modelName) + if (!result) return { modelName, ...modelOptionDefaults } + return result } @@ -361,10 +586,6 @@ const newOpenAICompatibleSDK = ({ settingsOfProvider, providerName, includeInPay const thisConfig = settingsOfProvider[providerName] return new OpenAI({ baseURL: thisConfig.endpoint, apiKey: thisConfig.apiKey, ...commonPayloadOpts }) } - else if (providerName === 'mistral') { - const thisConfig = settingsOfProvider[providerName] - return new OpenAI({ baseURL: 'https://api.mistral.ai/v1', apiKey: thisConfig.apiKey, ...commonPayloadOpts }) - } else if (providerName === 'groq') { const thisConfig = settingsOfProvider[providerName] return new OpenAI({ baseURL: 'https://api.groq.com/openai/v1', apiKey: thisConfig.apiKey, ...commonPayloadOpts }) @@ -379,33 +600,52 @@ const newOpenAICompatibleSDK = ({ settingsOfProvider, providerName, includeInPay -const manualParseOnText = ( - providerName: ProviderName, - modelName: string, - onText_: OnText -): OnText => { - return onText_ +const _sendOpenAICompatibleFIM = ({ messages: messages_, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, aiInstructions, }: SendFIMParams_Internal) => { + const { modelName, } = modelOptionsOfProvider(providerName, modelName_) + 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: 'Invalid API key.', fullError: error }); } + else { onError({ message: error + '', fullError: error }); } + }) } -const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName, aiInstructions, tools: tools_ }: SendChatParams_Internal) => { + + +const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, aiInstructions, tools: tools_ }: SendChatParams_Internal) => { const { - supportsReasoning: modelSupportsReasoning, + modelName, + supportsReasoningOutput, supportsSystemMessage, supportsTools, - } = modelOptionsOfProvider(providerName, modelName) + } = modelOptionsOfProvider(providerName, modelName_) const { messages } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsTools, }) const tools = (supportsTools && ((tools_?.length ?? 0) !== 0)) ? tools_?.map(tool => toOpenAICompatibleTool(tool)) : undefined - const includeInPayload = modelSupportsReasoning ? {} : modelSettingsOfProvider[providerName].providerReasoningOptions?.input?.includeInPayload || {} + const includeInPayload = supportsReasoningOutput ? modelSettingsOfProvider[providerName].ifSupportsReasoningOutput?.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 { nameOfFieldInDelta: nameOfReasoningFieldInDelta, needsManualParse: needsManualReasoningParse } = modelSettingsOfProvider[providerName].providerReasoningOptions?.output ?? {} - if (needsManualReasoningParse) onText = manualParseOnText(providerName, modelName, onText) + const { nameOfFieldInDelta: nameOfReasoningFieldInDelta, needsManualParse: needsManualReasoningParse } = modelSettingsOfProvider[providerName].ifSupportsReasoningOutput?.output ?? {} + if (needsManualReasoningParse && supportsReasoningOutput && supportsReasoningOutput.openSourceThinkTags) + onText = extractReasoningFromText(onText, supportsReasoningOutput.openSourceThinkTags) let fullReasoning = '' let fullText = '' @@ -432,7 +672,7 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage let newReasoning = '' if (nameOfReasoningFieldInDelta) { // @ts-ignore - newReasoning = (chunk.choices[0]?.delta?.[nameOfFieldInDelta] || '') + '' + newReasoning = (chunk.choices[0]?.delta?.[nameOfReasoningFieldInDelta] || '') + '' fullReasoning += newReasoning } @@ -477,10 +717,6 @@ const _openaiCompatibleList = async ({ onSuccess: onSuccess_, onError: onError_, -// ------------ OPENAI ------------ -const sendOpenAIChat = (params: SendChatParams_Internal) => { - return _sendOpenAICompatibleChat(params) -} // ------------ ANTHROPIC ------------ const toAnthropicTool = (toolInfo: InternalToolInfo) => { @@ -504,13 +740,14 @@ const toolCallsFromAnthropicContent = (content: Anthropic.Messages.ContentBlock[ }).filter(t => !!t) } -const sendAnthropicChat = ({ messages: messages_, onText, providerName, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, aiInstructions, tools: tools_ }: SendChatParams_Internal) => { +const sendAnthropicChat = ({ messages: messages_, onText, providerName, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, aiInstructions, tools: tools_ }: SendChatParams_Internal) => { const { // supportsReasoning: modelSupportsReasoning, + modelName, supportsSystemMessage, supportsTools, contextWindow, - } = modelOptionsOfProvider(providerName, modelName) + } = modelOptionsOfProvider(providerName, modelName_) const { messages, separateSystemMessageStr } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsTools, }) @@ -561,16 +798,6 @@ const sendAnthropicChat = ({ messages: messages_, onText, providerName, onFinalM // }) -// ------------ XAI ------------ -const sendXAIChat = (params: SendChatParams_Internal) => { - return _sendOpenAICompatibleChat(params) -} - -// ------------ GEMINI ------------ -const sendGeminiAPIChat = (params: SendChatParams_Internal) => { - return _sendOpenAICompatibleChat(params) -} - // ------------ 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 @@ -603,10 +830,12 @@ const ollamaList = async ({ onSuccess: onSuccess_, onError: onError_, settingsOf } } -const sendOllamaFIM = ({ messages, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }: SendFIMParams_Internal) => { +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, @@ -614,7 +843,7 @@ const sendOllamaFIM = ({ messages, onFinalMessage, onError, settingsOfProvider, suffix: messages.suffix, options: { stop: messages.stopTokens, - num_predict: 300, // max tokens + num_predict: messages.maxTokens, // max tokens // repeat_penalty: 1, }, raw: true, @@ -635,57 +864,73 @@ const sendOllamaFIM = ({ messages, onFinalMessage, onError, settingsOfProvider, } -// ollama's implementation of openai-compatible SDK dumps all reasoning tokens out with message, and supports tools, so we can use it for chat! -const sendOllamaChat = (params: SendChatParams_Internal) => { - return _sendOpenAICompatibleChat(params) + +type CallFnOfProvider = { + [providerName in ProviderName]: { + sendChat: (params: SendChatParams_Internal) => void; + sendFIM: ((params: SendFIMParams_Internal) => void) | null; + list: ((params: ListParams_Internal) => void) | null; + } } -// ------------ OPENAI-COMPATIBLE ------------ -// TODO!!! FIM - -// using openai's SDK is not ideal (your implementation might not do tools, reasoning, FIM etc correctly), talk to us for a custom integration -const sendOpenAICompatibleChat = (params: SendChatParams_Internal) => { - return _sendOpenAICompatibleChat(params) -} - -// ------------ OPENROUTER ------------ -const sendOpenRouterChat = (params: SendChatParams_Internal) => { - _sendOpenAICompatibleChat(params) -} - -// ------------ VLLM ------------ -const vLLMList = async (params: ListParams_Internal) => { - return _openaiCompatibleList(params) -} -const sendVLLMFIM = (params: SendFIMParams_Internal) => { - // TODO!!! -} - -// using openai's SDK is not ideal (your implementation might not do tools, reasoning, FIM etc correctly), talk to us for a custom integration -const sendVLLMChat = (params: SendChatParams_Internal) => { - return _sendOpenAICompatibleChat(params) -} - -// ------------ DEEPSEEK API ------------ -const sendDeepSeekAPIChat = (params: SendChatParams_Internal) => { - return _sendOpenAICompatibleChat(params) -} - -// ------------ MISTRAL ------------ -const sendMistralAPIChat = (params: SendChatParams_Internal) => { - return _sendOpenAICompatibleChat(params) -} - -// ------------ GROQ ------------ -const sendGroqAPIChat = (params: SendChatParams_Internal) => { - return _sendOpenAICompatibleChat(params) -} +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: +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|> @@ -706,71 +951,3 @@ codegemma https://ollama.com/library/codegemma:2b/blobs/48d9a8140749 <|fim_prefix|>{{ .Prompt }}<|fim_suffix|>{{ .Suffix }}<|fim_middle|> */ - - - -type CallFnOfProvider = { - [providerName in ProviderName]: { - sendChat: (params: SendChatParams_Internal) => void; - sendFIM: ((params: SendFIMParams_Internal) => void) | null; - list: ((params: ListParams_Internal) => void) | null; - } -} -export const sendLLMMessageToProviderImplementation = { - openAI: { - sendChat: sendOpenAIChat, - sendFIM: null, - list: null, - }, - anthropic: { - sendChat: sendAnthropicChat, - sendFIM: null, - list: null, - }, - xAI: { - sendChat: sendXAIChat, - sendFIM: null, - list: null, - }, - gemini: { - sendChat: sendGeminiAPIChat, - sendFIM: null, - list: null, - }, - ollama: { - sendChat: sendOllamaChat, - sendFIM: sendOllamaFIM, - list: ollamaList, - }, - openAICompatible: { - sendChat: sendOpenAICompatibleChat, - sendFIM: null, - list: null, - }, - openRouter: { - sendChat: sendOpenRouterChat, - sendFIM: null, - list: null, - }, - vLLM: { - sendChat: sendVLLMChat, - sendFIM: sendVLLMFIM, - list: vLLMList, - }, - deepseek: { - sendChat: sendDeepSeekAPIChat, - sendFIM: null, - list: null, - }, - groq: { - sendChat: sendGroqAPIChat, - sendFIM: null, - list: null, - }, - mistral: { - sendChat: sendMistralAPIChat, - sendFIM: null, - list: null, - }, - -} satisfies CallFnOfProvider diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts index 1d388338..1aec2649 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts @@ -1,6 +1,6 @@ -import { LLMChatMessage } from '../../common/llmMessageTypes.js'; +import { LLMChatMessage, LLMFIMMessage } from '../../common/llmMessageTypes.js'; import { deepClone } from '../../../../../base/common/objects.js'; @@ -322,3 +322,27 @@ export const prepareMessages = ({ } as const } + + + + +export const prepareFIMMessage = ({ + messages, + aiInstructions, +}: { + messages: LLMFIMMessage, + aiInstructions: string, +}) => { + + let prefix = `\ +## You are a helpful coding assistant that performs autocomplete (fill-in-the middle or "FIM") for the user. +${!aiInstructions ? '' : `\ +## Special user instructions: +${aiInstructions.split('\n').map(line => `##${line}`).join('\n')}`} + +${messages.prefix}` + + const suffix = messages.suffix + const stopTokens = messages.stopTokens + return { prefix, suffix, stopTokens, maxTokens: 300 } as const +} From d2fb0fb4fff7aca0c7c0bc57d84fefe9a43ee1be Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Mon, 24 Feb 2025 04:32:53 -0800 Subject: [PATCH 38/41] maxTokens for anthropic --- .../void/electron-main/llmMessage/MODELS.ts | 40 +++++++++++++++---- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts index d68408cd..14f5ec44 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts @@ -17,7 +17,8 @@ import { extractReasoningFromText } from '../../browser/helpers/extractCodeFromR type ModelOptions = { - contextWindow: number; + contextWindow: number; // input tokens + maxOutputTokens: number | null; // output tokens cost: { input: number; output: number; @@ -70,6 +71,7 @@ type ModelSettingsOfProvider = { const modelOptionDefaults: ModelOptions = { contextWindow: 32_000, + maxOutputTokens: null, cost: { input: 0, output: 0 }, supportsSystemMessage: false, supportsTools: false, @@ -82,6 +84,7 @@ const modelOptionDefaults: ModelOptions = { 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, @@ -90,6 +93,7 @@ const openAIModelOptions = { // https://platform.openai.com/docs/pricing }, 'o3-mini': { contextWindow: 200_000, + maxOutputTokens: 100_000, cost: { input: 1.10, cache_read: 0.55, output: 4.40, }, supportsFIM: false, supportsTools: false, @@ -98,6 +102,7 @@ const openAIModelOptions = { // https://platform.openai.com/docs/pricing }, 'gpt-4o': { contextWindow: 128_000, + maxOutputTokens: 16_384, cost: { input: 2.50, cache_read: 1.25, output: 10.00, }, supportsFIM: false, supportsTools: 'openai-style', @@ -106,6 +111,7 @@ const openAIModelOptions = { // https://platform.openai.com/docs/pricing }, 'o1-mini': { contextWindow: 128_000, + maxOutputTokens: 65_536, cost: { input: 1.10, cache_read: 0.55, output: 4.40, }, supportsFIM: false, supportsTools: false, @@ -114,6 +120,7 @@ const openAIModelOptions = { // https://platform.openai.com/docs/pricing }, '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', @@ -139,6 +146,7 @@ const openAISettings: ProviderSettings = { const anthropicModelOptions = { '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', @@ -147,6 +155,7 @@ const anthropicModelOptions = { }, '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', @@ -155,6 +164,7 @@ const anthropicModelOptions = { }, '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', @@ -163,6 +173,7 @@ const anthropicModelOptions = { }, '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', @@ -177,8 +188,9 @@ const anthropicSettings: ProviderSettings = { 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 null + return { modelName, ...modelOptionDefaults, maxOutputTokens: 4_096 } } } @@ -187,6 +199,7 @@ const anthropicSettings: ProviderSettings = { const xAIModelOptions = { 'grok-2-latest': { contextWindow: 131_072, + maxOutputTokens: null, // 131_072, cost: { input: 2.00, output: 10.00 }, supportsFIM: false, supportsSystemMessage: 'system-role', @@ -210,6 +223,7 @@ const xAISettings: ProviderSettings = { 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', @@ -218,6 +232,7 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing }, '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', @@ -226,6 +241,7 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing }, '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', @@ -234,6 +250,7 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing }, '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', @@ -242,6 +259,7 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing }, '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', @@ -332,11 +350,13 @@ const deepseekModelOptions = { 'deepseek-chat': { ...openSourceModelDefaultOptionsAssumingOAICompat.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': { ...openSourceModelDefaultOptionsAssumingOAICompat.deepseekCoderV2, contextWindow: 64_000, + maxOutputTokens: null, // 8_000, cost: { cache_read: .14, input: .55, output: 2.19, }, }, } as const satisfies { [s: string]: ModelOptions } @@ -357,6 +377,7 @@ const deepseekSettings: ProviderSettings = { const groqModelOptions = { 'llama-3.3-70b-versatile': { contextWindow: 128_000, + maxOutputTokens: null, // 32_768, cost: { input: 0.59, output: 0.79 }, supportsFIM: false, supportsSystemMessage: 'system-role', @@ -365,6 +386,7 @@ const groqModelOptions = { }, 'llama-3.1-8b-instant': { contextWindow: 128_000, + maxOutputTokens: null, // 8_192, cost: { input: 0.05, output: 0.08 }, supportsFIM: false, supportsSystemMessage: 'system-role', @@ -373,6 +395,7 @@ const groqModelOptions = { }, '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', @@ -401,11 +424,11 @@ const extensiveModelFallback: ProviderSettings['modelOptionsFallback'] = (modelN if (modelName.includes('gpt-4o')) return toFallback(openAIModelOptions['gpt-4o']) if (modelName.includes('claude')) return toFallback(anthropicModelOptions['claude-3-5-sonnet-20241022']) if (modelName.includes('grok')) return toFallback(xAIModelOptions['grok-2-latest']) - if (modelName.includes('deepseek-r1') || modelName.includes('deepseek-reasoner')) return toFallback({ ...openSourceModelDefaultOptionsAssumingOAICompat.deepseekR1, contextWindow: 32_000, }) - if (modelName.includes('deepseek')) return toFallback({ ...openSourceModelDefaultOptionsAssumingOAICompat.deepseekCoderV2, contextWindow: 32_000, }) - if (modelName.includes('llama3')) return toFallback({ ...openSourceModelDefaultOptionsAssumingOAICompat.llama3, contextWindow: 32_000, }) - if (modelName.includes('qwen2.5-coder')) return toFallback({ ...openSourceModelDefaultOptionsAssumingOAICompat['qwen2.5coder'], contextWindow: 32_000, }) - if (modelName.includes('codestral')) return toFallback({ ...openSourceModelDefaultOptionsAssumingOAICompat.codestral, contextWindow: 32_000, }) + if (modelName.includes('deepseek-r1') || modelName.includes('deepseek-reasoner')) return toFallback({ ...openSourceModelDefaultOptionsAssumingOAICompat.deepseekR1, contextWindow: 32_000, maxOutputTokens: 4_096, }) + if (modelName.includes('deepseek')) return toFallback({ ...openSourceModelDefaultOptionsAssumingOAICompat.deepseekCoderV2, contextWindow: 32_000, maxOutputTokens: 4_096, }) + if (modelName.includes('llama3')) return toFallback({ ...openSourceModelDefaultOptionsAssumingOAICompat.llama3, contextWindow: 32_000, maxOutputTokens: 4_096, }) + if (modelName.includes('qwen2.5-coder')) return toFallback({ ...openSourceModelDefaultOptionsAssumingOAICompat['qwen2.5coder'], contextWindow: 32_000, maxOutputTokens: 4_096, }) + if (modelName.includes('codestral')) return toFallback({ ...openSourceModelDefaultOptionsAssumingOAICompat.codestral, contextWindow: 32_000, maxOutputTokens: 4_096, }) if (/\bo1\b/.test(modelName) || /\bo3\b/.test(modelName)) return toFallback(openAIModelOptions['o1']) return toFallback(modelOptionDefaults) } @@ -437,10 +460,12 @@ const openRouterModelOptions = { 'deepseek/deepseek-r1': { ...openSourceModelDefaultOptionsAssumingOAICompat.deepseekR1, contextWindow: 128_000, + maxOutputTokens: null, cost: { input: 0.8, output: 2.4 }, }, 'anthropic/claude-3.5-sonnet': { contextWindow: 200_000, + maxOutputTokens: null, cost: { input: 3.00, output: 15.00 }, supportsFIM: false, supportsSystemMessage: 'system-role', @@ -450,6 +475,7 @@ const openRouterModelOptions = { 'mistralai/codestral-2501': { ...openSourceModelDefaultOptionsAssumingOAICompat.codestral, contextWindow: 256_000, + maxOutputTokens: null, cost: { input: 0.3, output: 0.9 }, supportsTools: 'openai-style', supportsReasoningOutput: false, From 5cbd0a19a4f1696393865258ad7d7872ff7f394a Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Mon, 24 Feb 2025 04:38:08 -0800 Subject: [PATCH 39/41] minor fixes --- .../void/browser/helpers/extractCodeFromResult.ts | 2 +- .../contrib/void/electron-main/llmMessage/MODELS.ts | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts b/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts index 297d82b6..00eb2ef1 100644 --- a/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts +++ b/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts @@ -250,7 +250,7 @@ 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 extractReasoningFromText = ( onText_: OnText, thinkTags: [string, string], diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts index 14f5ec44..6d39ee9f 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts @@ -627,7 +627,7 @@ const newOpenAICompatibleSDK = ({ settingsOfProvider, providerName, includeInPay const _sendOpenAICompatibleFIM = ({ messages: messages_, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, aiInstructions, }: SendFIMParams_Internal) => { - const { modelName, } = modelOptionsOfProvider(providerName, modelName_) + const { modelName } = modelOptionsOfProvider(providerName, modelName_) const messages = prepareFIMMessage({ messages: messages_, aiInstructions, }) const openai = newOpenAICompatibleSDK({ providerName, settingsOfProvider }) @@ -658,6 +658,7 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage supportsReasoningOutput, supportsSystemMessage, supportsTools, + maxOutputTokens, } = modelOptionsOfProvider(providerName, modelName_) const { messages } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsTools, }) @@ -666,8 +667,9 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage const includeInPayload = supportsReasoningOutput ? modelSettingsOfProvider[providerName].ifSupportsReasoningOutput?.input?.includeInPayload || {} : {} const toolsObj = tools ? { tools: tools, tool_choice: 'auto', parallel_tool_calls: false, } as const : {} + const maxTokensObj = maxOutputTokens ? { max_tokens: maxOutputTokens } : {} const openai: OpenAI = newOpenAICompatibleSDK({ providerName, settingsOfProvider, includeInPayload }) - const options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = { model: modelName, messages: messages, stream: true, ...toolsObj } + const options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = { model: modelName, messages: messages, stream: true, ...toolsObj, ...maxTokensObj } const { nameOfFieldInDelta: nameOfReasoningFieldInDelta, needsManualParse: needsManualReasoningParse } = modelSettingsOfProvider[providerName].ifSupportsReasoningOutput?.output ?? {} if (needsManualReasoningParse && supportsReasoningOutput && supportsReasoningOutput.openSourceThinkTags) @@ -772,7 +774,7 @@ const sendAnthropicChat = ({ messages: messages_, onText, providerName, onFinalM modelName, supportsSystemMessage, supportsTools, - contextWindow, + maxOutputTokens, } = modelOptionsOfProvider(providerName, modelName_) const { messages, separateSystemMessageStr } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsTools, }) @@ -785,7 +787,7 @@ const sendAnthropicChat = ({ messages: messages_, onText, providerName, onFinalM system: separateSystemMessageStr, messages: messages, model: modelName, - max_tokens: contextWindow, + max_tokens: maxOutputTokens ?? 4_096, // anthropic requires this tools: tools, tool_choice: tools ? { type: 'auto', disable_parallel_tool_use: true } : undefined // one tool use at a time }) From 5c047b78f36d09227550b7325c8fc6bbc103731d Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Mon, 24 Feb 2025 05:33:45 -0800 Subject: [PATCH 40/41] style+FIM prompt --- .../void/browser/react/src/sidebar-tsx/SidebarChat.tsx | 2 +- .../contrib/void/electron-main/llmMessage/MODELS.ts | 10 ++++++---- .../electron-main/llmMessage/preprocessLLMMessages.ts | 4 ++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx index 96e22bd4..979ae67b 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx @@ -1020,7 +1020,7 @@ export const SidebarChat = () => { {/* error message */} {latestError === undefined ? null : -
    +
    `Invalid ${displayInfoOfProviderName(providerName).title} API key.` + // ---------------- OPENAI ---------------- const openAIModelOptions = { // https://platform.openai.com/docs/pricing @@ -644,7 +646,7 @@ const _sendOpenAICompatibleFIM = ({ messages: messages_, onFinalMessage, onError onFinalMessage({ fullText, }); }) .catch(error => { - if (error instanceof OpenAI.APIError && error.status === 401) { onError({ message: 'Invalid API key.', fullError: error }); } + if (error instanceof OpenAI.APIError && error.status === 401) { onError({ message: invalidApiKeyMessage(providerName), fullError: error }); } else { onError({ message: error + '', fullError: error }); } }) } @@ -710,7 +712,7 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage }) // 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: 'Invalid API key.', fullError: error }); } + if (error instanceof OpenAI.APIError && error.status === 401) { onError({ message: invalidApiKeyMessage(providerName), fullError: error }); } else { onError({ message: error + '', fullError: error }); } }) } @@ -803,7 +805,7 @@ const sendAnthropicChat = ({ messages: messages_, onText, providerName, onFinalM }) // on error stream.on('error', (error) => { - if (error instanceof Anthropic.APIError && error.status === 401) { onError({ message: 'Invalid API key.', fullError: 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()) diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts index 1aec2649..40eb880c 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts @@ -335,9 +335,9 @@ export const prepareFIMMessage = ({ }) => { let prefix = `\ -## You are a helpful coding assistant that performs autocomplete (fill-in-the middle or "FIM") for the user. +## You are a helpful coding assistant that performs autocomplete. ${!aiInstructions ? '' : `\ -## Special user instructions: +## Instructions: ${aiInstructions.split('\n').map(line => `##${line}`).join('\n')}`} ${messages.prefix}` From 4a43b628d472849dddeade3d706b451d1bf96fd1 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Mon, 24 Feb 2025 06:23:24 -0800 Subject: [PATCH 41/41] improvements --- .../react/src/void-settings-tsx/Settings.tsx | 35 +++++++------ .../contrib/void/common/toolsService.ts | 4 +- .../void/common/voidSettingsService.ts | 52 ++++++++++--------- .../contrib/void/common/voidSettingsTypes.ts | 2 +- .../void/electron-main/llmMessage/MODELS.ts | 27 ++++++++-- .../llmMessage/preprocessLLMMessages.ts | 10 ++-- 6 files changed, 77 insertions(+), 53 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx index e2056fbf..7f28467f 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx @@ -5,7 +5,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js' -import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidModelInfo, globalSettingNames, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, defaultProviderSettings, nonlocalProviderNames, localProviderNames, GlobalSettingName, featureNames, displayInfoOfFeatureName, isProviderNameDisabled } from '../../../../common/voidSettingsTypes.js' +import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidModelInfo, globalSettingNames, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, defaultProviderSettings, nonlocalProviderNames, localProviderNames, GlobalSettingName, featureNames, displayInfoOfFeatureName, isProviderNameDisabled, FeatureName } from '../../../../common/voidSettingsTypes.js' import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js' import { VoidButton, VoidCheckBox, VoidCustomDropdownBox, VoidInputBox, VoidInputBox2, VoidSwitch } from '../util/inputs.js' import { useAccessor, useIsDark, useRefreshModelListener, useRefreshModelState, useSettingsState } from '../util/services.js' @@ -368,15 +368,15 @@ export const AutoRefreshToggle = () => { // right now this is just `enabled_autoRefreshModels` const enabled = voidSettingsState.globalSettings[settingName] - return { - voidSettingsService.setGlobalSetting(settingName, !enabled) - metricsService.capture('Click', { action: 'Autorefresh Toggle', settingName, enabled: !enabled }) - }} - text={`Automatically detect local providers and models (${refreshableProviderNames.map(providerName => displayInfoOfProviderName(providerName).title).join(', ')}).`} - icon={enabled ? : } - disabled={false} - /> + return { + voidSettingsService.setGlobalSetting(settingName, !enabled) + metricsService.capture('Click', { action: 'Autorefresh Toggle', settingName, enabled: !enabled }) + }} + text={`Automatically detect local providers and models (${refreshableProviderNames.map(providerName => displayInfoOfProviderName(providerName).title).join(', ')}).`} + icon={enabled ? : } + disabled={false} + /> } @@ -401,7 +401,7 @@ export const FeaturesTab = () => { -
    +
    @@ -437,12 +437,13 @@ export const FeaturesTab = () => {

    Feature Options

    {featureNames.map(featureName => -
    -

    {displayInfoOfFeatureName(featureName)}

    - -
    + (['Ctrl+L', 'Ctrl+K'] as FeatureName[]).includes(featureName) ? null : +
    +

    {displayInfoOfFeatureName(featureName)}

    + +
    )}
    diff --git a/src/vs/workbench/contrib/void/common/toolsService.ts b/src/vs/workbench/contrib/void/common/toolsService.ts index 4e92696c..f27739c0 100644 --- a/src/vs/workbench/contrib/void/common/toolsService.ts +++ b/src/vs/workbench/contrib/void/common/toolsService.ts @@ -55,7 +55,7 @@ export const voidTools = { query: { type: 'string', description: undefined }, ...paginationHelper.param, }, - required: ['query'] + required: ['query'], }, search: { @@ -305,6 +305,8 @@ export class ToolsService implements IToolsService { return { queryStr, uris, hasNextPage } }, search: async (s: string) => { + + console.log('search') const o = validateJSON(s) diff --git a/src/vs/workbench/contrib/void/common/voidSettingsService.ts b/src/vs/workbench/contrib/void/common/voidSettingsService.ts index be3f6689..72095c83 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsService.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsService.ts @@ -63,7 +63,30 @@ export interface IVoidSettingsService { -const _updatedValidatedState = (state: Omit) => { + +const _updatedModelsAfterDefaultModelsChange = (defaultModelNames: string[], options: { existingModels: VoidModelInfo[] }) => { + const { existingModels } = options + + const existingModelsMap: Record = {} + for (const existingModel of existingModels) { + existingModelsMap[existingModel.modelName] = existingModel + } + + const newDefaultModels = defaultModelNames.map((modelName, i) => ({ + modelName, + isDefault: true, + isAutodetected: true, + isHidden: !!existingModelsMap[modelName]?.isHidden, + })) + + return [ + ...newDefaultModels, // swap out all the default models for the new default models + ...existingModels.filter(m => !m.isDefault), // keep any non-default (custom) models + ] +} + + +const _validatedState = (state: Omit) => { let newSettingsOfProvider = state.settingsOfProvider @@ -201,7 +224,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { modelSelectionOfFeature: newModelSelectionOfFeature, } - this.state = _updatedValidatedState(readS) + this.state = _validatedState(readS) resolver() this._onDidChangeState.fire() @@ -248,7 +271,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { globalSettings: newGlobalSettings, } - this.state = _updatedValidatedState(newState) + this.state = _validatedState(newState) await this._storeState() this._onDidChangeState.fire() @@ -290,27 +313,6 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { } - private _updatedModelsAfterAutodetection = (defaultModelNames: string[], options: { existingModels: VoidModelInfo[] }) => { - const { existingModels } = options - - const existingModelsMap: Record = {} - for (const existingModel of existingModels) { - existingModelsMap[existingModel.modelName] = existingModel - } - - const newDefaultModels = defaultModelNames.map((modelName, i) => ({ - modelName, - isDefault: true, - isAutodetected: true, - isHidden: !!existingModelsMap[modelName]?.isHidden, - })) - - return [ - ...newDefaultModels, // swap out all the default models for the new default models - ...existingModels.filter(m => !m.isDefault), // keep any non-default (custom) models - ] - } - setAutodetectedModels(providerName: ProviderName, autodetectedModelNames: string[], logging: object) { @@ -318,7 +320,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { const { models } = this.state.settingsOfProvider[providerName] const oldModelNames = models.map(m => m.modelName) - const newModels = this._updatedModelsAfterAutodetection(autodetectedModelNames, { existingModels: models }) + const newModels = _updatedModelsAfterDefaultModelsChange(autodetectedModelNames, { existingModels: models }) this.setSettingOfProvider(providerName, 'models', newModels) // if the models changed, log it diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index 4111b53b..379a4817 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -83,7 +83,7 @@ export const defaultModelsOfProvider = { 'anthropic/claude-3.5-sonnet', 'deepseek/deepseek-r1', 'mistralai/codestral-2501', - 'qwen/qwen2.5-vl-72b-instruct:free', + 'qwen/qwen-2.5-coder-32b-instruct', ], groq: [ // https://console.groq.com/docs/models 'llama-3.3-70b-versatile', diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts index 872d5058..a4ad5487 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/MODELS.ts @@ -429,7 +429,7 @@ const extensiveModelFallback: ProviderSettings['modelOptionsFallback'] = (modelN if (modelName.includes('deepseek-r1') || modelName.includes('deepseek-reasoner')) return toFallback({ ...openSourceModelDefaultOptionsAssumingOAICompat.deepseekR1, contextWindow: 32_000, maxOutputTokens: 4_096, }) if (modelName.includes('deepseek')) return toFallback({ ...openSourceModelDefaultOptionsAssumingOAICompat.deepseekCoderV2, contextWindow: 32_000, maxOutputTokens: 4_096, }) if (modelName.includes('llama3')) return toFallback({ ...openSourceModelDefaultOptionsAssumingOAICompat.llama3, contextWindow: 32_000, maxOutputTokens: 4_096, }) - if (modelName.includes('qwen2.5-coder')) return toFallback({ ...openSourceModelDefaultOptionsAssumingOAICompat['qwen2.5coder'], contextWindow: 32_000, maxOutputTokens: 4_096, }) + if (modelName.includes('qwen') && modelName.includes('2.5') && modelName.includes('coder')) return toFallback({ ...openSourceModelDefaultOptionsAssumingOAICompat['qwen2.5coder'], contextWindow: 32_000, maxOutputTokens: 4_096, }) if (modelName.includes('codestral')) return toFallback({ ...openSourceModelDefaultOptionsAssumingOAICompat.codestral, contextWindow: 32_000, maxOutputTokens: 4_096, }) if (/\bo1\b/.test(modelName) || /\bo3\b/.test(modelName)) return toFallback(openAIModelOptions['o1']) return toFallback(modelOptionDefaults) @@ -482,6 +482,15 @@ const openRouterModelOptions = { supportsTools: 'openai-style', supportsReasoningOutput: false, }, + 'qwen/qwen-2.5-coder-32b-instruct': { + ...openSourceModelDefaultOptionsAssumingOAICompat['qwen2.5coder'], + 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 = { @@ -520,7 +529,7 @@ const modelSettingsOfProvider: ModelSettingsOfProvider = { -export const modelOptionsOfProvider = (providerName: ProviderName, modelName: string): ModelOptions & { modelName: string } => { +export const getModelCapabilities = (providerName: ProviderName, modelName: string): ModelOptions & { modelName: string } => { const { modelOptions, modelOptionsFallback } = modelSettingsOfProvider[providerName] if (modelName in modelOptions) return { modelName, ...modelOptions[modelName] } const result = modelOptionsFallback(modelName) @@ -629,7 +638,15 @@ const newOpenAICompatibleSDK = ({ settingsOfProvider, providerName, includeInPay const _sendOpenAICompatibleFIM = ({ messages: messages_, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, aiInstructions, }: SendFIMParams_Internal) => { - const { modelName } = modelOptionsOfProvider(providerName, modelName_) + const { modelName, supportsFIM } = getModelCapabilities(providerName, modelName_) + if (!supportsFIM) { + if (modelName === modelName_) + onFinalMessage({ fullText: `Model ${modelName} does not support FIM.` }) + else + onFinalMessage({ fullText: `Model ${modelName_} (${modelName}) does not support FIM.` }) + return + } + const messages = prepareFIMMessage({ messages: messages_, aiInstructions, }) const openai = newOpenAICompatibleSDK({ providerName, settingsOfProvider }) @@ -661,7 +678,7 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage supportsSystemMessage, supportsTools, maxOutputTokens, - } = modelOptionsOfProvider(providerName, modelName_) + } = getModelCapabilities(providerName, modelName_) const { messages } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsTools, }) const tools = (supportsTools && ((tools_?.length ?? 0) !== 0)) ? tools_?.map(tool => toOpenAICompatibleTool(tool)) : undefined @@ -777,7 +794,7 @@ const sendAnthropicChat = ({ messages: messages_, onText, providerName, onFinalM supportsSystemMessage, supportsTools, maxOutputTokens, - } = modelOptionsOfProvider(providerName, modelName_) + } = getModelCapabilities(providerName, modelName_) const { messages, separateSystemMessageStr } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsTools, }) diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts index 40eb880c..32b91d07 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts @@ -335,14 +335,16 @@ export const prepareFIMMessage = ({ }) => { let prefix = `\ -## You are a helpful coding assistant that performs autocomplete. ${!aiInstructions ? '' : `\ -## Instructions: -${aiInstructions.split('\n').map(line => `##${line}`).join('\n')}`} +// Instructions: +// Do not output an explanation. Try to avoid outputting comments. Only output the middle code. +${aiInstructions.split('\n').map(line => `//${line}`).join('\n')}`} ${messages.prefix}` const suffix = messages.suffix const stopTokens = messages.stopTokens - return { prefix, suffix, stopTokens, maxTokens: 300 } as const + const ret = { prefix, suffix, stopTokens, maxTokens: 300 } as const + console.log('ret', ret) + return ret }