Merge pull request #423 from voideditor/model-selection

Fix build?
This commit is contained in:
Andrew Pareles 2025-04-20 20:20:54 -07:00 committed by GitHub
commit 868736330a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 5674 additions and 7937 deletions

11111
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -106,8 +106,6 @@
"cross-spawn": "^7.0.6",
"diff": "^7.0.0",
"eslint-plugin-react": "^7.37.4",
"fast-json-stable-stringify": "^2.1.0",
"google-auth-library": "^9.15.1",
"groq-sdk": "^0.15.0",
"http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^7.0.2",
@ -175,7 +173,6 @@
"@vscode/v8-heap-parser": "^0.1.0",
"@vscode/vscode-perf": "^0.0.19",
"@webgpu/types": "^0.1.44",
"ajv": "^8.17.1",
"ansi-colors": "^3.2.3",
"asar": "^3.0.3",
"chromium-pickle-js": "^0.2.0",

View file

@ -38,7 +38,6 @@ registerAction2(class extends Action2 {
});
}
async run(accessor: ServicesAccessor): Promise<void> {
console.log('hi')
const n = accessor.get(IDummyService)
console.log('Hi', n._serviceBrand)
}

View file

@ -16,7 +16,7 @@ import { getErrorMessage, RawToolCallObj, RawToolParamsObj } from '../common/sen
import { generateUuid } from '../../../../base/common/uuid.js';
import { FeatureName, ModelSelection, ModelSelectionOptions } from '../common/voidSettingsTypes.js';
import { IVoidSettingsService } from '../common/voidSettingsService.js';
import { ToolCallParams, ToolResultType, toolNamesThatRequireApproval } from '../common/toolsServiceTypes.js';
import { approvalTypeOfToolName, ToolCallParams, ToolResultType } from '../common/toolsServiceTypes.js';
import { IToolsService } from './toolsService.js';
import { CancellationToken } from '../../../../base/common/cancellation.js';
import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js';
@ -33,8 +33,13 @@ import { truncate } from '../../../../base/common/strings.js';
import { THREAD_STORAGE_KEY } from '../common/storageKeys.js';
import { IConvertToLLMMessageService } from './convertToLLMMessageService.js';
import { timeout } from '../../../../base/common/async.js';
import { deepClone } from '../../../../base/common/objects.js';
// related to retrying when LLM message has error
const CHAT_RETRIES = 3
const RETRY_DELAY = 2500
export const findStagingSelectionIndex = (currentSelections: StagingSelectionItem[] | undefined, newSelection: StagingSelectionItem): number | null => {
if (!currentSelections) return null
@ -180,9 +185,12 @@ export interface IChatThreadService {
getCurrentThread(): ThreadType;
openNewThread(): void;
deleteThread(threadId: string): void;
switchToThread(threadId: string): void;
// thread selector
deleteThread(threadId: string): void;
duplicateThread(threadId: string): void;
// exposed getters/setters
// these all apply to current thread
getCurrentMessageState: (messageIdx: number) => UserMessageState
@ -194,6 +202,10 @@ export interface IChatThreadService {
getCurrentFocusedMessageIdx(): number | undefined;
isCurrentlyFocusingMessage(): boolean;
setCurrentlyFocusedMessageIdx(messageIdx: number | undefined): void;
dangerousSetState: (newState: ThreadsState) => void;
resetState: () => void;
// // current thread's staging selections
// closeCurrentStagingSelectionsInMessage(opts: { messageIdx: number }): void;
// closeCurrentStagingSelectionsInThread(): void;
@ -285,6 +297,16 @@ class ChatThreadService extends Disposable implements IChatThreadService {
}
dangerousSetState = (newState: ThreadsState) => {
this.state = newState
this._onDidChangeCurrentThread.fire()
}
resetState = () => {
this.state = { allThreads: {}, currentThreadId: null as unknown as string } // see constructor
this.openNewThread()
this._onDidChangeCurrentThread.fire()
}
// !!! this is important for properly restoring URIs from storage
// should probably re-use code from void/src/vs/base/common/marshalling.ts instead. but this is simple enough
private _convertThreadDataFromStorage(threadsStr: string): ChatThreads {
@ -368,7 +390,8 @@ class ChatThreadService extends Disposable implements IChatThreadService {
if (!messages) return false
const lastMsg = messages[messages.length - 1]
if (!lastMsg) return false
if (lastMsg.role === 'tool' && (lastMsg.type === 'running_now' || lastMsg.type === 'tool_request')) {
if (lastMsg.role === 'tool' && lastMsg.type !== 'invalid_params') {
this._editMessageInThread(threadId, messages.length - 1, tool)
return true
}
@ -385,9 +408,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
if (!thread) return // should never happen
const lastMsg = thread.messages[thread.messages.length - 1]
if (!(
lastMsg.role === 'tool' && (lastMsg.type === 'tool_request')
)) return // should never happen
if (!(lastMsg.role === 'tool' && lastMsg.type === 'tool_request')) return // should never happen
const callThisToolFirst: ToolMessage<ToolName> = lastMsg
@ -403,7 +424,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
const lastMsg = thread.messages[thread.messages.length - 1]
let params: ToolCallParams[ToolName]
if (lastMsg.role === 'tool' && (lastMsg.type === 'running_now' || lastMsg.type === 'tool_request')) {
if (lastMsg.role === 'tool' && lastMsg.type !== 'invalid_params') {
params = lastMsg.params
}
else return
@ -486,11 +507,13 @@ class ChatThreadService extends Disposable implements IChatThreadService {
if (toolName === 'edit_file') { this._addToolEditCheckpoint({ threadId, uri: (toolParams as ToolCallParams['edit_file']).uri }) }
// 2. if tool requires approval, break from the loop, awaiting approval
const toolRequiresApproval = toolNamesThatRequireApproval.has(toolName)
if (toolRequiresApproval) {
const autoApprove = this._settingsService.state.globalSettings.autoApprove
const approvalType = approvalTypeOfToolName[toolName]
if (approvalType) {
const autoApprove = this._settingsService.state.globalSettings.autoApprove[approvalType]
// add a tool_request because we use it for UI if a tool is loading (this should be improved in the future)
this._addMessageToThread(threadId, { role: 'tool', type: 'tool_request', content: '(never)', result: null, name: toolName, params: toolParams, id: toolId, rawParams: opts.unvalidatedToolParams })
this._addMessageToThread(threadId, { role: 'tool', type: 'tool_request', content: '(Awaiting user permission...)', result: null, name: toolName, params: toolParams, id: toolId, rawParams: opts.unvalidatedToolParams })
if (!autoApprove) {
return { awaitingUserApproval: true }
}
@ -519,6 +542,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
if (interrupted) { return { interrupted: true } } // the tool result is added where we interrupt, not here
}
catch (error) {
delete this._currentlyRunningToolInterruptor[threadId]
if (interrupted) { return { interrupted: true } } // the tool result is added where we interrupt, not here
@ -538,6 +562,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
// 5. add to history and keep going
this._updateLatestTool(threadId, { role: 'tool', type: 'success', params: toolParams, result: toolResult, name: toolName, content: toolResultStr, id: toolId, rawParams: opts.unvalidatedToolParams })
delete this._currentlyRunningToolInterruptor[threadId]
return {}
};
@ -582,12 +607,6 @@ class ChatThreadService extends Disposable implements IChatThreadService {
isRunningWhenEnd = undefined
nMessagesSent += 1
let resMessageIsDonePromise: (toolCall?: RawToolCallObj | undefined) => void // resolves when user approves this tool use (or if tool doesn't require approval)
const messageIsDonePromise = new Promise<RawToolCallObj | undefined>((res, rej) => { resMessageIsDonePromise = res })
// send llm message
this._setStreamState(threadId, { isRunning: 'LLM' }, 'merge')
const chatMessages = this.state.allThreads[threadId]?.messages ?? []
const { messages, separateSystemMessage } = await this._convertToLLMMessagesService.prepareLLMChatMessages({
chatMessages,
@ -595,15 +614,18 @@ class ChatThreadService extends Disposable implements IChatThreadService {
chatMode
})
let aborted = false
let shouldRetry = true
let nAttempts = 0
while (shouldRetry) {
shouldRetry = false
let resMessageIsDonePromise: (toolCall?: RawToolCallObj | undefined) => void // resolves when user approves this tool use (or if tool doesn't require approval)
const messageIsDonePromise = new Promise<RawToolCallObj | undefined>((res, rej) => { resMessageIsDonePromise = res })
// send llm message
this._setStreamState(threadId, { isRunning: 'LLM' }, 'merge')
const llmCancelToken = this._llmMessageService.sendLLMMessage({
messagesType: 'chatMessages',
chatMode,
@ -621,15 +643,16 @@ class ChatThreadService extends Disposable implements IChatThreadService {
resMessageIsDonePromise(toolCall) // resolve with tool calls
},
onError: (error) => {
onError: async (error) => {
const messageSoFar = this.streamState[threadId]?.displayContentSoFar ?? ''
const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? ''
this._setStreamState(threadId, { displayContentSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, toolCallSoFar: undefined }, 'merge')
if (nAttempts < CHAT_RETRIES) {
nAttempts += 1
shouldRetry = true
this._setStreamState(threadId, { displayContentSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, toolCallSoFar: undefined }, 'merge')
timeout(2500).then(() => { resMessageIsDonePromise() })
await timeout(RETRY_DELAY)
resMessageIsDonePromise()
}
else {
// const toolCallSoFar = this.streamState[threadId]?.toolCallSoFar
@ -641,9 +664,9 @@ class ChatThreadService extends Disposable implements IChatThreadService {
},
onAbort: () => {
// stop the loop to free up the promise, but don't modify state (already handled by whatever stopped it)
aborted = true
resMessageIsDonePromise()
this._metricsService.capture('Agent Loop Done (Aborted)', { nMessagesSent, chatMode })
aborted = true
},
})
@ -656,14 +679,15 @@ class ChatThreadService extends Disposable implements IChatThreadService {
}
this._setStreamState(threadId, { streamingToken: llmCancelToken }, 'merge') // new stream token for the new message
const toolCall = await messageIsDonePromise // wait for message to complete
if (shouldRetry) {
continue
}
if (aborted) {
return
}
this._setStreamState(threadId, { streamingToken: undefined }, 'merge') // streaming message is done
// this is a complete hack to make it so if an error loop was aborted, we stop (because onAbort does not get called if error happens instantly)
// maybe we should remove all the abort stuff and just make it so that we only go by state?
if (!this.streamState[threadId]?.isRunning) { return }
if (aborted) { return }
if (shouldRetry) { continue }
// call tool if there is one
const tool: RawToolCallObj | undefined = toolCall
if (tool) {
@ -671,9 +695,12 @@ class ChatThreadService extends Disposable implements IChatThreadService {
// stop if interrupted. we don't have to do this for llmMessage because we have a stream token for it and onAbort gets called, but we don't have the equivalent for tools.
// just detect tool interruption which is the same as chat interruption right now
if (!this.streamState[threadId]?.isRunning) { return }
if (aborted) { return }
if (interrupted) { return }
if (awaitingUserApproval) {
console.log('awaiting...')
isRunningWhenEnd = 'awaiting_user'
}
else {
@ -684,7 +711,6 @@ class ChatThreadService extends Disposable implements IChatThreadService {
} // end while (attempts)
} // end while (send message)
// if awaiting user approval, keep isRunning true, else end isRunning
this._setStreamState(threadId, { isRunning: isRunningWhenEnd }, 'merge')
@ -881,7 +907,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
const [_, toIdx] = c
if (toIdx === fromIdx) return
console.log(`going from ${fromIdx} to ${toIdx}`)
// console.log(`going from ${fromIdx} to ${toIdx}`)
// update the user's checkpoint
this._addUserModificationsToCurrCheckpoint({ threadId })
@ -1019,8 +1045,8 @@ We only need to do it for files that were edited since `from`, ie files between
if (!thread) return // should never happen
const llmCancelToken = this.streamState[threadId]?.streamingToken // currently streaming LLM on this thread
if (llmCancelToken === undefined && this.streamState[threadId]?.isRunning === 'LLM') {
// if about to call the other LLM, just wait for it by stopping right now
if (llmCancelToken === undefined && this.streamState[threadId]?.isRunning) {
// if about to call the other LLM, just wait for and stop now
return
}
// stop it (this simply resolves the promise to free up space)
@ -1199,8 +1225,9 @@ We only need to do it for files that were edited since `from`, ie files between
// else search codebase for `target`
let uris: URI[] = []
try {
const { result } = await this._toolsService.callTool['search_pathnames_only']({ queryStr: target, searchInFolder: null, pageNumber: 0 })
uris = result.uris
const { result } = await this._toolsService.callTool['search_pathnames_only']({ query: target, includePattern: null, pageNumber: 0 })
const { uris: uris_ } = await result
uris = uris_
} catch (e) {
return null
}
@ -1398,10 +1425,9 @@ We only need to do it for files that were edited since `from`, ie files between
const { allThreads: currentThreads } = this.state
for (const threadId in currentThreads) {
if (currentThreads[threadId]!.messages.length === 0) {
// switch to the thread
// switch to the existing empty thread and exit
this.switchToThread(threadId)
return
}
}
// otherwise, start a new thread
@ -1429,6 +1455,22 @@ We only need to do it for files that were edited since `from`, ie files between
this._setState({ ...this.state, allThreads: newThreads }, true)
}
duplicateThread(threadId: string) {
const { allThreads: currentThreads } = this.state
const threadToDuplicate = currentThreads[threadId]
if (!threadToDuplicate) return
const newThread = {
...deepClone(threadToDuplicate),
id: generateUuid(),
}
const newThreads = {
...currentThreads,
[newThread.id]: newThread,
}
this._storeAllThreads(newThreads)
this._setState({ allThreads: newThreads }, true)
}
private _addMessageToThread(threadId: string, message: ChatMessage) {
const { allThreads } = this.state

View file

@ -452,7 +452,6 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess
return voidRules.trim();
}
catch (e) {
console.log('Could not read .voidrules, continuing...')
return ''
}
}

View file

@ -10,11 +10,10 @@ import { createDecorator } from '../../../../platform/instantiation/common/insta
import { IFileService } from '../../../../platform/files/common/files.js';
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
import { ShallowDirectoryItem, ToolCallParams, ToolResultType } from '../common/toolsServiceTypes.js';
import { MAX_CHILDREN_URIs_PAGE } from './toolsService.js';
import { IExplorerService } from '../../files/browser/files.js';
import { SortOrder } from '../../files/common/files.js';
import { ExplorerItem } from '../../files/common/explorerModel.js';
import { MAX_DIRSTR_CHARS_TOTAL_BEGINNING, MAX_DIRSTR_CHARS_TOTAL_TOOL } from '../common/prompt/prompts.js';
import { MAX_CHILDREN_URIs_PAGE, MAX_DIRSTR_CHARS_TOTAL_BEGINNING, MAX_DIRSTR_CHARS_TOTAL_TOOL } from '../common/prompt/prompts.js';
const MAX_FILES_TOTAL = 300;
@ -111,14 +110,14 @@ export const computeDirectoryTree1Deep = async (
export const stringifyDirectoryTree1Deep = (params: ToolCallParams['ls_dir'], result: ToolResultType['ls_dir']): string => {
if (!result.children) {
return `Error: ${params.rootURI} is not a directory`;
return `Error: ${params.uri} is not a directory`;
}
let output = '';
const entries = result.children;
if (!result.hasPrevPage) { // is first page
output += `${params.rootURI.fsPath}\n`;
output += `${params.uri.fsPath}\n`;
}
for (let i = 0; i < entries.length; i++) {
@ -419,7 +418,7 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService {
}
if (cutOff) {
return `${str}\n${cutOffMessage}`
return `${str.trimEnd()}\n${cutOffMessage}`
}
return str

View file

@ -31,14 +31,11 @@ import { mountCtrlK } from './react/out/quick-edit-tsx/index.js'
import { QuickEditPropsType } from './quickEditActions.js';
import { IModelContentChangedEvent } from '../../../../editor/common/textModelEvents.js';
import { extractCodeFromFIM, extractCodeFromRegular, ExtractedSearchReplaceBlock, extractSearchReplaceBlocks } from '../common/helpers/extractCodeFromResult.js';
import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js';
import { isMacintosh } from '../../../../base/common/platform.js';
import { INotificationService, } from '../../../../platform/notification/common/notification.js';
import { EditorOption } from '../../../../editor/common/config/editorOptions.js';
import { Emitter } from '../../../../base/common/event.js';
import { VOID_OPEN_SETTINGS_ACTION_ID } from './voidSettingsPane.js';
import { ICommandService } from '../../../../platform/commands/common/commands.js';
import { ILLMMessageService } from '../common/sendLLMMessageService.js';
import { LLMChatMessage, OnError, errorDetails } from '../common/sendLLMMessageTypes.js';
import { LLMChatMessage } from '../common/sendLLMMessageTypes.js';
import { IMetricsService } from '../common/metricsService.js';
import { IEditCodeService, AddCtrlKOpts, StartApplyingOpts, CallBeforeStartApplyingOpts, } from './editCodeServiceInterface.js';
import { IVoidSettingsService } from '../common/voidSettingsService.js';
@ -48,6 +45,8 @@ import { deepClone } from '../../../../base/common/objects.js';
import { acceptBg, acceptBorder, buttonFontSize, buttonTextColor, rejectBg, rejectBorder } from '../common/helpers/colors.js';
import { DiffArea, Diff, CtrlKZone, VoidFileSnapshot, DiffAreaSnapshotEntry, diffAreaSnapshotKeys, DiffZone, TrackingZone, ComputedDiff } from '../common/editCodeServiceTypes.js';
import { IConvertToLLMMessageService } from './convertToLLMMessageService.js';
// import { isMacintosh } from '../../../../base/common/platform.js';
// import { VOID_OPEN_SETTINGS_ACTION_ID } from './voidSettingsPane.js';
const configOfBG = (color: Color) => {
return { dark: color, light: color, hcDark: color, hcLight: color, }
@ -199,7 +198,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
@IConsistentEditorItemService private readonly _consistentEditorItemService: IConsistentEditorItemService,
@IMetricsService private readonly _metricsService: IMetricsService,
@INotificationService private readonly _notificationService: INotificationService,
@ICommandService private readonly _commandService: ICommandService,
// @ICommandService private readonly _commandService: ICommandService,
@IVoidSettingsService private readonly _settingsService: IVoidSettingsService,
// @IFileService private readonly _fileService: IFileService,
@IVoidModelService private readonly _voidModelService: IVoidModelService,
@ -279,24 +278,24 @@ class EditCodeService extends Disposable implements IEditCodeService {
private _notifyError = (e: Parameters<OnError>[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
})
}
// private _notifyError = (e: Parameters<OnError>[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
// })
// }
@ -1393,7 +1392,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
// throws
const onError = (e: { message: string; fullError: Error | null; }) => {
this._notifyError(e)
// this._notifyError(e)
onDone()
this._undoHistory(uri)
throw e.fullError
@ -1612,7 +1611,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
}
const onError = (e: { message: string; fullError: Error | null; }) => {
this._notifyError(e)
// this._notifyError(e)
onDone()
this._undoHistory(uri)
throw e.fullError || new Error(e.message) // throw error h

View file

@ -4,7 +4,6 @@
*--------------------------------------------------------------------------------------*/
import React, { Component, ErrorInfo, ReactNode } from 'react';
import { ErrorDisplay } from './ErrorDisplay.js';
import { WarningBox } from '../void-settings-tsx/WarningBox.js';
interface Props {

View file

@ -3,12 +3,12 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { useState } from 'react';
import { IconShell1 } from '../markdown/ApplyBlockHoverButtons.js';
import { useAccessor, useChatThreadsState } from '../util/services.js';
import { useMemo, useState } from 'react';
import { CopyButton, IconShell1 } from '../markdown/ApplyBlockHoverButtons.js';
import { useAccessor, useChatThreadsState, useChatThreadsStreamState, useFullChatThreadsStreamState, useSettingsState } from '../util/services.js';
import { IconX } from './SidebarChat.js';
import { Check, Trash2, X } from 'lucide-react';
import { ThreadType } from '../../../chatThreadService.js';
import { Check, Copy, Icon, LoaderCircle, MessageCircleQuestion, Trash2, UserCheck, X } from 'lucide-react';
import { IsRunningType, ThreadType } from '../../../chatThreadService.js';
export const OldSidebarThreadSelector = () => {
@ -153,6 +153,14 @@ export const PastThreadsList = ({ className = '' }: { className?: string }) => {
const threadsState = useChatThreadsState()
const { allThreads } = threadsState
const streamState = useFullChatThreadsStreamState()
const runningThreadIds: { [threadId: string]: IsRunningType | undefined } = {}
for (const threadId in streamState) {
const isRunning = streamState[threadId]?.isRunning
if (isRunning) { runningThreadIds[threadId] = isRunning }
}
if (!allThreads) {
return <div key="error" className="p-1">{`Error accessing chat history.`}</div>;
}
@ -183,6 +191,7 @@ export const PastThreadsList = ({ className = '' }: { className?: string }) => {
idx={i}
hoveredIdx={hoveredIdx}
setHoveredIdx={setHoveredIdx}
isRunning={runningThreadIds[pastThread.id]}
/>
);
})
@ -238,6 +247,21 @@ const formatTime = (date: Date) => {
};
const DuplicateButton = ({ threadId }: { threadId: string }) => {
const accessor = useAccessor()
const chatThreadsService = accessor.get('IChatThreadService')
return <IconShell1
Icon={Copy}
className='size-[11px]'
onClick={() => { chatThreadsService.duplicateThread(threadId); }}
data-tooltip-id='void-tooltip'
data-tooltip-place='top'
data-tooltip-content='Duplicate thread'
>
</IconShell1>
}
const TrashButton = ({ threadId }: { threadId: string }) => {
const accessor = useAccessor()
@ -271,18 +295,51 @@ const TrashButton = ({ threadId }: { threadId: string }) => {
onClick={() => { setIsTrashPressed(true); }}
data-tooltip-id='void-tooltip'
data-tooltip-place='top'
data-tooltip-content='Delete thread?'
data-tooltip-content='Delete thread'
/>
)
}
const PastThreadElement = ({ pastThread, idx, hoveredIdx, setHoveredIdx }: { pastThread: ThreadType, idx: number, hoveredIdx: number | null, setHoveredIdx: (idx: number | null) => void }) => {
const PastThreadElement = ({ pastThread, idx, hoveredIdx, setHoveredIdx, isRunning }: {
pastThread: ThreadType,
idx: number,
hoveredIdx: number | null,
setHoveredIdx: (idx: number | null) => void,
isRunning: IsRunningType | undefined,
}
) => {
const accessor = useAccessor()
const chatThreadsService = accessor.get('IChatThreadService')
const sidebarStateService = accessor.get('ISidebarStateService')
// const settingsState = useSettingsState()
// const convertService = accessor.get('IConvertToLLMMessageService')
// const chatMode = settingsState.globalSettings.chatMode
// const modelSelection = settingsState.modelSelectionOfFeature?.Chat ?? null
// const copyChatButton = <CopyButton
// codeStr={async () => {
// const { messages } = await convertService.prepareLLMChatMessages({
// chatMessages: currentThread.messages,
// chatMode,
// modelSelection,
// })
// return JSON.stringify(messages, null, 2)
// }}
// toolTipName={modelSelection === null ? 'Copy As Messages Payload' : `Copy As ${displayInfoOfProviderName(modelSelection.providerName).title} Payload`}
// />
// const currentThread = chatThreadsService.getCurrentThread()
// const copyChatButton2 = <CopyButton
// codeStr={async () => {
// return JSON.stringify(currentThread.messages, null, 2)
// }}
// toolTipName={`Copy As Void Chat`}
// />
let firstMsg = null;
const firstUserMsgIdx = pastThread.messages.findIndex((msg) => msg.role === 'user');
@ -319,13 +376,28 @@ const PastThreadElement = ({ pastThread, idx, hoveredIdx, setHoveredIdx }: { pas
>
<div className="flex items-center justify-between gap-1">
<span className="flex items-center gap-2 min-w-0 overflow-hidden">
{/* spinner */}
{isRunning === 'LLM' || isRunning === 'tool' ? <LoaderCircle className="animate-spin bg-void-stroke-1 flex-shrink-0 flex-grow-0" size={14} />
:
isRunning === 'awaiting_user' ? <MessageCircleQuestion className="bg-void-stroke-1 flex-shrink-0 flex-grow-0" size={14} />
:
null}
{/* name */}
<span className="truncate overflow-hidden text-ellipsis">{firstMsg}</span>
</span>
<div className="flex items-center gap-2 opacity-60">
<div className="flex items-center gap-x-1 opacity-60">
{idx === hoveredIdx ?
<TrashButton threadId={pastThread.id} />
: detailsHTML
<>
{/* trash icon */}
<DuplicateButton threadId={pastThread.id} />
{/* trash icon */}
<TrashButton threadId={pastThread.id} />
</>
: <>
{detailsHTML}
</>
}
</div>
</div>

View file

@ -48,6 +48,80 @@ export const WidgetComponent = <CtorParams extends any[], Instance>({ ctor, prop
return <div ref={containerRef} className={className === undefined ? `w-full` : className}>{children}</div>
}
type GenerateNextOptions = (newPathText: string) => Option[]
type Option = {
name: string,
displayName: string,
} & (
| { nextOptions: Option[], generateNextOptions?: undefined }
| { nextOptions?: undefined, generateNextOptions: GenerateNextOptions }
| { nextOptions?: undefined, generateNextOptions?: undefined }
)
const getOptionsAtPath = (accessor: ReturnType<typeof useAccessor>, path: string[], newPathText: string) => {
const allOptions: Option[] = [
{
name: 'files',
displayName: 'files',
generateNextOptions: () => [
{ name: 'a.txt', displayName: 'a.txt', },
{ name: 'b.txt', displayName: 'b.txt', },
{ name: 'c.txt', displayName: 'c.txt', },
{ name: 'd.txt', displayName: 'd.txt', },
{ name: 'e.txt', displayName: 'e.txt', },
{ name: 'f.txt', displayName: 'f.txt', },
{ name: 'g.txt', displayName: 'g.txt', },
{ name: '!a.txt', displayName: '!a.txt', },
{ name: '!b.txt', displayName: '!b.txt', },
{ name: '!c.txt', displayName: '!c.txt', },
{ name: '!d.txt', displayName: '!d.txt', },
{ name: '!e.txt', displayName: '!e.txt', },
{ name: '!f.txt', displayName: '!f.txt', },
{ name: '!g.txt', displayName: '!g.txt', },
]
},
{
name: 'folders',
displayName: 'folders',
nextOptions: [
{ name: 'FOLDER', displayName: 'FOLDER', },
]
},
]
// follow the path in the optionsTree (until the last path element)
let nextOptionsAtPath = allOptions
let generateNextOptionsAtPath: GenerateNextOptions | undefined = undefined
for (const pn of path) {
const selectedOption = nextOptionsAtPath.find(o => o.name.toLowerCase() === pn.toLowerCase())
if (!selectedOption) return;
nextOptionsAtPath = selectedOption.nextOptions! // assume nextOptions exists until we hit the very last option (the path will never contain the last possible option)
generateNextOptionsAtPath = selectedOption.generateNextOptions
}
if (generateNextOptionsAtPath) {
nextOptionsAtPath = generateNextOptionsAtPath(newPathText)
}
const optionsAtPath = nextOptionsAtPath.filter(o => o.name.includes(newPathText))
return optionsAtPath
}
export type TextAreaFns = { setValue: (v: string) => void, enable: () => void, disable: () => void }
type InputBox2Props = {
@ -64,8 +138,235 @@ type InputBox2Props = {
}
export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(function X({ initValue, placeholder, multiline, fnsRef, className, onKeyDown, onFocus, onBlur, onChangeText }, ref) {
// mirrors whatever is in ref
const accessor = useAccessor()
const toolsService = accessor.get('IToolsService')
const textAreaRef = useRef<HTMLTextAreaElement | null>(null)
const selectedOptionRef = useRef<HTMLDivElement>(null);
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [path, setPath] = useState<string[]>([]);
const [optionIdx, setOptionIdx] = useState<number>(0);
const [options, setOptions] = useState<Option[]>([]);
const [newPathText, setNewPathText] = useState<string>('');
const insertTextAtCursor = (text: string) => {
const textarea = textAreaRef.current;
if (!textarea) return;
// Focus the textarea first
textarea.focus();
// The most reliable way to simulate typing is to use execCommand
// which will trigger all the appropriate native events
document.execCommand('insertText', false, text);
// React's onChange relies on a SyntheticEvent system
// The best way to ensure it runs is to call callbacks directly
if (onChangeText) {
onChangeText(textarea.value);
}
adjustHeight();
};
const onSelectOption = () => {
if (!options.length) { return; }
const option = options[optionIdx];
const newPath = [...path, option.name]
const isLastOption = !option.generateNextOptions && !option.nextOptions
setPath(newPath)
setNewPathText('')
setOptionIdx(0)
if (isLastOption) {
setIsMenuOpen(false)
insertTextAtCursor(`TODO-${option.displayName}`)
}
else {
setOptions(getOptionsAtPath(accessor, newPath, '') || [])
}
}
const onRemoveOption = () => {
const newPath = [...path.slice(0, path.length - 1)]
setPath(newPath)
setNewPathText('')
setOptionIdx(0)
setOptions(getOptionsAtPath(accessor, newPath, '') || [])
}
const onOpenOptionMenu = () => {
setPath([])
setNewPathText('')
setIsMenuOpen(true);
setOptionIdx(0);
setOptions(getOptionsAtPath(accessor, [], '') || []);
}
const onCloseOptionMenu = () => {
setIsMenuOpen(false);
}
const onNavigateUp = () => {
if (options.length === 0) return;
setOptionIdx((prevIdx) => (prevIdx - 1 + options.length) % options.length);
}
const onNavigateDown = () => {
if (options.length === 0) return;
setOptionIdx((prevIdx) => (prevIdx + 1) % options.length);
}
const onPathTextChange = (newStr: string) => {
setNewPathText(newStr);
setOptions(getOptionsAtPath(accessor, path, newStr) || []);
}
const onMenuKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'ArrowUp') {
onNavigateUp();
} else if (e.key === 'ArrowDown') {
onNavigateDown();
} else if (e.key === 'ArrowLeft') {
onSelectOption();
} else if (e.key === 'ArrowRight') {
onSelectOption();
} else if (e.key === 'Enter') {
onSelectOption();
} else if (e.key === 'Escape') {
onCloseOptionMenu()
} else if (e.key === 'Backspace') {
if (!newPathText) { // No text remaining
if (path.length === 0) {
onCloseOptionMenu()
} else {
onRemoveOption();
}
}
else if (e.altKey || e.ctrlKey || e.metaKey) { // Ctrl+Backspace
onPathTextChange('')
}
else { // Backspace
onPathTextChange(newPathText.slice(0, -1))
}
} else if (e.key.length === 1) {
if (e.altKey || e.ctrlKey || e.metaKey) { // Ctrl+letter
// do nothing
}
else { // letter
onPathTextChange(newPathText + e.key)
}
}
e.preventDefault();
e.stopPropagation();
};
// scroll the selected optionIdx into view on optionIdx and newPathText changes
useEffect(() => {
if (isMenuOpen && selectedOptionRef.current) {
selectedOptionRef.current.scrollIntoView({
behavior: 'instant',
block: 'nearest',
inline: 'nearest',
});
}
}, [optionIdx, isMenuOpen, newPathText, selectedOptionRef]);
const measureRef = useRef<HTMLDivElement>(null);
const gapPx = 2
const offsetPx = 2
const {
x,
y,
strategy,
refs,
middlewareData,
update
} = useFloating({
open: isMenuOpen,
onOpenChange: setIsMenuOpen,
placement: 'top',
middleware: [
offset({ mainAxis: gapPx, crossAxis: offsetPx }),
flip({
boundary: document.body,
padding: 8
}),
shift({
boundary: document.body,
padding: 8,
}),
size({
apply({ availableHeight, elements, rects }) {
const maxHeight = Math.min(availableHeight)
Object.assign(elements.floating.style, {
maxHeight: `${maxHeight}px`,
overflowY: 'auto',
// Ensure the width isn't constrained by the parent
width: `${Math.max(
rects.reference.width,
measureRef.current?.offsetWidth ?? 0
)}px`
});
},
padding: 8,
// Use viewport as boundary instead of any parent element
boundary: document.body,
}),
],
whileElementsMounted: autoUpdate,
strategy: 'fixed',
});
useEffect(() => {
if (!isMenuOpen) return;
const handleClickOutside = (event: MouseEvent) => {
const target = event.target as Node;
const floating = refs.floating.current;
const reference = refs.reference.current;
// Check if reference is an HTML element before using contains
const isReferenceHTMLElement = reference && 'contains' in reference;
if (
floating &&
(!isReferenceHTMLElement || !reference.contains(target)) &&
!floating.contains(target)
) {
setIsMenuOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, [isMenuOpen, refs.floating, refs.reference]);
const [isEnabled, setEnabled] = useState(true)
const adjustHeight = useCallback(() => {
@ -104,18 +405,20 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
return (
return <>
<textarea
autoFocus={false}
ref={useCallback((r: HTMLTextAreaElement | null) => {
if (fnsRef)
fnsRef.current = fns
refs.setReference(r)
textAreaRef.current = r
if (typeof ref === 'function') ref(r)
else if (ref) ref.current = r
adjustHeight()
}, [fnsRef, fns, setEnabled, adjustHeight, ref])}
}, [fnsRef, fns, setEnabled, adjustHeight, ref, refs])}
onFocus={onFocus}
onBlur={onBlur}
@ -130,7 +433,16 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
// inputBorder: asCssVariable(inputBorder),
}}
onChange={useCallback(() => {
onInput={useCallback((event: React.FormEvent<HTMLTextAreaElement>) => {
const latestChange = (event.nativeEvent as InputEvent).data;
if (latestChange === '@') {
onOpenOptionMenu()
}
}, [onOpenOptionMenu, accessor])}
onChange={useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
const r = textAreaRef.current
if (!r) return
onChangeText?.(r.value)
@ -138,18 +450,75 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
}, [onChangeText, adjustHeight])}
onKeyDown={useCallback((e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (isMenuOpen) {
onMenuKeyDown(e)
return;
}
if (e.key === 'Enter') {
// Shift + Enter when multiline = newline
const shouldAddNewline = e.shiftKey && multiline
if (!shouldAddNewline) e.preventDefault(); // prevent newline from being created
}
onKeyDown?.(e)
}, [onKeyDown, multiline])}
}, [onKeyDown, onMenuKeyDown, multiline])}
rows={1}
placeholder={placeholder}
/>
)
<div>{`idx ${optionIdx}`}</div>
{isMenuOpen && (
<div
ref={refs.setFloating}
className="z-[100] bg-void-bg-1 border-void-border-3 border rounded shadow-lg"
style={{
position: strategy,
top: y ?? 0,
left: x ?? 0,
width: refs.reference.current instanceof HTMLElement ? refs.reference.current.offsetWidth : 0
}}
onWheel={(e) => e.stopPropagation()}
>
<div className="py-1">
{/* Path navigation breadcrumbs */}
<div className="px-2 py-1 text-void-fg-3 text-sm border-b border-void-border-3">
{[...path, newPathText].join(' > ')}
</div>
{/* Options list */}
{options.length === 0 ? (
<div className="px-3 py-2 text-void-fg-3">No options available</div>
) : (
options.map((o, oIdx) => (
<div
ref={oIdx === optionIdx ? selectedOptionRef : null}
key={o.name}
className={`px-3 py-1.5 cursor-pointer bg-void-bg-2 ${oIdx === optionIdx ? 'bg-void-bg-2-hover' : ''}`}
onClick={() => { onSelectOption(); }}
>
<div className="flex items-center">
<span className="text-void-fg-1">{o.displayName}</span>
{o.nextOptions || o.generateNextOptions ? (
<svg className="ml-2 h-3 w-3 text-void-fg-3" viewBox="0 0 12 12" fill="none">
<path
d="M4.5 2.5L8 6L4.5 9.5"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
) : null}
</div>
</div>
))
)}
</div>
</div>
)}
</>
})

View file

@ -304,6 +304,16 @@ export const useChatThreadsStreamState = (threadId: string) => {
return s
}
export const useFullChatThreadsStreamState = () => {
const [s, ss] = useState(chatThreadsStreamState)
useEffect(() => {
ss(chatThreadsStreamState)
const listener = () => { ss(chatThreadsStreamState) }
chatThreadsStreamStateListeners.add(listener)
return () => { chatThreadsStreamStateListeners.delete(listener) }
}, [ss])
return s
}

View file

@ -9,8 +9,9 @@ import { Brain, Check, ChevronRight, DollarSign, ExternalLink, Lock, X } from 'l
import { displayInfoOfProviderName, ProviderName, providerNames, refreshableProviderNames } from '../../../../common/voidSettingsTypes.js';
import { getModelCapabilities, ollamaRecommendedModels } from '../../../../common/modelCapabilities.js';
import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js';
import { AddModelInputBox, AnimatedCheckmarkButton, ollamaSetupInstructions, OneClickSwitchButton, SettingsForProvider } from '../void-settings-tsx/Settings.js';
import { AddModelInputBox, AnimatedCheckmarkButton, OllamaSetupInstructions, OneClickSwitchButton, SettingsForProvider } from '../void-settings-tsx/Settings.js';
import { ColorScheme } from '../../../../../../../platform/theme/common/theme.js';
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js';
const OVERRIDE_VALUE = false
@ -29,7 +30,9 @@ export const VoidOnboarding = () => {
transition-all duration-1000 ${isOnboardingComplete ? 'opacity-0 pointer-events-none' : 'opacity-100 pointer-events-auto'}
`}
>
<VoidOnboardingContent />
<ErrorBoundary>
<VoidOnboardingContent />
</ErrorBoundary>
</div>
</div>
)
@ -303,11 +306,11 @@ const TableOfModelsForProvider = ({ providerName }: { providerName: ProviderName
// info used to show the table
const infoOfModelName: Record<string, { showAsDefault: boolean, isDownloaded: boolean }> = {}
const infoOfModelName: Record<string, { showAsDefault: boolean, isDownloaded: boolean } | undefined> = {}
voidSettingsState.settingsOfProvider[providerName].models.forEach(m => {
infoOfModelName[m.modelName] = {
showAsDefault: m.type === 'default',
showAsDefault: m.type !== 'custom',
isDownloaded: true
}
})
@ -317,7 +320,7 @@ const TableOfModelsForProvider = ({ providerName }: { providerName: ProviderName
for (const modelName of ollamaRecommendedModels) {
if (modelName in infoOfModelName) continue
infoOfModelName[modelName] = {
...infoOfModelName[modelName],
isDownloaded: infoOfModelName[modelName]?.isDownloaded ?? false,
showAsDefault: true,
}
}
@ -339,7 +342,7 @@ const TableOfModelsForProvider = ({ providerName }: { providerName: ProviderName
</thead>
<tbody>
{Object.keys(infoOfModelName).map(modelName => {
const { showAsDefault, isDownloaded } = infoOfModelName[modelName]
const { showAsDefault, isDownloaded } = infoOfModelName[modelName] ?? {}
const capabilities = getModelCapabilities(providerName, modelName)
@ -380,7 +383,7 @@ const TableOfModelsForProvider = ({ providerName }: { providerName: ProviderName
{/* <td className="py-2 px-3"><YesNoText val={!!reasoningCapabilities} /></td> */}
{isDetectableLocally && <td className="py-2 px-3 flex items-center justify-center">{!!isDownloaded ? <Check className="w-4 h-4" /> : <></>}</td>}
{providerName === 'ollama' && <th className="py-2 px-3">
<OllamaDownloadOrRemoveModelButton modelName={modelName} isModelInstalled={infoOfModelName[modelName].isDownloaded} sizeGb={downloadable && downloadable.sizeGb} />
<OllamaDownloadOrRemoveModelButton modelName={modelName} isModelInstalled={!!infoOfModelName[modelName]?.isDownloaded} sizeGb={downloadable && downloadable.sizeGb} />
</th>}
</tr>
@ -388,10 +391,13 @@ const TableOfModelsForProvider = ({ providerName }: { providerName: ProviderName
})}
<tr className="hover:bg-void-bg-3/50">
<td className="py-2 px-3 text-void-accent">
<AddModelInputBox
key={providerName}
providerName={providerName}
compact={true} />
<ErrorBoundary>
<AddModelInputBox
key={providerName}
providerName={providerName}
compact={true}
/>
</ErrorBoundary>
</td>
<td colSpan={4}></td>
</tr>
@ -672,19 +678,22 @@ const VoidOnboardingContent = () => {
{ id: 'cheap', label: 'Affordable' },
{ id: 'all', label: 'All' }
].map(option => (
<button
<ErrorBoundary
key={option.id}
onClick={() => setWantToUseOption(option.id as WantToUseOption)}
className={`py-1 px-2 text-xs cursor-pointer whitespace-nowrap rounded-sm transition-colors ${wantToUseOption === option.id
? 'dark:text-white text-black font-medium'
: 'text-void-fg-3 hover:text-void-fg-2'
}`}
data-tooltip-id='void-tooltip'
data-tooltip-content={`${option.label} providers`}
data-tooltip-place='bottom'
>
{option.label}
</button>
<button
onClick={() => setWantToUseOption(option.id as WantToUseOption)}
className={`py-1 px-2 text-xs cursor-pointer whitespace-nowrap rounded-sm transition-colors ${wantToUseOption === option.id
? 'dark:text-white text-black font-medium'
: 'text-void-fg-3 hover:text-void-fg-2'
}`}
data-tooltip-id='void-tooltip'
data-tooltip-content={`${option.label} providers`}
data-tooltip-place='bottom'
>
{option.label}
</button>
</ErrorBoundary>
))}
</div>
@ -693,108 +702,129 @@ const VoidOnboardingContent = () => {
{/* Provider Buttons - Modified to use separate components for each tab */}
<div className="mb-2 w-full">
{/* Intelligent tab */}
<div className={`flex flex-wrap items-center w-full ${wantToUseOption === 'smart' ? 'flex' : 'hidden'}`}>
{providerNamesOfWantToUseOption['smart'].map((providerName) => {
const isSelected = selectedIntelligentProvider === providerName;
return (
<button
key={providerName}
onClick={() => setSelectedIntelligentProvider(providerName)}
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-all duration-300
<ErrorBoundary>
<div className={`flex flex-wrap items-center w-full ${wantToUseOption === 'smart' ? 'flex' : 'hidden'}`}>
{providerNamesOfWantToUseOption['smart'].map((providerName) => {
const isSelected = selectedIntelligentProvider === providerName;
return (
<button
key={providerName}
onClick={() => setSelectedIntelligentProvider(providerName)}
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-all duration-300
${isSelected ? 'bg-zinc-100 text-zinc-900 shadow-sm border-white/80' : 'bg-zinc-100/40 hover:bg-zinc-100/50 text-zinc-900 border-white/20'}`}
>
{displayInfoOfProviderName(providerName).title}
</button>
);
})}
</div>
>
{displayInfoOfProviderName(providerName).title}
</button>
);
})}
</div>
</ErrorBoundary>
{/* Private tab */}
<div className={`flex flex-wrap items-center w-full ${wantToUseOption === 'private' ? 'flex' : 'hidden'}`}>
{providerNamesOfWantToUseOption['private'].map((providerName) => {
const isSelected = selectedPrivateProvider === providerName;
return (
<button
key={providerName}
onClick={() => setSelectedPrivateProvider(providerName)}
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-all duration-300
<ErrorBoundary>
<div className={`flex flex-wrap items-center w-full ${wantToUseOption === 'private' ? 'flex' : 'hidden'}`}>
{providerNamesOfWantToUseOption['private'].map((providerName) => {
const isSelected = selectedPrivateProvider === providerName;
return (
<button
key={providerName}
onClick={() => setSelectedPrivateProvider(providerName)}
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-all duration-300
${isSelected ? 'bg-zinc-100 text-zinc-900 shadow-sm border-white/80' : 'bg-zinc-100/40 hover:bg-zinc-100/50 text-zinc-900 border-white/20'}`}
>
{displayInfoOfProviderName(providerName).title}
</button>
);
})}
</div>
>
{displayInfoOfProviderName(providerName).title}
</button>
);
})}
</div>
</ErrorBoundary>
{/* Affordable tab */}
<div className={`flex flex-wrap items-center w-full ${wantToUseOption === 'cheap' ? 'flex' : 'hidden'}`}>
{providerNamesOfWantToUseOption['cheap'].map((providerName) => {
const isSelected = selectedAffordableProvider === providerName;
return (
<button
key={providerName}
onClick={() => setSelectedAffordableProvider(providerName)}
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-all duration-300
<ErrorBoundary>
<div className={`flex flex-wrap items-center w-full ${wantToUseOption === 'cheap' ? 'flex' : 'hidden'}`}>
{providerNamesOfWantToUseOption['cheap'].map((providerName) => {
const isSelected = selectedAffordableProvider === providerName;
return (
<button
key={providerName}
onClick={() => setSelectedAffordableProvider(providerName)}
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-all duration-300
${isSelected ? 'bg-zinc-100 text-zinc-900 shadow-sm border-white/80' : 'bg-zinc-100/40 hover:bg-zinc-100/50 text-zinc-900 border-white/20'}`}
>
{displayInfoOfProviderName(providerName).title}
</button>
);
})}
</div>
>
{displayInfoOfProviderName(providerName).title}
</button>
);
})}
</div>
</ErrorBoundary>
{/* All tab */}
<div className={`flex flex-wrap items-center w-full ${wantToUseOption === 'all' ? 'flex' : 'hidden'}`}>
{providerNames.map((providerName) => {
const isSelected = selectedAllProvider === providerName;
return (
<button
key={providerName}
onClick={() => setSelectedAllProvider(providerName)}
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-all duration-300
<ErrorBoundary>
<div className={`flex flex-wrap items-center w-full ${wantToUseOption === 'all' ? 'flex' : 'hidden'}`}>
{providerNames.map((providerName) => {
const isSelected = selectedAllProvider === providerName;
return (
<button
key={providerName}
onClick={() => setSelectedAllProvider(providerName)}
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-all duration-300
${isSelected ? 'bg-zinc-100 text-zinc-900 shadow-sm border-white/80' : 'bg-zinc-100/40 hover:bg-zinc-100/50 text-zinc-900 border-white/20'}`}
>
{displayInfoOfProviderName(providerName).title}
</button>
);
})}
</div>
>
{displayInfoOfProviderName(providerName).title}
</button>
);
})}
</div>
</ErrorBoundary>
</div>
{/* Description */}
<div className="text-left self-start text-sm text-void-fg-3 px-2 py-1">
<ChatMarkdownRender string={detailedDescOfWantToUseOption[wantToUseOption]} chatMessageLocation={undefined} />
</div>
<ErrorBoundary>
<div className="text-left self-start text-sm text-void-fg-3 px-2 py-1">
<ChatMarkdownRender string={detailedDescOfWantToUseOption[wantToUseOption]} chatMessageLocation={undefined} />
</div>
</ErrorBoundary>
{/* ModelsTable and ProviderFields */}
{selectedProviderName && <div className='mt-4 w-fit mx-auto'>
{/* Models Table */}
<TableOfModelsForProvider providerName={selectedProviderName} />
<ErrorBoundary>
<TableOfModelsForProvider providerName={selectedProviderName} />
</ErrorBoundary>
{/* Add provider section - simplified styling */}
<div className='mb-5 mt-8 mx-auto'>
<div className=''>
Add {displayInfoOfProviderName(selectedProviderName).title}
<ErrorBoundary>
<div className=''>
Add {displayInfoOfProviderName(selectedProviderName).title}
<div className='my-4'>
{selectedProviderName === 'ollama' ? <OllamaSetupInstructions /> : ''}
</div>
<div className='my-4'>
{selectedProviderName === 'ollama' ? ollamaSetupInstructions : ''}
</div>
</ErrorBoundary>
</div>
{selectedProviderName &&
<SettingsForProvider providerName={selectedProviderName} showProviderTitle={false} showProviderSuggestions={false} />
}
<ErrorBoundary>
{selectedProviderName &&
<SettingsForProvider providerName={selectedProviderName} showProviderTitle={false} showProviderSuggestions={false} />
}
</ErrorBoundary>
{/* Button and status indicators */}
{!didFillInProviderSettings ? <p className="text-xs text-void-fg-3 mt-2">Please fill in all fields to continue</p>
: !isAtLeastOneModel ? <p className="text-xs text-void-fg-3 mt-2">Please add a model to continue</p>
: !isApiKeyLongEnoughIfApiKeyExists ? <p className="text-xs text-void-fg-3 mt-2">Please enter a valid API key</p>
: <AnimatedCheckmarkButton className='text-xs text-void-fg-3 mt-2' text='Added' />}
<ErrorBoundary>
{!didFillInProviderSettings ? <p className="text-xs text-void-fg-3 mt-2">Please fill in all fields to continue</p>
: !isAtLeastOneModel ? <p className="text-xs text-void-fg-3 mt-2">Please add a model to continue</p>
: !isApiKeyLongEnoughIfApiKeyExists ? <p className="text-xs text-void-fg-3 mt-2">Please enter a valid API key</p>
: <AnimatedCheckmarkButton className='text-xs text-void-fg-3 mt-2' text='Added' />}
</ErrorBoundary>
</div>
</div>}
@ -802,10 +832,11 @@ const VoidOnboardingContent = () => {
}
bottom={
<FadeIn delayMs={50} durationMs={10}>
{prevAndNextButtons}
</FadeIn>
<ErrorBoundary>
<FadeIn delayMs={50} durationMs={10}>
{prevAndNextButtons}
</FadeIn>
</ErrorBoundary>
}
/>,
@ -864,7 +895,9 @@ const VoidOnboardingContent = () => {
return <div key={pageIndex} className="w-full h-full text-left mx-auto overflow-y-scroll flex flex-col items-center justify-around">
{contentOfIdx[pageIndex]}
<ErrorBoundary>
{contentOfIdx[pageIndex]}
</ErrorBoundary>
</div>
}

View file

@ -3,7 +3,7 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react'
import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidStatefulModelInfo, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, nonlocalProviderNames, localProviderNames, GlobalSettingName, featureNames, displayInfoOfFeatureName, isProviderNameDisabled, FeatureName, hasDownloadButtonsOnModelsProviderNames, subTextMdOfProviderName } from '../../../../common/voidSettingsTypes.js'
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
import { VoidButtonBgDarken, VoidCustomDropdownBox, VoidInputBox2, VoidSimpleInputBox, VoidSwitch } from '../util/inputs.js'
@ -16,7 +16,8 @@ import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js'
import { WarningBox } from './WarningBox.js'
import { os } from '../../../../common/helpers/systemInfo.js'
import { IconLoading } from '../sidebar-tsx/SidebarChat.js'
import { ToolApprovalType, toolApprovalTypes } from '../../../../common/toolsServiceTypes.js'
import Severity from '../../../../../../../base/common/severity.js'
const ButtonLeftTextRightOption = ({ text, leftButton }: { text: string, leftButton?: React.ReactNode }) => {
@ -152,6 +153,36 @@ const AddButton = ({ disabled, text = 'Add', ...props }: { disabled?: boolean, t
}
// ConfirmButton prompts for a second click to confirm an action, cancels if clicking outside
const ConfirmButton = ({ children, onConfirm, className }: { children: React.ReactNode, onConfirm: () => void, className?: string }) => {
const [confirm, setConfirm] = useState(false);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!confirm) return;
const handleClickOutside = (e: MouseEvent) => {
if (ref.current && !ref.current.contains(e.target as Node)) {
setConfirm(false);
}
};
document.addEventListener('click', handleClickOutside);
return () => document.removeEventListener('click', handleClickOutside);
}, [confirm]);
return (
<div ref={ref} className={`inline-block`}>
<VoidButtonBgDarken className={className} onClick={() => {
if (!confirm) {
setConfirm(true);
} else {
onConfirm();
setConfirm(false);
}
}}>
{confirm ? `Confirm Reset` : children}
</VoidButtonBgDarken>
</div>
);
};
// shows a providerName dropdown if no `providerName` is given
export const AddModelInputBox = ({ providerName: permanentProviderName, className, compact }: { providerName?: ProviderName, className?: string, compact?: boolean }) => {
@ -165,14 +196,14 @@ export const AddModelInputBox = ({ providerName: permanentProviderName, classNam
const [showCheckmark, setShowCheckmark] = useState(false)
// const providerNameRef = useRef<ProviderName | null>(null)
const [userChosenProviderName, setUserChosenProviderName] = useState<ProviderName>('anthropic')
const [userChosenProviderName, setUserChosenProviderName] = useState<ProviderName | null>(null)
const providerName = permanentProviderName ?? userChosenProviderName;
const [modelName, setModelName] = useState<string>('')
const [errorString, setErrorString] = useState('')
const numModels = settingsState.settingsOfProvider[providerName].models.length
const numModels = providerName === null ? 0 : settingsState.settingsOfProvider[providerName].models.length
if (showCheckmark) {
return <AnimatedCheckmarkButton text='Added' className={`bg-[#0e70c0] text-white dark:text-black px-3 py-1 rounded-sm ${className}`} />
@ -198,59 +229,66 @@ export const AddModelInputBox = ({ providerName: permanentProviderName, classNam
<button onClick={() => { setIsOpen(false) }} className='text-void-fg-4'><X className='size-4' /></button> */}
{/* provider input */}
{!permanentProviderName &&
<VoidCustomDropdownBox
options={providerNames}
selectedOption={providerName}
onChangeOption={(pn) => setUserChosenProviderName(pn)}
getOptionDisplayName={(pn) => pn ? displayInfoOfProviderName(pn).title : 'Provider Name'}
getOptionDropdownName={(pn) => pn ? displayInfoOfProviderName(pn).title : 'Provider Name'}
getOptionsEqual={(a, b) => a === b}
// className={`max-w-44 w-full border border-void-border-2 bg-void-bg-1 text-void-fg-3 text-root py-[4px] px-[6px]`}
className={`max-w-32 mx-2 w-full resize-none bg-void-bg-1 text-void-fg-1 placeholder:text-void-fg-3 border border-void-border-2 focus:border-void-border-1 py-1 px-2 rounded`}
arrowTouchesText={false}
/>
}
<ErrorBoundary>
{!permanentProviderName &&
<VoidCustomDropdownBox
options={providerNames}
selectedOption={providerName}
onChangeOption={(pn) => setUserChosenProviderName(pn)}
getOptionDisplayName={(pn) => pn ? displayInfoOfProviderName(pn).title : 'Provider Name'}
getOptionDropdownName={(pn) => pn ? displayInfoOfProviderName(pn).title : 'Provider Name'}
getOptionsEqual={(a, b) => a === b}
// className={`max-w-44 w-full border border-void-border-2 bg-void-bg-1 text-void-fg-3 text-root py-[4px] px-[6px]`}
className={`max-w-32 mx-2 w-full resize-none bg-void-bg-1 text-void-fg-1 placeholder:text-void-fg-3 border border-void-border-2 focus:border-void-border-1 py-1 px-2 rounded`}
arrowTouchesText={false}
/>
}
</ErrorBoundary>
{/* model input */}
<VoidSimpleInputBox
value={modelName}
onChangeValue={setModelName}
placeholder='Model Name'
compact={compact}
className={'max-w-32'}
/>
<ErrorBoundary>
<VoidSimpleInputBox
value={modelName}
onChangeValue={setModelName}
placeholder='Model Name'
compact={compact}
className={'max-w-32'}
/>
</ErrorBoundary>
{/* add button */}
<AddButton
type='submit'
disabled={!modelName}
onClick={(e) => {
if (providerName === null) {
setErrorString('Please select a provider.')
return
}
if (!modelName) {
setErrorString('Please enter a model name.')
return
}
// if model already exists here
if (settingsState.settingsOfProvider[providerName].models.find(m => m.modelName === modelName)) {
// setErrorString(`This model already exists under ${providerName}.`)
setErrorString(`This model already exists.`)
return
}
<ErrorBoundary>
<AddButton
type='submit'
disabled={!modelName}
onClick={(e) => {
if (providerName === null) {
setErrorString('Please select a provider.')
return
}
if (!modelName) {
setErrorString('Please enter a model name.')
return
}
// if model already exists here
if (settingsState.settingsOfProvider[providerName].models.find(m => m.modelName === modelName)) {
// setErrorString(`This model already exists under ${providerName}.`)
setErrorString(`This model already exists.`)
return
}
settingsStateService.addModel(providerName, modelName)
setShowCheckmark(true)
setTimeout(() => {
setShowCheckmark(false)
setIsOpen(false)
}, 1500)
setErrorString('')
setModelName('')
}}
/>
settingsStateService.addModel(providerName, modelName)
setShowCheckmark(true)
setTimeout(() => {
setShowCheckmark(false)
setIsOpen(false)
}, 1500)
setErrorString('')
setModelName('')
}}
/>
</ErrorBoundary>
</form>
@ -551,18 +589,20 @@ const FastApplyMethodDropdown = () => {
}
export const ollamaSetupInstructions = <div className='prose-p:my-0 prose-ol:list-decimal prose-p:py-0 prose-ol:my-0 prose-ol:py-0 prose-span:my-0 prose-span:py-0 text-void-fg-3 text-sm list-decimal select-text'>
<div className=''><ChatMarkdownRender string={`Ollama Setup Instructions`} chatMessageLocation={undefined} /></div>
<div className=' pl-6'><ChatMarkdownRender string={`1. Download [Ollama](https://ollama.com/download).`} chatMessageLocation={undefined} /></div>
<div className=' pl-6'><ChatMarkdownRender string={`2. Open your terminal.`} chatMessageLocation={undefined} /></div>
<div
className='pl-6 flex items-center w-fit'
data-tooltip-id='void-tooltip-ollama-settings'
>
<ChatMarkdownRender string={`3. Run \`ollama pull your_model\` to install a model.`} chatMessageLocation={undefined} />
export const OllamaSetupInstructions = () => {
return <div className='prose-p:my-0 prose-ol:list-decimal prose-p:py-0 prose-ol:my-0 prose-ol:py-0 prose-span:my-0 prose-span:py-0 text-void-fg-3 text-sm list-decimal select-text'>
<div className=''><ChatMarkdownRender string={`Ollama Setup Instructions`} chatMessageLocation={undefined} /></div>
<div className=' pl-6'><ChatMarkdownRender string={`1. Download [Ollama](https://ollama.com/download).`} chatMessageLocation={undefined} /></div>
<div className=' pl-6'><ChatMarkdownRender string={`2. Open your terminal.`} chatMessageLocation={undefined} /></div>
<div
className='pl-6 flex items-center w-fit'
data-tooltip-id='void-tooltip-ollama-settings'
>
<ChatMarkdownRender string={`3. Run \`ollama pull your_model\` to install a model.`} chatMessageLocation={undefined} />
</div>
<div className=' pl-6'><ChatMarkdownRender string={`Void automatically detects locally running models and enables them.`} chatMessageLocation={undefined} /></div>
</div>
<div className=' pl-6'><ChatMarkdownRender string={`Void automatically detects locally running models and enables them.`} chatMessageLocation={undefined} /></div>
</div>
}
const RedoOnboardingButton = ({ className }: { className?: string }) => {
@ -711,6 +751,34 @@ const transferTheseFilesOfOS = (os: 'mac' | 'windows' | 'linux' | null, fromEdit
}
export const ToolApprovalTypeSwitch = ({ approvalType, size, desc }: { approvalType: ToolApprovalType, size: "xxs" | "xs" | "sm" | "sm+" | "md", desc: string }) => {
const accessor = useAccessor()
const voidSettingsService = accessor.get('IVoidSettingsService')
const voidSettingsState = useSettingsState()
const metricsService = accessor.get('IMetricsService')
const onToggleAutoApprove = useCallback((approvalType: ToolApprovalType, newValue: boolean) => {
voidSettingsService.setGlobalSetting('autoApprove', {
...voidSettingsService.state.globalSettings.autoApprove,
[approvalType]: newValue
})
metricsService.capture('Tool Auto-Accept Toggle', { enabled: newValue })
}, [voidSettingsService, metricsService])
return <>
<VoidSwitch
size={size}
value={voidSettingsState.globalSettings.autoApprove[approvalType] ?? false}
onChange={(newVal) => onToggleAutoApprove(approvalType, newVal)}
/>
<span className="text-void-fg-3 text-xs">{desc}</span>
</>
}
export const OneClickSwitchButton = ({ fromEditor = 'VS Code', className = '' }: { fromEditor?: TransferEditorType, className?: string }) => {
const accessor = useAccessor()
const fileService = accessor.get('IFileService')
@ -737,7 +805,28 @@ export const OneClickSwitchButton = ({ fromEditor = 'VS Code', className = '' }:
setTransferState({ type: 'loading' })
let errAcc = ''
for (let { from, to } of transferTheseFiles) {
// Define extensions to skip when transferring
const extensionBlacklist = [
// ignore extensions
'ms-vscode-remote.remote-ssh',
'ms-vscode-remote.remote-wsl',
// ignore other AI copilots that could conflict with Void keybindings
'sourcegraph.cody-ai',
'continue.continue',
'codeium.codeium',
'saoudrizwan.claude-dev', // cline
'rooveterinaryinc.roo-cline', // roo
];
for (const { from, to } of transferTheseFiles) {
try {
// find a blacklisted item
const isBlacklisted = extensionBlacklist.find(blacklistItem => {
return from.fsPath?.includes(blacklistItem)
})
if (isBlacklisted) continue
} catch { }
console.log('transferring', from, to)
// Check if the source file exists before attempting to copy
try {
@ -794,6 +883,72 @@ export const Settings = () => {
const nativeHostService = accessor.get('INativeHostService')
const settingsState = useSettingsState()
const voidSettingsService = accessor.get('IVoidSettingsService')
const chatThreadsService = accessor.get('IChatThreadService')
const notificationService = accessor.get('INotificationService')
const onDownload = (t: 'Chats' | 'Settings') => {
let dataStr: string
let downloadName: string
if (t === 'Chats') {
// Export chat threads
dataStr = JSON.stringify(chatThreadsService.state, null, 2)
downloadName = 'void-chats.json'
}
else if (t === 'Settings') {
// Export user settings
dataStr = JSON.stringify(voidSettingsService.state, null, 2)
downloadName = 'void-settings.json'
}
else {
dataStr = ''
downloadName = ''
}
const blob = new Blob([dataStr], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = downloadName
a.click()
URL.revokeObjectURL(url)
}
// Add file input refs
const fileInputSettingsRef = useRef<HTMLInputElement>(null)
const fileInputChatsRef = useRef<HTMLInputElement>(null)
const [s, ss] = useState(0)
const handleUpload = (t: 'Chats' | 'Settings') => (e: React.ChangeEvent<HTMLInputElement>,) => {
const files = e.target.files
if (!files) return;
const file = files[0]
if (!file) return
const reader = new FileReader();
reader.onload = () => {
try {
const json = JSON.parse(reader.result as string);
if (t === 'Chats') {
chatThreadsService.dangerousSetState(json as any)
}
else if (t === 'Settings') {
voidSettingsService.dangerousSetState(json as any)
}
notificationService.info(`${t} imported successfully!`)
} catch (err) {
notificationService.notify({ message: `Failed to import ${t}`, source: err + '', severity: Severity.Error, })
}
};
reader.readAsText(file);
e.target.value = '';
ss(s => s + 1)
}
return <div className={`@@void-scope ${isDark ? 'dark' : ''}`} style={{ height: '100%', width: '100%' }}>
<div className='overflow-y-auto w-full h-full px-10 py-10 select-none'>
@ -805,6 +960,8 @@ export const Settings = () => {
{/* separator */}
<div className='w-full h-[1px] my-4' />
{/* Models section (formerly FeaturesTab) */}
{/* Models section (formerly FeaturesTab) */}
<ErrorBoundary>
<h2 className={`text-3xl mb-2`}>Models</h2>
@ -820,7 +977,7 @@ export const Settings = () => {
<h3 className={`text-void-fg-3 mb-2`}>{`Void can access any model that you host locally. We automatically detect your local models by default.`}</h3>
<div className='opacity-80 mb-4'>
{ollamaSetupInstructions}
<OllamaSetupInstructions />
</div>
<ErrorBoundary>
@ -933,15 +1090,12 @@ export const Settings = () => {
<div className='my-2'>
{/* Auto Accept Switch */}
<ErrorBoundary>
{[...toolApprovalTypes].map((approvalType) => {
return <div key={approvalType} className="flex items-center gap-x-2 my-2">
<ToolApprovalTypeSwitch size='xs' approvalType={approvalType} desc={`Auto-approve ${approvalType}`} />
</div>
})}
<div className='flex items-center gap-x-2 my-2'>
<VoidSwitch
size='xs'
value={settingsState.globalSettings.autoApprove}
onChange={(newVal) => voidSettingsService.setGlobalSetting('autoApprove', newVal)}
/>
<span className='text-void-fg-3 text-xs pointer-events-none'>{settingsState.globalSettings.autoApprove ? 'Auto-approve' : 'Auto-approve'}</span>
</div>
</ErrorBoundary>
{/* Tool Lint Errors Switch */}
@ -985,10 +1139,10 @@ export const Settings = () => {
{/* General section (formerly GeneralTab) */}
<div className='mt-12'>
<ErrorBoundary>
<h2 className={`text-3xl mb-2 mt-12`}>One-Click Switch</h2>
<h4 className={`text-void-fg-3 mb-4`}>{`Transfer your settings from another editor to Void in one click.`}</h4>
<h2 className='text-3xl mb-2 mt-12'>One-Click Switch</h2>
<h4 className='text-void-fg-3 mb-4'>{`Transfer your settings from another editor to Void in one click.`}</h4>
<div className='flex flex-col gap-4'>
<div className='flex flex-col gap-2'>
<OneClickSwitchButton className='w-48' fromEditor="VS Code" />
<OneClickSwitchButton className='w-48' fromEditor="Cursor" />
<OneClickSwitchButton className='w-48' fromEditor="Windsurf" />
@ -996,6 +1150,41 @@ export const Settings = () => {
</ErrorBoundary>
</div>
{/* Import/Export section, as its own block right after One-Click Switch */}
<div className='mt-12'>
<h2 className='text-3xl mb-2'>Import/Export</h2>
<div className='flex gap-8'>
{/* Settings Subcategory */}
<div className='flex flex-col gap-2 max-w-48 w-full'>
<h3 className='text-xl mb-2'>Settings</h3>
<input key={2 * s} ref={fileInputSettingsRef} type='file' accept='.json' className='hidden' onChange={handleUpload('Settings')} />
<VoidButtonBgDarken className='px-4 py-1 w-full' onClick={() => { fileInputSettingsRef.current?.click() }}>
Import Settings
</VoidButtonBgDarken>
<VoidButtonBgDarken className='px-4 py-1 w-full' onClick={() => onDownload('Settings')}>
Export Settings
</VoidButtonBgDarken>
<ConfirmButton className='px-4 py-1 w-full' onConfirm={() => { voidSettingsService.resetState(); }}>
Reset Settings
</ConfirmButton>
</div>
{/* Chats Subcategory */}
<div className='flex flex-col gap-2 w-full max-w-48'>
<h3 className='text-xl mb-2'>Chat</h3>
<input key={2 * s + 1} ref={fileInputChatsRef} type='file' accept='.json' className='hidden' onChange={handleUpload('Chats')} />
<VoidButtonBgDarken className='px-4 py-1 w-full' onClick={() => { fileInputChatsRef.current?.click() }}>
Import Chats
</VoidButtonBgDarken>
<VoidButtonBgDarken className='px-4 py-1 w-full' onClick={() => onDownload('Chats')}>
Export Chats
</VoidButtonBgDarken>
<ConfirmButton className='px-4 py-1 w-full' onConfirm={() => { chatThreadsService.resetState(); }}>
Reset Chats
</ConfirmButton>
</div>
</div>
</div>
<div className='mt-12'>
@ -1004,23 +1193,17 @@ export const Settings = () => {
<h4 className={`text-void-fg-3 mb-4`}>{`IDE settings, keyboard settings, and theme customization.`}</h4>
<ErrorBoundary>
<div className='my-4'>
<VoidButtonBgDarken className='px-4 py-2' onClick={() => { commandService.executeCommand('workbench.action.openSettings') }}>
<div className='flex flex-col gap-2 justify-center max-w-48 w-full'>
<VoidButtonBgDarken className='px-4 py-1' onClick={() => { commandService.executeCommand('workbench.action.openSettings') }}>
General Settings
</VoidButtonBgDarken>
</div>
<div className='my-4'>
<VoidButtonBgDarken className='px-4 py-2' onClick={() => { commandService.executeCommand('workbench.action.openGlobalKeybindings') }}>
<VoidButtonBgDarken className='px-4 py-1' onClick={() => { commandService.executeCommand('workbench.action.openGlobalKeybindings') }}>
Keyboard Settings
</VoidButtonBgDarken>
</div>
<div className='my-4'>
<VoidButtonBgDarken className='px-4 py-2' onClick={() => { commandService.executeCommand('workbench.action.selectTheme') }}>
<VoidButtonBgDarken className='px-4 py-1' onClick={() => { commandService.executeCommand('workbench.action.selectTheme') }}>
Theme Settings
</VoidButtonBgDarken>
</div>
<div className='my-4'>
<VoidButtonBgDarken className='px-4 py-2' onClick={() => { nativeHostService.showItemInFolder(environmentService.logsHome.fsPath) }}>
<VoidButtonBgDarken className='px-4 py-1' onClick={() => { nativeHostService.showItemInFolder(environmentService.logsHome.fsPath) }}>
Open Logs
</VoidButtonBgDarken>
</div>

View file

@ -15,7 +15,7 @@ export const WarningBox = ({ text, onClick, className }: { text: string; onClick
>
<IconWarning
size={14}
className='mr-1'
className='mr-1 flex-shrink-0'
/>
<span>{text}</span>
</div>

View file

@ -3,14 +3,14 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js';
import { Disposable, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
import { removeAnsiEscapeCodes } from '../../../../base/common/strings.js';
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { TerminalLocation } from '../../../../platform/terminal/common/terminal.js';
import { TerminalExitReason, TerminalLocation } from '../../../../platform/terminal/common/terminal.js';
import { ITerminalService, ITerminalInstance } from '../../../../workbench/contrib/terminal/browser/terminal.js';
import { MAX_TERMINAL_CHARS, MAX_TERMINAL_INACTIVE_TIME } from '../common/prompt/prompts.js';
import { TerminalResolveReason } from '../common/toolsServiceTypes.js';
import { MAX_TERMINAL_CHARS_PAGE, TERMINAL_BG_WAIT_TIME, TERMINAL_TIMEOUT_TIME } from './toolsService.js';
@ -18,9 +18,12 @@ export interface ITerminalToolService {
readonly _serviceBrand: undefined;
listTerminalIds(): string[];
runCommand(command: string, proposedTerminalId: string, waitForCompletion: boolean): Promise<{ terminalId: string, didCreateTerminal: boolean, result: string, resolveReason: TerminalResolveReason }>;
openTerminal(terminalId: string): Promise<void>
runCommand(command: string, bgTerminalId: string | null): Promise<{ terminalId: string, resPromise: Promise<{ result: string, resolveReason: TerminalResolveReason }> }>;
focusTerminal(terminalId: string): Promise<void>
terminalExists(terminalId: string): boolean
createTerminal(): Promise<string>
killTerminal(terminalId: string): Promise<void>
}
export const ITerminalToolService = createDecorator<ITerminalToolService>('TerminalToolService');
@ -103,12 +106,7 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
throw new Error('This should never be reached by pigeonhole principle');
}
private async _getOrCreateTerminal(proposedTerminalId: string) {
// if terminal ID exists, return it
if (proposedTerminalId in this.terminalInstanceOfId) return { terminalId: proposedTerminalId, didCreateTerminal: false }
async createTerminal() {
// create new terminal and return its ID
const terminalId = this.getValidNewTerminalId();
const terminal = await this.terminalService.createTerminal({
@ -127,13 +125,21 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
})
disposables.push(d)
})
const waitForTimeout = new Promise<void>(res => { setTimeout(() => { res() }, 1000) })
const waitForTimeout = new Promise<void>(res => { setTimeout(() => { res() }, 5000) })
await Promise.any([waitForMount, waitForTimeout,])
disposables.forEach(d => d.dispose())
this.terminalInstanceOfId[terminalId] = terminal
return { terminalId, didCreateTerminal: true }
return terminalId
}
async killTerminal(terminalId: string) {
const terminal = this.terminalInstanceOfId[terminalId]
if (!terminal) throw new Error(`Kill Terminal: Terminal with ID ${terminalId} did not exist.`);
terminal.dispose(TerminalExitReason.Extension)
delete this.terminalInstanceOfId[terminalId]
return
}
terminalExists(terminalId: string): boolean {
@ -141,7 +147,7 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
}
openTerminal: ITerminalToolService['openTerminal'] = async (terminalId) => {
focusTerminal: ITerminalToolService['focusTerminal'] = async (terminalId) => {
if (!terminalId) return
const terminal = this.terminalInstanceOfId[terminalId]
if (!terminal) return // should never happen
@ -151,81 +157,104 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
runCommand: ITerminalToolService['runCommand'] = async (command, proposedTerminalId, waitForCompletion) => {
runCommand: ITerminalToolService['runCommand'] = async (command, bgTerminalId) => {
await this.terminalService.whenConnected;
const { terminalId, didCreateTerminal } = await this._getOrCreateTerminal(proposedTerminalId)
const terminal = this.terminalInstanceOfId[terminalId];
if (!terminal) throw new Error(`Unexpected internal error: Terminal with ID ${terminalId} did not exist.`);
// focus the terminal about to run
this.terminalService.setActiveInstance(terminal)
await this.terminalService.focusActiveInstance()
let result: string = ''
let resolveReason: TerminalResolveReason | undefined = undefined
let terminal: ITerminalInstance
const disposables: IDisposable[] = []
const waitUntilDone = new Promise<void>((res, rej) => {
const d2 = terminal.onData(async newData => {
if (resolveReason) return
const isBG = bgTerminalId !== null
let terminalId: string
if (isBG) { // BG process
terminal = this.terminalInstanceOfId[bgTerminalId];
if (!terminal) throw new Error(`Unexpected internal error: Terminal with ID ${bgTerminalId} did not exist.`);
terminalId = bgTerminalId
}
else {
terminalId = await this.createTerminal()
terminal = this.terminalInstanceOfId[terminalId]
if (!terminal) throw new Error(`Unexpected error: Terminal could not be created.`)
}
result += newData
// onPageFull
if (result.length > MAX_TERMINAL_CHARS_PAGE) {
result = result.substring(0, MAX_TERMINAL_CHARS_PAGE)
await terminal.sendText('\x03', true) // interrupt the terminal with Ctrl+C
resolveReason = { type: 'toofull' }
res()
return
}
const waitForResult = async () => {
// focus the terminal about to run
this.terminalService.setActiveInstance(terminal)
await this.terminalService.focusActiveInstance()
// onDone
const isDone = isCommandComplete(result)
if (isDone) {
resolveReason = { type: 'done', exitCode: isDone.exitCode }
res()
return
}
let result: string = ''
let resolveReason: TerminalResolveReason | undefined = undefined
// create this before we send so that we don't miss events on terminal
const waitUntilDone = new Promise<void>((res, rej) => {
const d2 = terminal.onData(async newData => {
if (resolveReason) return
result += newData
// onDone
const isDone = isCommandComplete(result)
if (isDone) {
resolveReason = { type: 'done', exitCode: isDone.exitCode }
res()
return
}
})
disposables.push(d2)
})
disposables.push(d2)
})
// send the command here
await terminal.sendText(command, true)
// send the command here
await terminal.sendText(command, true)
// timeout promise
const waitUntilTimeout = new Promise<void>((res, rej) => {
setTimeout(async () => {
if (resolveReason) return
await terminal.sendText('\x03', true) // interrupt the terminal with Ctrl+C
resolveReason = { type: waitForCompletion ? 'timeout' : 'bgtask' }
res()
return
}, (waitForCompletion ? TERMINAL_TIMEOUT_TIME : TERMINAL_BG_WAIT_TIME) * 1000)
})
// inactivity-based timeout
const waitUntilInactive = new Promise<void>(res => {
let globalTimeoutId: ReturnType<typeof setTimeout>;
const resetTimer = () => {
clearTimeout(globalTimeoutId);
globalTimeoutId = setTimeout(() => {
if (resolveReason) return
await Promise.any([
waitUntilDone,
waitUntilTimeout,
])
resolveReason = { type: 'timeout' };
res();
}, MAX_TERMINAL_INACTIVE_TIME * 1000);
};
disposables.forEach(d => d.dispose())
const dTimeout = terminal.onData(() => { resetTimer(); });
disposables.push(dTimeout, toDisposable(() => clearTimeout(globalTimeoutId)));
resetTimer();
});
if (!resolveReason) throw new Error('Unexpected internal error: Promise.any should have resolved with a reason.')
// wait for result
await Promise.any([waitUntilDone, waitUntilInactive,])
disposables.forEach(d => d.dispose())
if (!isBG) {
await this.killTerminal(terminalId)
}
result = removeAnsiEscapeCodes(result)
.split('\n').slice(1, -1) // remove first and last line (first = command, last = andrewpareles/void %)
.join('\n')
if (!resolveReason) throw new Error('Unexpected internal error: Promise.any should have resolved with a reason.')
return { terminalId, didCreateTerminal, result, resolveReason }
result = removeAnsiEscapeCodes(result)
.split('\n').slice(1, -1) // remove first and last line (first = command, last = andrewpareles/void %)
.join('\n')
if (result.length > MAX_TERMINAL_CHARS) {
const half = MAX_TERMINAL_CHARS / 2
result = result.slice(0, half)
+ '\n...\n'
+ result.slice(result.length - half, Infinity)
}
return { result, resolveReason }
}
const resPromise = waitForResult()
return { terminalId, resPromise }
}
}
registerSingleton(ITerminalToolService, TerminalToolService, InstantiationType.Delayed);

View file

@ -17,7 +17,7 @@ import { computeDirectoryTree1Deep, IDirectoryStrService, stringifyDirectoryTree
import { IMarkerService, MarkerSeverity } from '../../../../platform/markers/common/markers.js'
import { timeout } from '../../../../base/common/async.js'
import { RawToolParamsObj } from '../common/sendLLMMessageTypes.js'
import { ToolName } from '../common/prompt/prompts.js'
import { MAX_CHILDREN_URIs_PAGE, MAX_FILE_CHARS_PAGE, MAX_TERMINAL_INACTIVE_TIME, ToolName } from '../common/prompt/prompts.js'
import { IVoidSettingsService } from '../common/voidSettingsService.js'
@ -27,20 +27,11 @@ import { IVoidSettingsService } from '../common/voidSettingsService.js'
type ValidateParams = { [T in ToolName]: (p: RawToolParamsObj) => ToolCallParams[T] }
type CallTool = { [T in ToolName]: (p: ToolCallParams[T]) => Promise<{ result: ToolResultType[T], interruptTool?: () => void }> }
type CallTool = { [T in ToolName]: (p: ToolCallParams[T]) => Promise<{ result: ToolResultType[T] | Promise<ToolResultType[T]>, interruptTool?: () => void }> }
type ToolResultToString = { [T in ToolName]: (p: ToolCallParams[T], result: Awaited<ToolResultType[T]>) => string }
// pagination info
export const MAX_FILE_CHARS_PAGE = 500_000
export const MAX_CHILDREN_URIs_PAGE = 500
export const MAX_TERMINAL_CHARS_PAGE = 20_000
export const TERMINAL_TIMEOUT_TIME = 5 // seconds
export const TERMINAL_BG_WAIT_TIME = 1
const isFalsy = (u: unknown) => {
return !u || u === 'null' || u === 'undefined'
}
@ -176,12 +167,12 @@ export class ToolsService implements IToolsService {
const uri = validateURI(uriStr)
const pageNumber = validatePageNum(pageNumberUnknown)
return { rootURI: uri, pageNumber }
return { uri, pageNumber }
},
get_dir_structure: (params: RawToolParamsObj) => {
get_dir_tree: (params: RawToolParamsObj) => {
const { uri: uriStr, } = params
const uri = validateURI(uriStr)
return { rootURI: uri }
return { uri }
},
search_pathnames_only: (params: RawToolParamsObj) => {
const {
@ -192,9 +183,9 @@ export class ToolsService implements IToolsService {
const queryStr = validateStr('query', queryUnknown)
const pageNumber = validatePageNum(pageNumberUnknown)
const searchInFolder = validateOptionalStr('search_in_folder', includeUnknown)
const includePattern = validateOptionalStr('include_pattern', includeUnknown)
return { queryStr, searchInFolder, pageNumber }
return { query: queryStr, includePattern, pageNumber }
},
search_for_files: (params: RawToolParamsObj) => {
@ -204,14 +195,23 @@ export class ToolsService implements IToolsService {
is_regex: isRegexUnknown,
page_number: pageNumberUnknown
} = params
const queryStr = validateStr('query', queryUnknown)
const pageNumber = validatePageNum(pageNumberUnknown)
const searchInFolder = validateOptionalURI(searchInFolderUnknown)
const isRegex = validateBoolean(isRegexUnknown, { default: false })
return { queryStr, searchInFolder, isRegex, pageNumber }
return {
query: queryStr,
isRegex,
searchInFolder,
pageNumber
}
},
search_in_file: (params: RawToolParamsObj) => {
const { uri: uriStr, query: queryUnknown, is_regex: isRegexUnknown } = params;
const uri = validateURI(uriStr);
const query = validateStr('query', queryUnknown);
const isRegex = validateBoolean(isRegexUnknown, { default: false });
return { uri, query, isRegex };
},
read_lint_errors: (params: RawToolParamsObj) => {
@ -242,18 +242,28 @@ export class ToolsService implements IToolsService {
},
edit_file: (params: RawToolParamsObj) => {
const { uri: uriStr, change_description: changeDescriptionUnknown } = params
const { uri: uriStr, change_diff: changeDiffUnknown } = params
const uri = validateURI(uriStr)
const changeDescription = validateStr('changeDescription', changeDescriptionUnknown)
return { uri, changeDescription }
const changeDiff = validateStr('changeDiff', changeDiffUnknown)
return { uri, changeDiff }
},
command_tool: (params: RawToolParamsObj) => {
const { command: commandUnknown, terminal_id: terminalIdUnknown, wait_for_completion: waitForCompletionUnknown } = params
const command = validateStr('command', commandUnknown)
const proposedTerminalId = validateProposedTerminalId(terminalIdUnknown)
const waitForCompletion = validateBoolean(waitForCompletionUnknown, { default: true })
return { command, proposedTerminalId, waitForCompletion }
// ---
run_command: (params: RawToolParamsObj) => {
const { command: commandUnknown, terminal_id: terminalIdUnknown } = params;
const command = validateStr('command', commandUnknown);
const proposedTerminalId = terminalIdUnknown ? validateProposedTerminalId(terminalIdUnknown) : null;
return { command, bgTerminalId: proposedTerminalId };
},
open_persistent_terminal: (_params: RawToolParamsObj) => {
// No parameters needed; will open a new background terminal
return {};
},
kill_persistent_terminal: (params: RawToolParamsObj) => {
const { terminal_id: terminalIdUnknown } = params;
const terminalId = validateProposedTerminalId(terminalIdUnknown);
return { terminalId };
},
}
@ -283,21 +293,21 @@ export class ToolsService implements IToolsService {
return { result: { fileContents, totalFileLen, hasNextPage } }
},
ls_dir: async ({ rootURI, pageNumber }) => {
const dirResult = await computeDirectoryTree1Deep(fileService, rootURI, pageNumber)
ls_dir: async ({ uri, pageNumber }) => {
const dirResult = await computeDirectoryTree1Deep(fileService, uri, pageNumber)
return { result: dirResult }
},
get_dir_structure: async ({ rootURI }) => {
const str = await this.directoryStrService.getDirectoryStrTool(rootURI)
get_dir_tree: async ({ uri }) => {
const str = await this.directoryStrService.getDirectoryStrTool(uri)
return { result: { str } }
},
search_pathnames_only: async ({ queryStr, searchInFolder, pageNumber }) => {
search_pathnames_only: async ({ query: queryStr, includePattern, pageNumber }) => {
const query = queryBuilder.file(workspaceContextService.getWorkspace().folders.map(f => f.uri), {
filePattern: queryStr,
includePattern: searchInFolder ?? undefined,
includePattern: includePattern ?? undefined,
})
const data = await searchService.fileSearch(query, CancellationToken.None)
@ -311,7 +321,7 @@ export class ToolsService implements IToolsService {
return { result: { uris, hasNextPage } }
},
search_for_files: async ({ queryStr, isRegex, searchInFolder, pageNumber }) => {
search_for_files: async ({ query: queryStr, isRegex, searchInFolder, pageNumber }) => {
const searchFolders = searchInFolder === null ?
workspaceContextService.getWorkspace().folders.map(f => f.uri)
: [searchInFolder]
@ -332,6 +342,24 @@ export class ToolsService implements IToolsService {
const hasNextPage = (data.results.length - 1) - toIdx >= 1
return { result: { queryStr, uris, hasNextPage } }
},
search_in_file: async ({ uri, query, isRegex }) => {
await voidModelService.initializeModel(uri);
const { model } = await voidModelService.getModelSafe(uri);
if (model === null) { throw new Error(`No contents; File does not exist.`); }
const contents = model.getValue(EndOfLinePreference.LF);
const contentOfLine = contents.split('\n');
const totalLines = contentOfLine.length;
const regex = isRegex ? new RegExp(query) : null;
const lines: number[] = []
for (let i = 0; i < totalLines; i++) {
const line = contentOfLine[i];
if ((isRegex && regex!.test(line)) || (!isRegex && line.includes(query))) {
const matchLine = i + 1;
lines.push(matchLine);
}
}
return { result: { lines } };
},
read_lint_errors: async ({ uri }) => {
await timeout(1000)
@ -355,14 +383,14 @@ export class ToolsService implements IToolsService {
return { result: {} }
},
edit_file: async ({ uri, changeDescription }) => {
edit_file: async ({ uri, changeDiff }) => {
await voidModelService.initializeModel(uri)
if (this.commandBarService.getStreamState(uri) === 'streaming') {
throw new Error(`Another LLM is currently making changes to this file. Please stop streaming for now and resume later.`)
throw new Error(`Another LLM is currently making changes to this file. Please stop streaming for now and ask the user to resume later.`)
}
const opts = {
uri,
applyStr: changeDescription,
applyStr: changeDiff,
from: 'ClickApply',
startBehavior: 'keep-conflicts',
} as const
@ -385,10 +413,25 @@ export class ToolsService implements IToolsService {
return { result: lintErrorsPromise, interruptTool }
},
command_tool: async ({ command, proposedTerminalId, waitForCompletion }) => {
const { terminalId, didCreateTerminal, result, resolveReason } = await this.terminalToolService.runCommand(command, proposedTerminalId, waitForCompletion)
return { result: { terminalId, didCreateTerminal, result, resolveReason } }
// ---
run_command: async ({ command, bgTerminalId }) => {
const { terminalId, resPromise } = await this.terminalToolService.runCommand(command, bgTerminalId)
const interruptTool = () => {
this.terminalToolService.killTerminal(terminalId)
}
return { result: resPromise, interruptTool }
},
open_persistent_terminal: async () => {
// Open a new background terminal without waiting for completion
const terminalId = await this.terminalToolService.createTerminal()
return { result: { terminalId } }
},
kill_persistent_terminal: async ({ terminalId }) => {
// Close the background terminal by sending exit
await this.terminalToolService.killTerminal(terminalId)
return { result: {} }
},
}
@ -401,7 +444,7 @@ export class ToolsService implements IToolsService {
.substring(0, MAX_FILE_CHARS_PAGE)
}
// given to the LLM after the call
// given to the LLM after the call for successful tool calls
this.stringOfResult = {
read_file: (params, result) => {
return `${params.uri.fsPath}\n\`\`\`\n${result.fileContents}\n\`\`\`${nextPageStr(result.hasNextPage)}`
@ -410,7 +453,7 @@ export class ToolsService implements IToolsService {
const dirTreeStr = stringifyDirectoryTree1Deep(params, result)
return dirTreeStr // + nextPageStr(result.hasNextPage) // already handles num results remaining
},
get_dir_structure: (params, result) => {
get_dir_tree: (params, result) => {
return result.str
},
search_pathnames_only: (params, result) => {
@ -419,6 +462,15 @@ export class ToolsService implements IToolsService {
search_for_files: (params, result) => {
return result.uris.map(uri => uri.fsPath).join('\n') + nextPageStr(result.hasNextPage)
},
search_in_file: (params, result) => {
const { model } = voidModelService.getModel(params.uri)
if (!model) return '<Error getting string of result>'
const lines = result.lines.map(n => {
const lineContent = model.getValueInRange({ startLineNumber: n, startColumn: 1, endLineNumber: n, endColumn: Number.MAX_SAFE_INTEGER }, EndOfLinePreference.LF)
return `Line ${n}:\n\`\`\`\n${lineContent}\n\`\`\``
}).join('\n\n');
return lines;
},
read_lint_errors: (params, result) => {
return result.lintErrors ?
stringifyLintErrors(result.lintErrors)
@ -440,30 +492,41 @@ export class ToolsService implements IToolsService {
return `Change successfully made to ${params.uri.fsPath}.${lintErrsString}`
},
command_tool: (params, result) => {
run_command: (params, result) => {
const {
terminalId,
didCreateTerminal,
resolveReason,
result: result_,
} = result
const { bgTerminalId } = params
const terminalDesc = `terminal ${terminalId}${didCreateTerminal ? ` (a newly-created terminal)` : ''}`
// success
if (resolveReason.type === 'done') {
const desc = bgTerminalId ? ` in terminal ${bgTerminalId}` : ''
return `Terminal command executed and finished${desc}. Result (exit code ${resolveReason.exitCode}):\n${result_}`
}
if (resolveReason.type === 'timeout') {
return `Terminal command ran in ${terminalDesc}, but did not complete after ${TERMINAL_TIMEOUT_TIME} seconds. Result:\n${result_}`
// bg command
if (bgTerminalId !== null) {
if (resolveReason.type === 'timeout') {
return `Terminal command is running in the background in terminal ${bgTerminalId}. Here were the outputs after ${MAX_TERMINAL_INACTIVE_TIME} seconds:\n${result_}`
}
}
else if (resolveReason.type === 'bgtask') {
return `Terminal command is running in the background in ${terminalDesc}. Here were the outputs after ${TERMINAL_BG_WAIT_TIME} seconds:\n${result_}`
}
else if (resolveReason.type === 'toofull') {
return `Terminal command executed in terminal ${terminalDesc}. Command was interrupted because output was too long. Result:\n${result_}`
}
else if (resolveReason.type === 'done') {
return `Terminal command executed in terminal ${terminalDesc}. Result (exit code ${resolveReason.exitCode}):\n${result_}`
// normal command
else {
if (resolveReason.type === 'timeout') {
return `Terminal command ran, but was interrupted after ${MAX_TERMINAL_INACTIVE_TIME}s of inactivity and did not necessarily finish successfully. Full output:\n${result_}`
}
}
throw new Error(`Unexpected internal error: Terminal command did not resolve with a valid reason.`)
},
open_persistent_terminal: (_params, result) => {
const { terminalId } = result;
return `Successfully created background terminal with ID ${terminalId}`;
},
kill_persistent_terminal: (params, _result) => {
return `Successfully closed terminal ${params.terminalId}.`;
},
}

View file

@ -50,10 +50,10 @@ export const defaultProviderSettings = {
liteLLM: { // https://docs.litellm.ai/docs/providers/openai_compatible
endpoint: '',
},
googleVertex: { // google https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/call-vertex-using-openai-library
region: 'us-west2',
project: '',
},
// googleVertex: { // google https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/call-vertex-using-openai-library
// region: 'us-west2',
// project: '',
// },
microsoftAzure: { // microsoft Azure Foundry
project: '', // really 'resource'
apiKey: '',
@ -129,7 +129,7 @@ export const defaultModelsOfProvider = {
'ministral-8b-latest',
],
openAICompatible: [], // fallback
googleVertex: [],
// googleVertex: [],
microsoftAzure: [],
liteLLM: [],
@ -830,12 +830,12 @@ const groqSettings: VoidStaticProviderInfo = {
// ---------------- GOOGLE VERTEX ----------------
const googleVertexModelOptions = {
} as const satisfies Record<string, VoidStaticModelInfo>
const googleVertexSettings: VoidStaticProviderInfo = {
modelOptions: googleVertexModelOptions,
modelOptionsFallback: (modelName) => { return null }
}
// const googleVertexModelOptions = {
// } as const satisfies Record<string, VoidStaticModelInfo>
// const googleVertexSettings: VoidStaticProviderInfo = {
// modelOptions: googleVertexModelOptions,
// modelOptionsFallback: (modelName) => { return null }
// }
// ---------------- MICROSOFT AZURE ----------------
const microsoftAzureModelOptions = {
@ -1081,7 +1081,7 @@ const modelSettingsOfProvider: { [providerName in ProviderName]: VoidStaticProvi
liteLLM: liteLLMSettings,
lmStudio: lmStudioSettings,
googleVertex: googleVertexSettings,
// googleVertex: googleVertexSettings,
microsoftAzure: microsoftAzureSettings,
} as const

View file

@ -7,7 +7,7 @@ import { EndOfLinePreference } from '../../../../../editor/common/model.js';
import { StagingSelectionItem } from '../chatThreadServiceTypes.js';
import { os } from '../helpers/systemInfo.js';
import { RawToolParamsObj } from '../sendLLMMessageTypes.js';
import { toolNamesThatRequireApproval } from '../toolsServiceTypes.js';
import { approvalTypeOfToolName, ToolResultType } from '../toolsServiceTypes.js';
import { IVoidModelService } from '../voidModelService.js';
import { ChatMode } from '../voidSettingsTypes.js';
@ -20,6 +20,14 @@ export const MAX_DIRSTR_CHARS_TOTAL_TOOL = 20_000
export const MAX_DIRSTR_RESULTS_TOTAL_BEGINNING = 100
export const MAX_DIRSTR_RESULTS_TOTAL_TOOL = 100
// tool info
export const MAX_FILE_CHARS_PAGE = 500_000
export const MAX_CHILDREN_URIs_PAGE = 500
// terminal tool info
export const MAX_TERMINAL_CHARS = 100_000
export const MAX_TERMINAL_INACTIVE_TIME = 8 // seconds
// Maximum character limits for prefix and suffix context
export const MAX_PREFIX_SUFFIX_CHARS = 20_000
@ -66,7 +74,36 @@ const paginationParam = {
} as const
// export type SnakeCase<S extends string> =
// // exact acronym URI
// S extends 'URI' ? 'uri'
// // suffix URI: e.g. 'rootURI' -> snakeCase('root') + '_uri'
// : S extends `${infer Prefix}URI` ? `${SnakeCase<Prefix>}_uri`
// // default: for each char, prefix '_' on uppercase letters
// : S extends `${infer C}${infer Rest}`
// ? `${C extends Lowercase<C> ? C : `_${Lowercase<C>}`}${SnakeCase<Rest>}`
// : S;
// export type SnakeCaseKeys<T extends Record<string, any>> = {
// [K in keyof T as SnakeCase<Extract<K, string>>]: T[K]
// };
export const voidTools = {
// export const voidTools
// : {
// [T in keyof ToolCallParams]: {
// name: string;
// description: string;
// params: {
// [paramName in keyof SnakeCaseKeys<ToolCallParams[T]>]: { description: string }
// }
// }
// }
// = {
// --- context-gathering (read/search/list) ---
read_file: {
@ -74,8 +111,8 @@ export const voidTools = {
description: `Returns full contents of a given file.`,
params: {
...uriParam('file'),
start_line: { description: 'Optional. Only fill this in if you already know the line numbers you need to search. Defaults to 1.' },
end_line: { description: 'Optional. Only fill this in if you already know the line numbers you need to search. Defaults to Infinity.' },
start_line: { description: 'Optional. Do NOT fill this in unless you already know the line numbers you need to search. Defaults to 1.' },
end_line: { description: 'Optional. Do NOT fill this in unless you already know the line numbers you need to search. Defaults to Infinity.' },
...paginationParam,
},
},
@ -89,8 +126,8 @@ export const voidTools = {
},
},
get_dir_structure: {
name: 'get_dir_structure',
get_dir_tree: {
name: 'get_dir_tree',
description: `This is a very effective way to learn about the user's codebase. Returns a tree diagram of all the files and folders in the given folder. `,
params: {
...uriParam('folder')
@ -106,7 +143,7 @@ export const voidTools = {
description: `Returns all pathnames that match a given query (searches ONLY file names). You should use this when looking for a file with a specific name or path.`,
params: {
query: { description: `Your query for the search.` },
search_in_folder: { description: 'Optional. Only fill this in if you need to limit your search because there were too many results.' },
include_pattern: { description: 'Optional. Only fill this in if you need to limit your search because there were too many results.' },
...paginationParam,
},
},
@ -118,12 +155,23 @@ export const voidTools = {
description: `Returns a list of file names whose content matches the given query. The query can be any substring or regex.`,
params: {
query: { description: `Your query for the search.` },
search_in_folder: { description: 'Optional. Only fill this in if you need to limit your search because there were too many results.' },
is_regex: { description: 'Optional. Default is false. Whether query is a regex.' },
search_in_folder: { description: 'Optional. Leave as blank by default. ONLY fill this in if your previous search with the same query was truncated. Searches descendants of this folder only.' },
is_regex: { description: 'Optional. Default is false. Whether the query is a regex.' },
...paginationParam,
},
},
// add new search_in_file tool
search_in_file: {
name: 'search_in_file',
description: `Returns an array of all the start line numbers where the content appears in the file.`,
params: {
...uriParam('file'),
query: { description: 'The string or regex to search for in the file.' },
is_regex: { description: 'Optional. Default is false. Whether the query is a regex.' }
}
},
read_lint_errors: {
name: 'read_lint_errors',
description: `Returns all lint errors on a given file.`,
@ -156,33 +204,46 @@ export const voidTools = {
description: `Edits the contents of a file given the file's URI and a description.`,
params: {
...uriParam('file'),
change_description: {
change_diff: {
description: `\
Your description MUST be wrapped in triple backticks. \
A code description of the change you want to make, with comments like "// ... existing code ..." to condense your writing. \
NEVER re-write the whole file. Bias towards writing as little as possible. \
Here's an example of a good description:\n${editToolDescriptionExample}`
A code diff describing the change to make to the file. \
Your DIFF is the only context that will be given to another LLM to apply the change, so it must be accurate and complete. \
Your DIFF MUST be wrapped in triple backticks. \
NEVER re-write the whole file. Always bias towards writing as little as possible. \
Use comments like "// ... existing code ..." to condense your writing. \
Here's an example of a good output:\n${editToolDescriptionExample}`
}
},
},
command_tool: {
name: 'command_tool',
description: `Runs a terminal command. You can use this tool to run any command: sed, grep, etc. We just prefer you edit with the edit tool, not this tool if possible.`,
run_command: {
name: 'run_command',
description: `Runs a terminal command and waits for the result (times out after ${MAX_TERMINAL_INACTIVE_TIME}s of inactivity). You can use this tool to run any command: sed, grep, etc. Do not edit any files with this tool; use edit_file instead. When working with git and other tools that open an editor (e.g. git diff), you should pipe to cat to get all results and not get stuck in vim.`,
params: {
command: { description: 'The terminal command to run.' },
wait_for_completion: { description: `Optional. Default is true. Make this value false when you want a command to run without waiting for it to complete.` },
terminal_id: { description: 'Optional. The ID of the terminal instance that should execute the command (if not provided, defaults to the preferred terminal ID). The primary purpose of this is to let you open a new terminal for testing or background processes (e.g. running a dev server for the user in a separate terminal). Must be an integer >= 1.' },
bg_terminal_id: { description: 'Optional. This only applies to terminals that have been opened with open_persistent_terminal. Runs the command in the terminal with the specified ID.' },
},
},
open_persistent_terminal: {
name: 'open_persistent_terminal',
description: `Use this tool when you want to run a terminal command indefinitely, like a dev server (eg \`npm run dev\`), a background listener, etc. Opens a new terminal in the user's environment which will not awaited for or killed.`,
params: {}
},
kill_persistent_terminal: {
name: 'kill_persistent_terminal',
description: `Closes a BG terminal with the given ID.`,
params: { terminal_id: { description: `The terminal ID to interrupt and close.` } }
}
// go_to_definition
// go_to_usages
} satisfies { [name: string]: InternalToolInfo }
} satisfies { [T in keyof ToolResultType]: InternalToolInfo }
export type ToolName = keyof typeof voidTools
export type ToolName = keyof ToolResultType
export const toolNames = Object.keys(voidTools) as ToolName[]
type ToolParamNameOfTool<T extends ToolName> = keyof (typeof voidTools)[T]['params']
@ -197,7 +258,7 @@ export const isAToolName = (toolName: string): toolName is ToolName => {
export const availableTools = (chatMode: ChatMode) => {
const toolNames: ToolName[] | undefined = chatMode === 'normal' ? undefined
: chatMode === 'gather' ? (Object.keys(voidTools) as ToolName[]).filter(toolName => !toolNamesThatRequireApproval.has(toolName))
: chatMode === 'gather' ? (Object.keys(voidTools) as ToolName[]).filter(toolName => !(toolName in approvalTypeOfToolName))
: chatMode === 'agent' ? Object.keys(voidTools) as ToolName[]
: undefined
@ -447,7 +508,7 @@ export const DIVIDER = `=======`
export const FINAL = `>>>>>>> UPDATED`
export const searchReplace_systemMessage = `\
You are a coding assistant that takes in a diff describing of a change to make, and outputs SEARCH/REPLACE code blocks which implement the change.
You are a coding assistant that takes in a diff, and outputs SEARCH/REPLACE code blocks to implement the change(s) in the diff.
The diff will be labeled \`DIFF\` and the original file will be labeled \`ORIGINAL_FILE\`.
Format your SEARCH/REPLACE blocks as follows:
@ -459,11 +520,11 @@ ${DIVIDER}
${FINAL}
${tripleTick[1]}
1. Every single item written in \`CHANGE\` should show up in the final result, except for comments explicitly saying things like "// ... existing code". Make sure to include ALL other comments (even descriptive ones), code, whitespace, etc. in the final result.
1. Your SEARCH/REPLACE block(s) must implement the diff EXACTLY. Do NOT leave anything out.
2. Your SEARCH/REPLACE block(s) must implement the change EXACTLY. You should use comments like "// ... existing code" as reference points, and everything else in the change should be written verbatim.
2. You are allowed to output multiple SEARCH/REPLACE blocks to implement the change.
3. You are allowed to output multiple SEARCH/REPLACE blocks.
3. Assume any comments in the diff are PART OF THE CHANGE. Include them in the output.
4. Your output should consist ONLY of SEARCH/REPLACE blocks. Do NOT output any text or explanations before or after this.

View file

@ -3,7 +3,7 @@ import { ToolName } from './prompt/prompts.js';
export type TerminalResolveReason = { type: 'toofull' | 'timeout' | 'bgtask' } | { type: 'done', exitCode: number }
export type TerminalResolveReason = { type: 'timeout' } | { type: 'done', exitCode: number }
export type LintErrorItem = { code: string, message: string, startLineNumber: number, endLineNumber: number }
@ -16,39 +16,57 @@ export type ShallowDirectoryItem = {
}
export const approvalTypeOfToolName: Partial<{ [T in ToolName]?: 'edits' | 'terminal' }> = {
'create_file_or_folder': 'edits',
'delete_file_or_folder': 'edits',
'edit_file': 'edits',
'run_command': 'terminal',
}
const toolNamesWithApproval = ['create_file_or_folder', 'delete_file_or_folder', 'edit_file', 'command_tool'] as const satisfies readonly ToolName[]
export type ToolNameWithApproval = typeof toolNamesWithApproval[number]
export const toolNamesThatRequireApproval = new Set<ToolName>(toolNamesWithApproval)
// {{add: define new type for approval types}}
export type ToolApprovalType = NonNullable<(typeof approvalTypeOfToolName)[keyof typeof approvalTypeOfToolName]>;
export const toolApprovalTypes = new Set<ToolApprovalType>(
Object.values(approvalTypeOfToolName).filter((v): v is ToolApprovalType => v !== undefined)
)
// PARAMS OF TOOL CALL
export type ToolCallParams = {
'read_file': { uri: URI, startLine: number | null, endLine: number | null, pageNumber: number },
'ls_dir': { rootURI: URI, pageNumber: number },
'get_dir_structure': { rootURI: URI },
'search_pathnames_only': { queryStr: string, searchInFolder: string | null, pageNumber: number },
'search_for_files': { queryStr: string, isRegex: boolean, searchInFolder: URI | null, pageNumber: number },
'ls_dir': { uri: URI, pageNumber: number },
'get_dir_tree': { uri: URI },
'search_pathnames_only': { query: string, includePattern: string | null, pageNumber: number },
'search_for_files': { query: string, isRegex: boolean, searchInFolder: URI | null, pageNumber: number },
'search_in_file': { uri: URI, query: string, isRegex: boolean },
'read_lint_errors': { uri: URI },
// ---
'edit_file': { uri: URI, changeDescription: string },
'edit_file': { uri: URI, changeDiff: string },
'create_file_or_folder': { uri: URI, isFolder: boolean },
'delete_file_or_folder': { uri: URI, isRecursive: boolean, isFolder: boolean },
'command_tool': { command: string, proposedTerminalId: string, waitForCompletion: boolean },
// ---
'run_command': { command: string; bgTerminalId: string | null },
'open_persistent_terminal': {},
'kill_persistent_terminal': { terminalId: string },
}
// RESULT OF TOOL CALL
export type ToolResultType = {
'read_file': { fileContents: string, totalFileLen: number, hasNextPage: boolean },
'ls_dir': { children: ShallowDirectoryItem[] | null, hasNextPage: boolean, hasPrevPage: boolean, itemsRemaining: number },
'get_dir_structure': { str: string, },
'get_dir_tree': { str: string, },
'search_pathnames_only': { uris: URI[], hasNextPage: boolean },
'search_for_files': { uris: URI[], hasNextPage: boolean },
'search_in_file': { lines: number[]; },
'read_lint_errors': { lintErrors: LintErrorItem[] | null },
// ---
'edit_file': Promise<{ lintErrors: LintErrorItem[] | null }>,
'create_file_or_folder': {},
'delete_file_or_folder': {},
'command_tool': { terminalId: string, didCreateTerminal: boolean, result: string; resolveReason: TerminalResolveReason; },
// ---
'run_command': { result: string; resolveReason: TerminalResolveReason; },
'open_persistent_terminal': { terminalId: string },
'kill_persistent_terminal': {},
}

View file

@ -62,6 +62,9 @@ export interface IVoidSettingsService {
setOptionsOfModelSelection: SetOptionsOfModelSelection;
setGlobalSetting: SetGlobalSettingFn;
dangerousSetState(newState: VoidSettingsState): Promise<void>;
resetState(): Promise<void>;
setAutodetectedModels(providerName: ProviderName, modelNames: string[], logging: object): void;
toggleModelHidden(providerName: ProviderName, modelName: string): void;
addModel(providerName: ProviderName, modelName: string): void;
@ -231,12 +234,31 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
this.readAndInitializeState()
}
dangerousSetState = async (newState: VoidSettingsState) => {
this.state = _validatedModelState(newState)
await this._storeState()
this._onDidChangeState.fire()
this._onUpdate_syncApplyToChat()
}
async resetState() {
await this.dangerousSetState(defaultState())
}
async readAndInitializeState() {
let readS: VoidSettingsState
try {
readS = await this._readState();
// 1.0.3 addition, remove when enough users have had this code run
if (readS.globalSettings.includeToolLintErrors === undefined) readS.globalSettings.includeToolLintErrors = true
// autoapprove is now an obj not a boolean (1.2.5)
if (typeof readS.globalSettings.autoApprove === 'boolean') readS.globalSettings.autoApprove = {}
}
catch (e) {
readS = defaultState()

View file

@ -5,6 +5,7 @@
*--------------------------------------------------------------------------------------*/
import { defaultModelsOfProvider, defaultProviderSettings } from './modelCapabilities.js';
import { ToolApprovalType } from './toolsServiceTypes.js';
import { VoidSettingsState } from './voidSettingsService.js'
@ -96,9 +97,9 @@ export const displayInfoOfProviderName = (providerName: ProviderName): DisplayIn
else if (providerName === 'mistral') {
return { title: 'Mistral', }
}
else if (providerName === 'googleVertex') {
return { title: 'Google Vertex AI', }
}
// else if (providerName === 'googleVertex') {
// return { title: 'Google Vertex AI', }
// }
else if (providerName === 'microsoftAzure') {
return { title: 'Microsoft Azure OpenAI', }
}
@ -117,7 +118,7 @@ export const subTextMdOfProviderName = (providerName: ProviderName): string => {
if (providerName === 'xAI') return 'Get your [API Key here](https://console.x.ai).'
if (providerName === 'mistral') return 'Get your [API Key here](https://console.mistral.ai/api-keys).'
if (providerName === 'openAICompatible') return `Use any OpenAI-compatible endpoint (LM Studio, LiteLM, etc).`
if (providerName === 'googleVertex') return 'You must authenticate before using Vertex with Void. Read more about endpoints [here](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/call-vertex-using-openai-library), and regions [here](https://cloud.google.com/vertex-ai/docs/general/locations#available-regions).'
// if (providerName === 'googleVertex') return 'You must authenticate before using Vertex with Void. Read more about endpoints [here](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/call-vertex-using-openai-library), and regions [here](https://cloud.google.com/vertex-ai/docs/general/locations#available-regions).'
if (providerName === 'microsoftAzure') return 'Read more about endpoints [here](https://learn.microsoft.com/en-us/rest/api/aifoundry/model-inference/get-chat-completions/get-chat-completions?view=rest-aifoundry-model-inference-2024-05-01-preview&tabs=HTTP), and get your API key [here](https://learn.microsoft.com/en-us/azure/search/search-security-api-keys?tabs=rest-use%2Cportal-find%2Cportal-query#find-existing-keys).'
if (providerName === 'ollama') return 'If you would like to change this endpoint, please read more about [Endpoints here](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-can-i-expose-ollama-on-my-network).'
if (providerName === 'vLLM') return 'If you would like to change this endpoint, please read more about [Endpoints here](https://docs.vllm.ai/en/latest/getting_started/quickstart.html#openai-compatible-server).'
@ -148,9 +149,9 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
providerName === 'openAICompatible' ? 'sk-key...' :
providerName === 'xAI' ? 'xai-key...' :
providerName === 'mistral' ? 'api-key...' :
providerName === 'googleVertex' ? 'AIzaSy...' :
providerName === 'microsoftAzure' ? 'key-...' :
'',
// providerName === 'googleVertex' ? 'AIzaSy...' :
providerName === 'microsoftAzure' ? 'key-...' :
'',
isPasswordField: true,
}
@ -161,10 +162,10 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
providerName === 'vLLM' ? 'Endpoint' :
providerName === 'lmStudio' ? 'Endpoint' :
providerName === 'openAICompatible' ? 'baseURL' : // (do not include /chat/completions)
providerName === 'googleVertex' ? 'baseURL' :
providerName === 'microsoftAzure' ? 'baseURL' :
providerName === 'liteLLM' ? 'baseURL' :
'(never)',
// providerName === 'googleVertex' ? 'baseURL' :
providerName === 'microsoftAzure' ? 'baseURL' :
providerName === 'liteLLM' ? 'baseURL' :
'(never)',
placeholder: providerName === 'ollama' ? defaultProviderSettings.ollama.endpoint
: providerName === 'vLLM' ? defaultProviderSettings.vLLM.endpoint
@ -176,14 +177,14 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
}
}
else if (settingName === 'region') {
// vertex only
return {
title: 'Region',
placeholder: providerName === 'googleVertex' ? defaultProviderSettings.googleVertex.region
: ''
}
}
// else if (settingName === 'region') {
// // vertex only
// return {
// title: 'Region',
// placeholder: providerName === 'googleVertex' ? defaultProviderSettings.googleVertex.region
// : ''
// }
// }
else if (settingName === 'azureApiVersion') {
// azure only
return {
@ -194,12 +195,12 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
}
else if (settingName === 'project') {
return {
title: providerName === 'googleVertex' ? 'Project'
: providerName === 'microsoftAzure' ? 'Resource'
: '',
placeholder: providerName === 'googleVertex' ? 'my-project'
: providerName === 'microsoftAzure' ? 'my-resource'
: ''
title: providerName === 'microsoftAzure' ? 'Resource'
// : providerName === 'googleVertex' ? 'Project'
: '',
placeholder: providerName === 'microsoftAzure' ? 'my-resource'
// : providerName === 'googleVertex' ? 'my-project'
: ''
}
@ -227,7 +228,7 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
const defaultCustomSettings: Record<CustomSettingName, undefined> = {
apiKey: undefined,
endpoint: undefined,
region: undefined,
// region: undefined, // googleVertex
project: undefined,
azureApiVersion: undefined,
}
@ -323,12 +324,12 @@ export const defaultSettingsOfProvider: SettingsOfProvider = {
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.vLLM),
_didFillInProviderSettings: undefined,
},
googleVertex: { // aggregator (serves models from multiple providers)
...defaultCustomSettings,
...defaultProviderSettings.googleVertex,
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.googleVertex),
_didFillInProviderSettings: undefined,
},
// googleVertex: { // aggregator (serves models from multiple providers)
// ...defaultCustomSettings,
// ...defaultProviderSettings.googleVertex,
// ...modelInfoOfDefaultModelNames(defaultModelsOfProvider.googleVertex),
// _didFillInProviderSettings: undefined,
// },
microsoftAzure: { // aggregator (serves models from multiple providers)
...defaultCustomSettings,
...defaultProviderSettings.microsoftAzure,
@ -425,7 +426,7 @@ export type GlobalSettings = {
syncApplyToChat: boolean;
enableFastApply: boolean;
chatMode: ChatMode;
autoApprove: boolean;
autoApprove: { [approvalType in ToolApprovalType]?: boolean };
showInlineSuggestions: boolean;
includeToolLintErrors: boolean;
isOnboardingComplete: boolean;
@ -438,7 +439,7 @@ export const defaultGlobalSettings: GlobalSettings = {
syncApplyToChat: true,
enableFastApply: true,
chatMode: 'agent',
autoApprove: false,
autoApprove: {},
showInlineSuggestions: true,
includeToolLintErrors: true,
isOnboardingComplete: false,

View file

@ -10,7 +10,7 @@ import { Ollama } from 'ollama';
import OpenAI, { ClientOptions } from 'openai';
import { MistralCore } from '@mistralai/mistralai/core.js';
import { fimComplete } from '@mistralai/mistralai/funcs/fimComplete.js';
import { GoogleAuth } from 'google-auth-library'
// import { GoogleAuth } from 'google-auth-library'
/* eslint-enable */
import { AnthropicLLMChatMessage, LLMChatMessage, LLMFIMMessage, ModelListParams, OllamaModelResponse, OnError, OnFinalMessage, OnText, RawToolCallObj, RawToolParamsObj } from '../../common/sendLLMMessageTypes.js';
@ -42,13 +42,13 @@ const invalidApiKeyMessage = (providerName: ProviderName) => `Invalid ${displayI
// ------------ OPENAI-COMPATIBLE (HELPERS) ------------
const getGoogleApiKey = async () => {
// modulelevel singleton
const auth = new GoogleAuth({ scopes: `https://www.googleapis.com/auth/cloud-platform` });
const key = await auth.getAccessToken()
if (!key) throw new Error(`Google API failed to generate a key.`)
return key
}
// const getGoogleApiKey = async () => {
// // modulelevel singleton
// const auth = new GoogleAuth({ scopes: `https://www.googleapis.com/auth/cloud-platform` });
// const key = await auth.getAccessToken()
// if (!key) throw new Error(`Google API failed to generate a key.`)
// return key
// }
const newOpenAICompatibleSDK = async ({ settingsOfProvider, providerName, includeInPayload }: { settingsOfProvider: SettingsOfProvider, providerName: ProviderName, includeInPayload?: { [s: string]: any } }) => {
@ -92,13 +92,12 @@ const newOpenAICompatibleSDK = async ({ settingsOfProvider, providerName, includ
const thisConfig = settingsOfProvider[providerName]
return new OpenAI({ baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai', apiKey: thisConfig.apiKey, ...commonPayloadOpts })
}
else if (providerName === 'googleVertex') {
// https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/call-vertex-using-openai-library
const apiKey = await getGoogleApiKey()
const thisConfig = settingsOfProvider[providerName]
const baseURL = `https://${thisConfig.region}-aiplatform.googleapis.com/v1/projects/${thisConfig.project}/locations/${thisConfig.region}/endpoints/${'openapi'}`
return new OpenAI({ baseURL: baseURL, apiKey: apiKey, ...commonPayloadOpts })
}
// else if (providerName === 'googleVertex') {
// // https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/call-vertex-using-openai-library
// const thisConfig = settingsOfProvider[providerName]
// const baseURL = `https://${thisConfig.region}-aiplatform.googleapis.com/v1/projects/${thisConfig.project}/locations/${thisConfig.region}/endpoints/${'openapi'}`
// return new OpenAI({ baseURL: baseURL, apiKey: apiKey, ...commonPayloadOpts })
// }
else if (providerName === 'microsoftAzure') {
// https://learn.microsoft.com/en-us/rest/api/aifoundry/model-inference/get-chat-completions/get-chat-completions?view=rest-aifoundry-model-inference-2024-05-01-preview&tabs=HTTP
const thisConfig = settingsOfProvider[providerName]
@ -692,11 +691,11 @@ export const sendLLMMessageToProviderImplementation = {
sendFIM: null,
list: null,
},
googleVertex: {
sendChat: (params) => _sendOpenAICompatibleChat(params),
sendFIM: null,
list: null,
},
// googleVertex: {
// sendChat: (params) => _sendOpenAICompatibleChat(params),
// sendFIM: null,
// list: null,
// },
microsoftAzure: {
sendChat: (params) => _sendOpenAICompatibleChat(params),
sendFIM: null,