mirror of
https://github.com/voideditor/void
synced 2026-05-24 09:58:23 +00:00
commit
c775d34d75
31 changed files with 1401 additions and 1891 deletions
1589
package-lock.json
generated
1589
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -79,6 +79,7 @@
|
||||||
"@mistralai/mistralai": "^1.5.0",
|
"@mistralai/mistralai": "^1.5.0",
|
||||||
"@modelcontextprotocol/sdk": "^1.9.0",
|
"@modelcontextprotocol/sdk": "^1.9.0",
|
||||||
"@parcel/watcher": "2.5.1",
|
"@parcel/watcher": "2.5.1",
|
||||||
|
"@types/katex": "^0.16.7",
|
||||||
"@types/semver": "^7.5.8",
|
"@types/semver": "^7.5.8",
|
||||||
"@vscode/deviceid": "^0.1.1",
|
"@vscode/deviceid": "^0.1.1",
|
||||||
"@vscode/iconv-lite-umd": "0.7.0",
|
"@vscode/iconv-lite-umd": "0.7.0",
|
||||||
|
|
@ -106,10 +107,13 @@
|
||||||
"cross-spawn": "^7.0.6",
|
"cross-spawn": "^7.0.6",
|
||||||
"diff": "^7.0.0",
|
"diff": "^7.0.0",
|
||||||
"eslint-plugin-react": "^7.37.4",
|
"eslint-plugin-react": "^7.37.4",
|
||||||
|
"fast-json-stable-stringify": "^2.1.0",
|
||||||
|
"google-auth-library": "^9.15.1",
|
||||||
"groq-sdk": "^0.15.0",
|
"groq-sdk": "^0.15.0",
|
||||||
"http-proxy-agent": "^7.0.0",
|
"http-proxy-agent": "^7.0.0",
|
||||||
"https-proxy-agent": "^7.0.2",
|
"https-proxy-agent": "^7.0.2",
|
||||||
"jschardet": "3.1.4",
|
"jschardet": "3.1.4",
|
||||||
|
"katex": "^0.16.22",
|
||||||
"kerberos": "2.1.1",
|
"kerberos": "2.1.1",
|
||||||
"lucide-react": "^0.477.0",
|
"lucide-react": "^0.477.0",
|
||||||
"marked": "^15.0.7",
|
"marked": "^15.0.7",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"nameShort": "Void",
|
"nameShort": "Void",
|
||||||
"nameLong": "Void",
|
"nameLong": "Void",
|
||||||
"voidVersion": "1.2.5",
|
"voidVersion": "1.2.6",
|
||||||
"applicationName": "void",
|
"applicationName": "void",
|
||||||
"dataFolderName": ".void-editor",
|
"dataFolderName": ".void-editor",
|
||||||
"win32MutexName": "voideditor",
|
"win32MutexName": "voideditor",
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,6 @@ class DummyService extends Disposable implements IWorkbenchContribution, IDummyS
|
||||||
|
|
||||||
|
|
||||||
// pick one and delete the other:
|
// pick one and delete the other:
|
||||||
registerSingleton(IDummyService, DummyService, InstantiationType.Eager);
|
registerSingleton(IDummyService, DummyService, InstantiationType.Eager); // lazily loaded, even if Eager
|
||||||
|
|
||||||
registerWorkbenchContribution2(DummyService.ID, DummyService, WorkbenchPhase.BlockRestore);
|
registerWorkbenchContribution2(DummyService.ID, DummyService, WorkbenchPhase.BlockRestore); // mounts on start
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,9 @@ import { INotificationService, Severity } from '../../../../platform/notificatio
|
||||||
import { truncate } from '../../../../base/common/strings.js';
|
import { truncate } from '../../../../base/common/strings.js';
|
||||||
import { THREAD_STORAGE_KEY } from '../common/storageKeys.js';
|
import { THREAD_STORAGE_KEY } from '../common/storageKeys.js';
|
||||||
import { IConvertToLLMMessageService } from './convertToLLMMessageService.js';
|
import { IConvertToLLMMessageService } from './convertToLLMMessageService.js';
|
||||||
|
import { timeout } from '../../../../base/common/async.js';
|
||||||
|
|
||||||
|
const CHAT_RETRIES = 3
|
||||||
|
|
||||||
export const findStagingSelectionIndex = (currentSelections: StagingSelectionItem[] | undefined, newSelection: StagingSelectionItem): number | null => {
|
export const findStagingSelectionIndex = (currentSelections: StagingSelectionItem[] | undefined, newSelection: StagingSelectionItem): number | null => {
|
||||||
if (!currentSelections) return null
|
if (!currentSelections) return null
|
||||||
|
|
@ -91,7 +94,7 @@ const defaultMessageState: UserMessageState = {
|
||||||
|
|
||||||
// a 'thread' means a chat message history
|
// a 'thread' means a chat message history
|
||||||
|
|
||||||
type ThreadType = {
|
export type ThreadType = {
|
||||||
id: string; // store the id here too
|
id: string; // store the id here too
|
||||||
createdAt: string; // ISO string
|
createdAt: string; // ISO string
|
||||||
lastModified: string; // ISO string
|
lastModified: string; // ISO string
|
||||||
|
|
@ -177,6 +180,7 @@ export interface IChatThreadService {
|
||||||
|
|
||||||
getCurrentThread(): ThreadType;
|
getCurrentThread(): ThreadType;
|
||||||
openNewThread(): void;
|
openNewThread(): void;
|
||||||
|
deleteThread(threadId: string): void;
|
||||||
switchToThread(threadId: string): void;
|
switchToThread(threadId: string): void;
|
||||||
|
|
||||||
// exposed getters/setters
|
// exposed getters/setters
|
||||||
|
|
@ -564,7 +568,6 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||||
let nMessagesSent = 0
|
let nMessagesSent = 0
|
||||||
let shouldSendAnotherMessage = true
|
let shouldSendAnotherMessage = true
|
||||||
let isRunningWhenEnd: IsRunningType = undefined
|
let isRunningWhenEnd: IsRunningType = undefined
|
||||||
let aborted = false
|
|
||||||
|
|
||||||
// before enter loop, call tool
|
// before enter loop, call tool
|
||||||
if (callThisToolFirst) {
|
if (callThisToolFirst) {
|
||||||
|
|
@ -592,69 +595,94 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
||||||
chatMode
|
chatMode
|
||||||
})
|
})
|
||||||
|
|
||||||
const llmCancelToken = this._llmMessageService.sendLLMMessage({
|
|
||||||
messagesType: 'chatMessages',
|
|
||||||
chatMode,
|
|
||||||
messages: messages,
|
|
||||||
modelSelection,
|
|
||||||
modelSelectionOptions,
|
|
||||||
logging: { loggingName: `Chat - ${chatMode}`, loggingExtras: { threadId, nMessagesSent, chatMode } },
|
|
||||||
separateSystemMessage: separateSystemMessage,
|
|
||||||
onText: ({ fullText, fullReasoning, toolCall }) => {
|
|
||||||
this._setStreamState(threadId, { displayContentSoFar: fullText, reasoningSoFar: fullReasoning, toolCallSoFar: toolCall }, 'merge')
|
|
||||||
},
|
|
||||||
onFinalMessage: async ({ fullText, fullReasoning, toolCall, anthropicReasoning, }) => {
|
|
||||||
this._addMessageToThread(threadId, { role: 'assistant', displayContent: fullText, reasoning: fullReasoning, anthropicReasoning })
|
|
||||||
this._setStreamState(threadId, { displayContentSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, toolCallSoFar: undefined }, 'merge')
|
|
||||||
resMessageIsDonePromise(toolCall) // resolve with tool calls
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
const messageSoFar = this.streamState[threadId]?.displayContentSoFar ?? ''
|
|
||||||
const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? ''
|
|
||||||
// const toolCallSoFar = this.streamState[threadId]?.toolCallSoFar
|
|
||||||
// add assistant's message to chat history, and clear selection
|
|
||||||
this._addMessageToThread(threadId, { role: 'assistant', displayContent: messageSoFar, reasoning: reasoningSoFar, anthropicReasoning: null })
|
|
||||||
this._setStreamState(threadId, { error }, 'set')
|
|
||||||
resMessageIsDonePromise()
|
|
||||||
},
|
|
||||||
onAbort: () => {
|
|
||||||
// stop the loop to free up the promise, but don't modify state (already handled by whatever stopped it)
|
|
||||||
resMessageIsDonePromise()
|
|
||||||
this._metricsService.capture('Agent Loop Done (Aborted)', { nMessagesSent, chatMode })
|
|
||||||
aborted = true
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// should never happen, just for safety
|
let aborted = false
|
||||||
if (llmCancelToken === null) {
|
|
||||||
this._setStreamState(threadId, {
|
|
||||||
error: { message: 'There was an unexpected error when sending your chat message.', fullError: null }
|
|
||||||
}, 'set')
|
|
||||||
break
|
|
||||||
}
|
|
||||||
this._setStreamState(threadId, { streamingToken: llmCancelToken }, 'merge') // new stream token for the new message
|
|
||||||
const toolCall = await messageIsDonePromise // wait for message to complete
|
|
||||||
if (aborted) { return }
|
|
||||||
this._setStreamState(threadId, { streamingToken: undefined }, 'merge') // streaming message is done
|
|
||||||
|
|
||||||
// call tool if there is one
|
let shouldRetry = true
|
||||||
const tool: RawToolCallObj | undefined = toolCall
|
let nAttempts = 0
|
||||||
if (tool) {
|
|
||||||
const { awaitingUserApproval, interrupted } = await this._runToolCall(threadId, tool.name, tool.id, { preapproved: false, unvalidatedToolParams: tool.rawParams })
|
|
||||||
|
|
||||||
// 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.
|
while (shouldRetry) {
|
||||||
// just detect tool interruption which is the same as chat interruption right now
|
shouldRetry = false
|
||||||
if (interrupted) { return }
|
|
||||||
|
|
||||||
if (awaitingUserApproval) {
|
const llmCancelToken = this._llmMessageService.sendLLMMessage({
|
||||||
isRunningWhenEnd = 'awaiting_user'
|
messagesType: 'chatMessages',
|
||||||
|
chatMode,
|
||||||
|
messages: messages,
|
||||||
|
modelSelection,
|
||||||
|
modelSelectionOptions,
|
||||||
|
logging: { loggingName: `Chat - ${chatMode}`, loggingExtras: { threadId, nMessagesSent, chatMode } },
|
||||||
|
separateSystemMessage: separateSystemMessage,
|
||||||
|
onText: ({ fullText, fullReasoning, toolCall }) => {
|
||||||
|
this._setStreamState(threadId, { displayContentSoFar: fullText, reasoningSoFar: fullReasoning, toolCallSoFar: toolCall }, 'merge')
|
||||||
|
},
|
||||||
|
onFinalMessage: async ({ fullText, fullReasoning, toolCall, anthropicReasoning, }) => {
|
||||||
|
this._addMessageToThread(threadId, { role: 'assistant', displayContent: fullText, reasoning: fullReasoning, anthropicReasoning })
|
||||||
|
this._setStreamState(threadId, { displayContentSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, toolCallSoFar: undefined }, 'merge')
|
||||||
|
resMessageIsDonePromise(toolCall) // resolve with tool calls
|
||||||
|
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
const messageSoFar = this.streamState[threadId]?.displayContentSoFar ?? ''
|
||||||
|
const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? ''
|
||||||
|
|
||||||
|
if (nAttempts < CHAT_RETRIES) {
|
||||||
|
nAttempts += 1
|
||||||
|
shouldRetry = true
|
||||||
|
this._setStreamState(threadId, { displayContentSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, toolCallSoFar: undefined }, 'merge')
|
||||||
|
timeout(2500).then(() => { resMessageIsDonePromise() })
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// const toolCallSoFar = this.streamState[threadId]?.toolCallSoFar
|
||||||
|
// add assistant's message to chat history, and clear selection
|
||||||
|
this._addMessageToThread(threadId, { role: 'assistant', displayContent: messageSoFar, reasoning: reasoningSoFar, anthropicReasoning: null })
|
||||||
|
this._setStreamState(threadId, { error }, 'set')
|
||||||
|
resMessageIsDonePromise()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onAbort: () => {
|
||||||
|
// stop the loop to free up the promise, but don't modify state (already handled by whatever stopped it)
|
||||||
|
resMessageIsDonePromise()
|
||||||
|
this._metricsService.capture('Agent Loop Done (Aborted)', { nMessagesSent, chatMode })
|
||||||
|
aborted = true
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// should never happen, just for safety
|
||||||
|
if (llmCancelToken === null) {
|
||||||
|
this._setStreamState(threadId, {
|
||||||
|
error: { message: 'There was an unexpected error when sending your chat message.', fullError: null }
|
||||||
|
}, 'set')
|
||||||
|
break
|
||||||
}
|
}
|
||||||
else {
|
this._setStreamState(threadId, { streamingToken: llmCancelToken }, 'merge') // new stream token for the new message
|
||||||
shouldSendAnotherMessage = true
|
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
|
||||||
|
|
||||||
} // end while
|
// call tool if there is one
|
||||||
|
const tool: RawToolCallObj | undefined = toolCall
|
||||||
|
if (tool) {
|
||||||
|
const { awaitingUserApproval, interrupted } = await this._runToolCall(threadId, tool.name, tool.id, { preapproved: false, unvalidatedToolParams: tool.rawParams })
|
||||||
|
|
||||||
|
// stop if interrupted. we don't have to do this for llmMessage because we have a stream token for it and onAbort gets called, but we don't have the equivalent for tools.
|
||||||
|
// just detect tool interruption which is the same as chat interruption right now
|
||||||
|
if (interrupted) { return }
|
||||||
|
|
||||||
|
if (awaitingUserApproval) {
|
||||||
|
isRunningWhenEnd = 'awaiting_user'
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
shouldSendAnotherMessage = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // end while (attempts)
|
||||||
|
} // end while (send message)
|
||||||
|
|
||||||
|
|
||||||
// if awaiting user approval, keep isRunning true, else end isRunning
|
// if awaiting user approval, keep isRunning true, else end isRunning
|
||||||
|
|
@ -1389,6 +1417,19 @@ We only need to do it for files that were edited since `from`, ie files between
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
deleteThread(threadId: string): void {
|
||||||
|
const { allThreads: currentThreads } = this.state
|
||||||
|
|
||||||
|
// delete the thread
|
||||||
|
const newThreads = { ...currentThreads };
|
||||||
|
delete newThreads[threadId];
|
||||||
|
|
||||||
|
// store the updated threads
|
||||||
|
this._storeAllThreads(newThreads);
|
||||||
|
this._setState({ ...this.state, allThreads: newThreads }, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private _addMessageToThread(threadId: string, message: ChatMessage) {
|
private _addMessageToThread(threadId: string, message: ChatMessage) {
|
||||||
const { allThreads } = this.state
|
const { allThreads } = this.state
|
||||||
const oldThread = allThreads[threadId]
|
const oldThread = allThreads[threadId]
|
||||||
|
|
|
||||||
|
|
@ -438,27 +438,33 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess
|
||||||
super()
|
super()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read .voidinstructions files from workspace folders
|
// Read .voidrules files from workspace folders
|
||||||
private _getVoidInstructionsFileContents(): string {
|
private _getVoidRulesFileContents(): string {
|
||||||
const workspaceFolders = this.workspaceContextService.getWorkspace().folders;
|
try {
|
||||||
let voidInstructions = '';
|
const workspaceFolders = this.workspaceContextService.getWorkspace().folders;
|
||||||
for (const folder of workspaceFolders) {
|
let voidRules = '';
|
||||||
const uri = URI.joinPath(folder.uri, '.voidinstructions')
|
for (const folder of workspaceFolders) {
|
||||||
const { model } = this.voidModelService.getModel(uri)
|
const uri = URI.joinPath(folder.uri, '.voidrules')
|
||||||
if (!model) continue
|
const { model } = this.voidModelService.getModel(uri)
|
||||||
voidInstructions += model.getValue() + '\n\n';
|
if (!model) continue
|
||||||
|
voidRules += model.getValue() + '\n\n';
|
||||||
|
}
|
||||||
|
return voidRules.trim();
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.log('Could not read .voidrules, continuing...')
|
||||||
|
return ''
|
||||||
}
|
}
|
||||||
return voidInstructions.trim();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get combined AI instructions from settings and .voidinstructions files
|
// Get combined AI instructions from settings and .voidrules files
|
||||||
private _getCombinedAIInstructions(): string {
|
private _getCombinedAIInstructions(): string {
|
||||||
const globalAIInstructions = this.voidSettingsService.state.globalSettings.aiInstructions;
|
const globalAIInstructions = this.voidSettingsService.state.globalSettings.aiInstructions;
|
||||||
const voidInstructionsFileContent = this._getVoidInstructionsFileContents();
|
const voidRulesFileContent = this._getVoidRulesFileContents();
|
||||||
|
|
||||||
const ans: string[] = []
|
const ans: string[] = []
|
||||||
if (globalAIInstructions) ans.push(globalAIInstructions)
|
if (globalAIInstructions) ans.push(globalAIInstructions)
|
||||||
if (voidInstructionsFileContent) ans.push(voidInstructionsFileContent)
|
if (voidRulesFileContent) ans.push(voidRulesFileContent)
|
||||||
return ans.join('\n\n')
|
return ans.join('\n\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,8 @@ class ConvertContribWorkbenchContribution extends Disposable implements IWorkben
|
||||||
|
|
||||||
const initializeURI = (uri: URI) => {
|
const initializeURI = (uri: URI) => {
|
||||||
this.workspaceContext.getWorkspace()
|
this.workspaceContext.getWorkspace()
|
||||||
const voidInstrsURI = URI.joinPath(uri, '.voidinstructions')
|
const voidRulesURI = URI.joinPath(uri, '.voidrules')
|
||||||
this.voidModelService.initializeModel(voidInstrsURI)
|
this.voidModelService.initializeModel(voidRulesURI)
|
||||||
}
|
}
|
||||||
|
|
||||||
// call
|
// call
|
||||||
|
|
|
||||||
|
|
@ -327,7 +327,7 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService {
|
||||||
|
|
||||||
async getDirectoryStrTool(uri: URI, options?: { maxItemsPerDir?: number }) {
|
async getDirectoryStrTool(uri: URI, options?: { maxItemsPerDir?: number }) {
|
||||||
const eRoot = this.explorerService.findClosest(uri)
|
const eRoot = this.explorerService.findClosest(uri)
|
||||||
if (!eRoot) throw new Error(`There was a problem reading the URI: ${uri.fsPath}.`)
|
if (!eRoot) throw new Error(`The folder ${uri.fsPath} does not exist.`)
|
||||||
|
|
||||||
const maxItemsPerDir = options?.maxItemsPerDir ?? START_MAX_ITEMS_PER_DIR; // Use START_MAX_ITEMS_PER_DIR
|
const maxItemsPerDir = options?.maxItemsPerDir ?? START_MAX_ITEMS_PER_DIR; // Use START_MAX_ITEMS_PER_DIR
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ export const IconShell1 = ({ onClick, Icon, disabled, className, ...props }: Ico
|
||||||
size-[18px]
|
size-[18px]
|
||||||
p-[2px]
|
p-[2px]
|
||||||
flex items-center justify-center
|
flex items-center justify-center
|
||||||
text-sm bg-void-bg-3 text-void-fg-3
|
text-sm text-void-fg-3
|
||||||
hover:brightness-110
|
hover:brightness-110
|
||||||
disabled:opacity-50 disabled:cursor-not-allowed
|
disabled:opacity-50 disabled:cursor-not-allowed
|
||||||
${className}
|
${className}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,9 @@
|
||||||
|
|
||||||
import React, { JSX, useMemo, useState } from 'react'
|
import React, { JSX, useMemo, useState } from 'react'
|
||||||
import { marked, MarkedToken, Token } from 'marked'
|
import { marked, MarkedToken, Token } from 'marked'
|
||||||
|
import katex from 'katex'
|
||||||
|
import 'katex/dist/katex.min.css'
|
||||||
|
import dompurify from '../../../../../../../base/browser/dompurify/dompurify.js'
|
||||||
|
|
||||||
import { convertToVscodeLang, detectLanguage } from '../../../../common/helpers/languageHelpers.js'
|
import { convertToVscodeLang, detectLanguage } from '../../../../common/helpers/languageHelpers.js'
|
||||||
import { BlockCodeApplyWrapper } from './ApplyBlockHoverButtons.js'
|
import { BlockCodeApplyWrapper } from './ApplyBlockHoverButtons.js'
|
||||||
|
|
@ -31,6 +34,63 @@ function isValidUri(s: string): boolean {
|
||||||
return s.length > 5 && isAbsolute(s) && !s.includes('//') && !s.includes('/*') // common case that is a false positive is comments like //
|
return s.length > 5 && isAbsolute(s) && !s.includes('//') && !s.includes('/*') // common case that is a false positive is comments like //
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// renders contiguous string of latex eg $e^{i\pi}$
|
||||||
|
const LatexRender = ({ latex }: { latex: string }) => {
|
||||||
|
|
||||||
|
try {
|
||||||
|
let formula = latex;
|
||||||
|
let displayMode = false;
|
||||||
|
|
||||||
|
// Extract the formula from delimiters
|
||||||
|
if (latex.startsWith('$') && latex.endsWith('$')) {
|
||||||
|
// Check if it's display math $$...$$
|
||||||
|
if (latex.startsWith('$$') && latex.endsWith('$$')) {
|
||||||
|
formula = latex.slice(2, -2);
|
||||||
|
displayMode = true;
|
||||||
|
} else {
|
||||||
|
formula = latex.slice(1, -1);
|
||||||
|
}
|
||||||
|
} else if (latex.startsWith('\\(') && latex.endsWith('\\)')) {
|
||||||
|
formula = latex.slice(2, -2);
|
||||||
|
} else if (latex.startsWith('\\[') && latex.endsWith('\\]')) {
|
||||||
|
formula = latex.slice(2, -2);
|
||||||
|
displayMode = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render LaTeX
|
||||||
|
const html = katex.renderToString(formula, {
|
||||||
|
displayMode: displayMode,
|
||||||
|
throwOnError: false,
|
||||||
|
output: 'html'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sanitize the HTML output with DOMPurify
|
||||||
|
const sanitizedHtml = dompurify.sanitize(html, {
|
||||||
|
RETURN_TRUSTED_TYPE: true,
|
||||||
|
USE_PROFILES: { html: true, svg: true, mathMl: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add proper styling based on mode
|
||||||
|
const className = displayMode
|
||||||
|
? 'katex-block my-2 text-center'
|
||||||
|
: 'katex-inline';
|
||||||
|
|
||||||
|
// Use the ref approach to avoid dangerouslySetInnerHTML
|
||||||
|
const mathRef = React.useRef<HTMLSpanElement>(null);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (mathRef.current) {
|
||||||
|
mathRef.current.innerHTML = sanitizedHtml as unknown as string;
|
||||||
|
}
|
||||||
|
}, [sanitizedHtml]);
|
||||||
|
|
||||||
|
return <span ref={mathRef} className={className}></span>;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('KaTeX rendering error:', error);
|
||||||
|
return <span className="katex-error text-red-500">{latex}</span>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const Codespan = ({ text, className, onClick }: { text: string, className?: string, onClick?: () => void }) => {
|
const Codespan = ({ text, className, onClick }: { text: string, className?: string, onClick?: () => void }) => {
|
||||||
|
|
||||||
// TODO compute this once for efficiency. we should use `labels.ts/shorten` to display duplicates properly
|
// TODO compute this once for efficiency. we should use `labels.ts/shorten` to display duplicates properly
|
||||||
|
|
@ -108,6 +168,105 @@ const CodespanWithLink = ({ text, rawText, chatMessageLocation }: { text: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const paragraphToLatexSegments = (paragraphText: string) => {
|
||||||
|
|
||||||
|
const segments: React.ReactNode[] = [];
|
||||||
|
|
||||||
|
if (paragraphText
|
||||||
|
&& !(paragraphText.includes('#') || paragraphText.includes('`')) // don't process latex if a codespan or header tag
|
||||||
|
&& !/^[\w\s.()[\]{}]+$/.test(paragraphText) // don't process latex if string only contains alphanumeric chars, whitespace, periods, and brackets
|
||||||
|
) {
|
||||||
|
const rawText = paragraphText;
|
||||||
|
// Regular expressions to match LaTeX delimiters
|
||||||
|
const displayMathRegex = /\$\$(.*?)\$\$/g; // Display math: $$...$$
|
||||||
|
const inlineMathRegex = /\$((?!\$).*?)\$/g; // Inline math: $...$ (but not $$)
|
||||||
|
|
||||||
|
// Check if the paragraph contains any LaTeX expressions
|
||||||
|
if (displayMathRegex.test(rawText) || inlineMathRegex.test(rawText)) {
|
||||||
|
// Reset the regex state (since we used .test earlier)
|
||||||
|
displayMathRegex.lastIndex = 0;
|
||||||
|
inlineMathRegex.lastIndex = 0;
|
||||||
|
|
||||||
|
// Parse the text into segments of regular text and LaTeX
|
||||||
|
let lastIndex = 0;
|
||||||
|
let segmentId = 0;
|
||||||
|
|
||||||
|
// First replace display math ($$...$$)
|
||||||
|
let match;
|
||||||
|
while ((match = displayMathRegex.exec(rawText)) !== null) {
|
||||||
|
const [fullMatch, formula] = match;
|
||||||
|
const matchIndex = match.index;
|
||||||
|
|
||||||
|
// Add text before the LaTeX expression
|
||||||
|
if (matchIndex > lastIndex) {
|
||||||
|
const textBefore = rawText.substring(lastIndex, matchIndex);
|
||||||
|
segments.push(
|
||||||
|
<span key={`text-${segmentId++}`}>
|
||||||
|
{textBefore}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the LaTeX expression
|
||||||
|
segments.push(
|
||||||
|
<LatexRender key={`latex-${segmentId++}`} latex={fullMatch} />
|
||||||
|
);
|
||||||
|
|
||||||
|
lastIndex = matchIndex + fullMatch.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add any remaining text (which might contain inline math)
|
||||||
|
if (lastIndex < rawText.length) {
|
||||||
|
const remainingText = rawText.substring(lastIndex);
|
||||||
|
|
||||||
|
// Process inline math in the remaining text
|
||||||
|
lastIndex = 0;
|
||||||
|
inlineMathRegex.lastIndex = 0;
|
||||||
|
const inlineSegments: React.ReactNode[] = [];
|
||||||
|
|
||||||
|
while ((match = inlineMathRegex.exec(remainingText)) !== null) {
|
||||||
|
const [fullMatch] = match;
|
||||||
|
const matchIndex = match.index;
|
||||||
|
|
||||||
|
// Add text before the inline LaTeX
|
||||||
|
if (matchIndex > lastIndex) {
|
||||||
|
const textBefore = remainingText.substring(lastIndex, matchIndex);
|
||||||
|
inlineSegments.push(
|
||||||
|
<span key={`inline-text-${segmentId++}`}>
|
||||||
|
{textBefore}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the inline LaTeX
|
||||||
|
inlineSegments.push(
|
||||||
|
<LatexRender key={`inline-latex-${segmentId++}`} latex={fullMatch} />
|
||||||
|
);
|
||||||
|
|
||||||
|
lastIndex = matchIndex + fullMatch.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add any remaining text after all inline math
|
||||||
|
if (lastIndex < remainingText.length) {
|
||||||
|
inlineSegments.push(
|
||||||
|
<span key={`inline-final-${segmentId++}`}>
|
||||||
|
{remainingText.substring(lastIndex)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
segments.push(...inlineSegments);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return segments
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export type RenderTokenOptions = { isApplyEnabled?: boolean, isLinkDetectionEnabled?: boolean }
|
export type RenderTokenOptions = { isApplyEnabled?: boolean, isLinkDetectionEnabled?: boolean }
|
||||||
const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ...options }: { token: Token | string, inPTag?: boolean, codeURI?: URI, chatMessageLocation?: ChatMessageLocation, tokenIdx: string, } & RenderTokenOptions): React.ReactNode => {
|
const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ...options }: { token: Token | string, inPTag?: boolean, codeURI?: URI, chatMessageLocation?: ChatMessageLocation, tokenIdx: string, } & RenderTokenOptions): React.ReactNode => {
|
||||||
const accessor = useAccessor()
|
const accessor = useAccessor()
|
||||||
|
|
@ -189,24 +348,25 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
|
||||||
}
|
}
|
||||||
|
|
||||||
if (t.type === 'table') {
|
if (t.type === 'table') {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
{t.header.map((cell: any, index: number) => (
|
{t.header.map((h, hIdx: number) => (
|
||||||
<th key={index}>
|
<th key={hIdx}>
|
||||||
{cell.raw}
|
{h.text}
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{t.rows.map((row: any[], rowIndex: number) => (
|
{t.rows.map((row, rowIdx: number) => (
|
||||||
<tr key={rowIndex}>
|
<tr key={rowIdx}>
|
||||||
{row.map((cell: any, cellIndex: number) => (
|
{row.map((r, rIdx: number) => (
|
||||||
<td key={cellIndex} >
|
<td key={rIdx} >
|
||||||
{cell.raw}
|
{r.text}
|
||||||
</td>
|
</td>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -288,6 +448,17 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
|
||||||
}
|
}
|
||||||
|
|
||||||
if (t.type === 'paragraph') {
|
if (t.type === 'paragraph') {
|
||||||
|
|
||||||
|
// check for latex
|
||||||
|
const latexSegments = paragraphToLatexSegments(t.raw)
|
||||||
|
if (latexSegments.length !== 0) {
|
||||||
|
if (inPTag) {
|
||||||
|
return <span className='block'>{latexSegments}</span>;
|
||||||
|
}
|
||||||
|
return <p>{latexSegments}</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if no latex, default behavior
|
||||||
const contents = <>
|
const contents = <>
|
||||||
{t.tokens.map((token, index) => (
|
{t.tokens.map((token, index) => (
|
||||||
<RenderToken key={index}
|
<RenderToken key={index}
|
||||||
|
|
@ -384,4 +555,3 @@ export const ChatMarkdownRender = ({ string, inPTag = false, chatMessageLocation
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import { IDisposable } from '../../../../../../../base/common/lifecycle.js';
|
||||||
import { ErrorDisplay } from './ErrorDisplay.js';
|
import { ErrorDisplay } from './ErrorDisplay.js';
|
||||||
import { BlockCode, TextAreaFns, VoidCustomDropdownBox, VoidInputBox2, VoidSlider, VoidSwitch } from '../util/inputs.js';
|
import { BlockCode, TextAreaFns, VoidCustomDropdownBox, VoidInputBox2, VoidSlider, VoidSwitch } from '../util/inputs.js';
|
||||||
import { ModelDropdown, } from '../void-settings-tsx/ModelDropdown.js';
|
import { ModelDropdown, } from '../void-settings-tsx/ModelDropdown.js';
|
||||||
import { SidebarThreadSelector } from './SidebarThreadSelector.js';
|
import { OldSidebarThreadSelector, PastThreadsList } from './SidebarThreadSelector.js';
|
||||||
import { VOID_CTRL_L_ACTION_ID } from '../../../actionIDs.js';
|
import { VOID_CTRL_L_ACTION_ID } from '../../../actionIDs.js';
|
||||||
import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js';
|
import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js';
|
||||||
import { ChatMode, displayInfoOfProviderName, FeatureName, isFeatureNameDisabled } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js';
|
import { ChatMode, displayInfoOfProviderName, FeatureName, isFeatureNameDisabled } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js';
|
||||||
|
|
@ -29,6 +29,7 @@ import { acceptAllBg, acceptBorder, buttonFontSize, buttonTextColor, rejectAllBg
|
||||||
import { ToolName, toolNames } from '../../../../common/prompt/prompts.js';
|
import { ToolName, toolNames } from '../../../../common/prompt/prompts.js';
|
||||||
import { RawToolCallObj } from '../../../../common/sendLLMMessageTypes.js';
|
import { RawToolCallObj } from '../../../../common/sendLLMMessageTypes.js';
|
||||||
import { MAX_FILE_CHARS_PAGE } from '../../../toolsService.js';
|
import { MAX_FILE_CHARS_PAGE } from '../../../toolsService.js';
|
||||||
|
import jsonStringify from 'fast-json-stable-stringify'
|
||||||
import ErrorBoundary from './ErrorBoundary.js';
|
import ErrorBoundary from './ErrorBoundary.js';
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -143,9 +144,6 @@ export const IconLoading = ({ className = '' }: { className?: string }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const getChatBubbleId = (threadId: string, messageIdx: number) => `${threadId}-${messageIdx}`;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// SLIDER ONLY:
|
// SLIDER ONLY:
|
||||||
const ReasoningOptionSlider = ({ featureName }: { featureName: FeatureName }) => {
|
const ReasoningOptionSlider = ({ featureName }: { featureName: FeatureName }) => {
|
||||||
|
|
@ -554,8 +552,7 @@ export const SelectedFiles = (
|
||||||
{allSelections.map((selection, i) => {
|
{allSelections.map((selection, i) => {
|
||||||
|
|
||||||
const isThisSelectionProspective = i > selections.length - 1
|
const isThisSelectionProspective = i > selections.length - 1
|
||||||
|
const thisKey = jsonStringify(selection)
|
||||||
const thisKey = `${isThisSelectionProspective}-${i}-${selections.length}`
|
|
||||||
|
|
||||||
return <div // container for summarybox and code
|
return <div // container for summarybox and code
|
||||||
key={thisKey}
|
key={thisKey}
|
||||||
|
|
@ -1979,7 +1976,13 @@ type ChatBubbleProps = {
|
||||||
_scrollToBottom: (() => void) | null,
|
_scrollToBottom: (() => void) | null,
|
||||||
}
|
}
|
||||||
|
|
||||||
const ChatBubble = ({ threadId, chatMessage, currCheckpointIdx, isCommitted, messageIdx, chatIsRunning, _scrollToBottom }: ChatBubbleProps) => {
|
const ChatBubble = (props: ChatBubbleProps) => {
|
||||||
|
return <ErrorBoundary>
|
||||||
|
<_ChatBubble {...props} />
|
||||||
|
</ErrorBoundary>
|
||||||
|
}
|
||||||
|
|
||||||
|
const _ChatBubble = ({ threadId, chatMessage, currCheckpointIdx, isCommitted, messageIdx, chatIsRunning, _scrollToBottom }: ChatBubbleProps) => {
|
||||||
const role = chatMessage.role
|
const role = chatMessage.role
|
||||||
|
|
||||||
const isCheckpointGhost = messageIdx > (currCheckpointIdx ?? Infinity) && !chatIsRunning // whether to show as gray (if chat is running, for good measure just dont show any ghosts)
|
const isCheckpointGhost = messageIdx > (currCheckpointIdx ?? Infinity) && !chatIsRunning // whether to show as gray (if chat is running, for good measure just dont show any ghosts)
|
||||||
|
|
@ -2001,33 +2004,6 @@ const ChatBubble = ({ threadId, chatMessage, currCheckpointIdx, isCommitted, mes
|
||||||
isCommitted={isCommitted}
|
isCommitted={isCommitted}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
// else if (role === 'tool_request') {
|
|
||||||
// const ToolRequestWrapper = toolNameToComponent[chatMessage.name]?.requestWrapper as RequestWrapper<ToolName>
|
|
||||||
// const toolRequestState = (
|
|
||||||
// chatIsRunning === 'awaiting_user' ? 'awaiting_user'
|
|
||||||
// : chatIsRunning === 'tool' ? 'running'
|
|
||||||
// : chatIsRunning === 'message' ? null
|
|
||||||
// : null
|
|
||||||
// )
|
|
||||||
// if (ToolRequestWrapper && canAcceptReject) { // if it's the last message
|
|
||||||
// return <>
|
|
||||||
// {toolRequestState !== null &&
|
|
||||||
// <div className={`${isCheckpointGhost ? 'opacity-50' : ''}`}>
|
|
||||||
// <ToolRequestWrapper
|
|
||||||
// toolRequestState={toolRequestState}
|
|
||||||
// toolRequest={chatMessage}
|
|
||||||
// messageIdx={messageIdx}
|
|
||||||
// threadId={threadId}
|
|
||||||
// />
|
|
||||||
// </div>}
|
|
||||||
// {chatIsRunning === 'awaiting_user' &&
|
|
||||||
// <div className={`${isCheckpointGhost ? 'opacity-50 pointer-events-none' : ''}`}>
|
|
||||||
// <ToolRequestAcceptRejectButtons />
|
|
||||||
// </div>}
|
|
||||||
// </>
|
|
||||||
// }
|
|
||||||
// return null
|
|
||||||
// }
|
|
||||||
else if (role === 'tool') {
|
else if (role === 'tool') {
|
||||||
|
|
||||||
if (chatMessage.type === 'invalid_params') {
|
if (chatMessage.type === 'invalid_params') {
|
||||||
|
|
@ -2537,8 +2513,8 @@ export const SidebarChat = () => {
|
||||||
// const lastMessageIdx = previousMessages.findLastIndex(v => v.role !== 'checkpoint')
|
// const lastMessageIdx = previousMessages.findLastIndex(v => v.role !== 'checkpoint')
|
||||||
// tool request shows up as Editing... if in progress
|
// tool request shows up as Editing... if in progress
|
||||||
return previousMessages.map((message, i) => {
|
return previousMessages.map((message, i) => {
|
||||||
return <ErrorBoundary><ChatBubble
|
return <ChatBubble
|
||||||
key={getChatBubbleId(threadId, i)}
|
key={i}
|
||||||
currCheckpointIdx={currCheckpointIdx}
|
currCheckpointIdx={currCheckpointIdx}
|
||||||
chatMessage={message}
|
chatMessage={message}
|
||||||
messageIdx={i}
|
messageIdx={i}
|
||||||
|
|
@ -2546,14 +2522,14 @@ export const SidebarChat = () => {
|
||||||
chatIsRunning={isRunning}
|
chatIsRunning={isRunning}
|
||||||
threadId={threadId}
|
threadId={threadId}
|
||||||
_scrollToBottom={() => scrollToBottom(scrollContainerRef)}
|
_scrollToBottom={() => scrollToBottom(scrollContainerRef)}
|
||||||
/></ErrorBoundary>
|
/>
|
||||||
})
|
})
|
||||||
}, [previousMessages, threadId, currCheckpointIdx, isRunning])
|
}, [previousMessages, threadId, currCheckpointIdx, isRunning])
|
||||||
|
|
||||||
const streamingChatIdx = previousMessagesHTML.length
|
const streamingChatIdx = previousMessagesHTML.length
|
||||||
const currStreamingMessageHTML = reasoningSoFar || displayContentSoFar || isRunning ?
|
const currStreamingMessageHTML = reasoningSoFar || displayContentSoFar || isRunning ?
|
||||||
<ErrorBoundary><ChatBubble
|
<ChatBubble
|
||||||
key={getChatBubbleId(threadId, streamingChatIdx)}
|
key={'curr-streaming-msg'}
|
||||||
currCheckpointIdx={currCheckpointIdx}
|
currCheckpointIdx={currCheckpointIdx}
|
||||||
chatMessage={{
|
chatMessage={{
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
|
|
@ -2567,13 +2543,13 @@ export const SidebarChat = () => {
|
||||||
|
|
||||||
threadId={threadId}
|
threadId={threadId}
|
||||||
_scrollToBottom={null}
|
_scrollToBottom={null}
|
||||||
/></ErrorBoundary> : null
|
/> : null
|
||||||
|
|
||||||
|
|
||||||
// the tool currently being generated
|
// the tool currently being generated
|
||||||
const generatingTool = toolIsGenerating ?
|
const generatingTool = toolIsGenerating ?
|
||||||
toolCallSoFar.name === 'edit_file' ? <EditToolSoFar
|
toolCallSoFar.name === 'edit_file' ? <EditToolSoFar
|
||||||
key={getChatBubbleId(threadId, streamingChatIdx + 1)}
|
key={'curr-streaming-tool'}
|
||||||
toolCallSoFar={toolCallSoFar}
|
toolCallSoFar={toolCallSoFar}
|
||||||
/>
|
/>
|
||||||
: null
|
: null
|
||||||
|
|
@ -2631,61 +2607,106 @@ export const SidebarChat = () => {
|
||||||
}
|
}
|
||||||
}, [onSubmit, onAbort, isRunning])
|
}, [onSubmit, onAbort, isRunning])
|
||||||
|
|
||||||
const inputForm = <div key={'input' + chatThreadsState.currentThreadId}>
|
|
||||||
<div className='px-4'>
|
|
||||||
{previousMessages.length > 0 &&
|
|
||||||
<CommandBarInChat />
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className='px-2 pb-2'
|
|
||||||
>
|
|
||||||
<VoidChatArea
|
|
||||||
featureName='Chat'
|
|
||||||
onSubmit={onSubmit}
|
|
||||||
onAbort={onAbort}
|
|
||||||
isStreaming={!!isRunning}
|
|
||||||
isDisabled={isDisabled}
|
|
||||||
showSelections={true}
|
|
||||||
showProspectiveSelections={previousMessagesHTML.length === 0}
|
|
||||||
selections={selections}
|
|
||||||
setSelections={setSelections}
|
|
||||||
onClickAnywhere={() => { textAreaRef.current?.focus() }}
|
|
||||||
>
|
|
||||||
<VoidInputBox2
|
|
||||||
className={`min-h-[81px] px-0.5 py-0.5`}
|
|
||||||
placeholder={`${keybindingString ? `${keybindingString} to add a file. ` : ''}Enter instructions...`}
|
|
||||||
onChangeText={onChangeText}
|
|
||||||
onKeyDown={onKeyDown}
|
|
||||||
onFocus={() => { chatThreadsService.setCurrentlyFocusedMessageIdx(undefined) }}
|
|
||||||
ref={textAreaRef}
|
|
||||||
fnsRef={textAreaFnsRef}
|
|
||||||
multiline={true}
|
|
||||||
/>
|
|
||||||
|
|
||||||
</VoidChatArea>
|
|
||||||
|
|
||||||
|
const inputChatArea = <VoidChatArea
|
||||||
|
featureName='Chat'
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
onAbort={onAbort}
|
||||||
|
isStreaming={!!isRunning}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
showSelections={true}
|
||||||
|
showProspectiveSelections={previousMessagesHTML.length === 0}
|
||||||
|
selections={selections}
|
||||||
|
setSelections={setSelections}
|
||||||
|
onClickAnywhere={() => { textAreaRef.current?.focus() }}
|
||||||
|
>
|
||||||
|
<VoidInputBox2
|
||||||
|
className={`min-h-[81px] px-0.5 py-0.5`}
|
||||||
|
placeholder={`${keybindingString ? `${keybindingString} to add a file. ` : ''}Enter instructions...`}
|
||||||
|
onChangeText={onChangeText}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
onFocus={() => { chatThreadsService.setCurrentlyFocusedMessageIdx(undefined) }}
|
||||||
|
ref={textAreaRef}
|
||||||
|
fnsRef={textAreaFnsRef}
|
||||||
|
multiline={true}
|
||||||
|
/>
|
||||||
|
|
||||||
|
</VoidChatArea>
|
||||||
|
|
||||||
|
|
||||||
|
const isLandingPage = previousMessages.length === 0
|
||||||
|
|
||||||
|
|
||||||
|
const threadPageInput = <div key={'input' + chatThreadsState.currentThreadId}>
|
||||||
|
<div className='px-4'>
|
||||||
|
<CommandBarInChat />
|
||||||
|
</div>
|
||||||
|
<div className='px-2 pb-2'>
|
||||||
|
{inputChatArea}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
return (
|
const landingPageInput = <div>
|
||||||
<div ref={sidebarRef} className='w-full h-full flex flex-col overflow-hidden'>
|
<div className='pt-8'>
|
||||||
{/* History selector */}
|
{inputChatArea}
|
||||||
<div className={`w-full ${isHistoryOpen ? '' : 'hidden'} ring-2 ring-widget-shadow ring-inset z-10`}>
|
|
||||||
<ErrorBoundary>
|
|
||||||
<SidebarThreadSelector />
|
|
||||||
</ErrorBoundary>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='flex-1 flex flex-col overflow-hidden'>
|
|
||||||
<div className={`flex-1 overflow-hidden ${previousMessages.length === 0 ? 'h-0 max-h-0 pb-2' : ''}`}>
|
|
||||||
<ErrorBoundary>
|
|
||||||
{messagesHTML}
|
|
||||||
</ErrorBoundary>
|
|
||||||
</div>
|
|
||||||
<ErrorBoundary>
|
|
||||||
{inputForm}
|
|
||||||
</ErrorBoundary>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
const landingPageContent = <div
|
||||||
|
ref={sidebarRef}
|
||||||
|
className='w-full h-full max-h-full flex flex-col overflow-auto px-4'
|
||||||
|
>
|
||||||
|
<ErrorBoundary>
|
||||||
|
{landingPageInput}
|
||||||
|
</ErrorBoundary>
|
||||||
|
|
||||||
|
{Object.values(chatThreadsState.allThreads).length > 0 && // show if there are threads
|
||||||
|
<ErrorBoundary>
|
||||||
|
<div className='pt-8 mb-2 text-void-fg-1 text-root'>Previous Threads</div>
|
||||||
|
<PastThreadsList />
|
||||||
|
</ErrorBoundary>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
// const threadPageContent = <div>
|
||||||
|
// {/* Thread content */}
|
||||||
|
// <div className='flex flex-col overflow-hidden'>
|
||||||
|
// <div className={`overflow-hidden ${previousMessages.length === 0 ? 'h-0 max-h-0 pb-2' : ''}`}>
|
||||||
|
// <ErrorBoundary>
|
||||||
|
// {messagesHTML}
|
||||||
|
// </ErrorBoundary>
|
||||||
|
// </div>
|
||||||
|
// <ErrorBoundary>
|
||||||
|
// {inputForm}
|
||||||
|
// </ErrorBoundary>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
const threadPageContent = <div
|
||||||
|
ref={sidebarRef}
|
||||||
|
className='w-full h-full flex flex-col overflow-hidden'
|
||||||
|
>
|
||||||
|
|
||||||
|
<ErrorBoundary>
|
||||||
|
{messagesHTML}
|
||||||
|
</ErrorBoundary>
|
||||||
|
<ErrorBoundary>
|
||||||
|
{threadPageInput}
|
||||||
|
</ErrorBoundary>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment key={threadId} // force rerender when change thread
|
||||||
|
>
|
||||||
|
{isLandingPage ?
|
||||||
|
landingPageContent
|
||||||
|
: threadPageContent}
|
||||||
|
</Fragment>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,35 +3,20 @@
|
||||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||||
*--------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
import React from "react";
|
import { useState } from 'react';
|
||||||
|
import { IconShell1 } from '../markdown/ApplyBlockHoverButtons.js';
|
||||||
import { useAccessor, useChatThreadsState } from '../util/services.js';
|
import { useAccessor, useChatThreadsState } from '../util/services.js';
|
||||||
import { ISidebarStateService } from '../../../sidebarStateService.js';
|
|
||||||
import { IconX } from './SidebarChat.js';
|
import { IconX } from './SidebarChat.js';
|
||||||
|
import { Check, Trash2, X } from 'lucide-react';
|
||||||
|
import { ThreadType } from '../../../chatThreadService.js';
|
||||||
|
|
||||||
|
|
||||||
const truncate = (s: string) => {
|
export const OldSidebarThreadSelector = () => {
|
||||||
let len = s.length
|
|
||||||
const TRUNC_AFTER = 16
|
|
||||||
if (len >= TRUNC_AFTER)
|
|
||||||
s = s.substring(0, TRUNC_AFTER) + '...'
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const SidebarThreadSelector = () => {
|
|
||||||
const threadsState = useChatThreadsState()
|
|
||||||
|
|
||||||
const accessor = useAccessor()
|
const accessor = useAccessor()
|
||||||
const chatThreadsService = accessor.get('IChatThreadService')
|
|
||||||
const sidebarStateService = accessor.get('ISidebarStateService')
|
const sidebarStateService = accessor.get('ISidebarStateService')
|
||||||
|
|
||||||
const { allThreads } = threadsState
|
|
||||||
|
|
||||||
// sorted by most recent to least recent
|
|
||||||
const sortedThreadIds = Object.keys(allThreads ?? {})
|
|
||||||
.sort((threadId1, threadId2) => (allThreads[threadId1]?.lastModified ?? 0) > (allThreads[threadId2]?.lastModified ?? 0) ? -1 : 1)
|
|
||||||
.filter(threadId => (allThreads![threadId]?.messages.length ?? 0) !== 0)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex p-2 flex-col gap-y-1 max-h-[200px] overflow-y-auto">
|
<div className="flex p-2 flex-col gap-y-1 max-h-[200px] overflow-y-auto">
|
||||||
|
|
||||||
|
|
@ -52,72 +37,297 @@ export const SidebarThreadSelector = () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* a list of all the past threads */}
|
{/* a list of all the past threads */}
|
||||||
<div className="px-1">
|
{/* <OldPastThreadsList /> */}
|
||||||
<ul className="flex flex-col gap-y-0.5 overflow-y-auto list-disc">
|
|
||||||
|
|
||||||
{sortedThreadIds.length === 0
|
|
||||||
|
|
||||||
? <div key="nothreads" className="text-center text-void-fg-3 brightness-90 text-sm">{`There are no chat threads yet.`}</div>
|
|
||||||
|
|
||||||
: sortedThreadIds.map((threadId) => {
|
|
||||||
if (!allThreads) {
|
|
||||||
return <li key="error" className="text-void-warning">{`Error accessing chat history.`}</li>;
|
|
||||||
}
|
|
||||||
const pastThread = allThreads[threadId];
|
|
||||||
if (!pastThread) {
|
|
||||||
return <li key="error" className="text-void-warning">{`Error accessing chat history.`}</li>;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
let firstMsg = null;
|
|
||||||
// let secondMsg = null;
|
|
||||||
|
|
||||||
const firstUserMsgIdx = pastThread.messages.findIndex((msg) => msg.role === 'user');
|
|
||||||
|
|
||||||
if (firstUserMsgIdx !== -1) {
|
|
||||||
// firstMsg = truncate(pastThread.messages[firstMsgIdx].displayContent ?? '');
|
|
||||||
const firsUsertMsgObj = pastThread.messages[firstUserMsgIdx]
|
|
||||||
firstMsg = firsUsertMsgObj.role === 'user' && firsUsertMsgObj.displayContent || '';
|
|
||||||
} else {
|
|
||||||
firstMsg = '""';
|
|
||||||
}
|
|
||||||
|
|
||||||
// const secondMsgIdx = pastThread.messages.findIndex(
|
|
||||||
// (msg, i) => msg.role !== 'system' && !!msg.displayContent && i > firstMsgIdx
|
|
||||||
// );
|
|
||||||
|
|
||||||
// if (secondMsgIdx !== -1) {
|
|
||||||
// secondMsg = truncate(pastThread.messages[secondMsgIdx].displayContent ?? '');
|
|
||||||
// }
|
|
||||||
|
|
||||||
const numMessages = pastThread.messages.filter((msg) => msg.role === 'assistant' || msg.role === 'user').length;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li key={pastThread.id}>
|
|
||||||
<button
|
|
||||||
type='button'
|
|
||||||
className={`
|
|
||||||
hover:bg-void-bg-1
|
|
||||||
${threadsState.currentThreadId === pastThread.id ? 'bg-void-bg-1' : ''}
|
|
||||||
rounded-sm px-2 py-1
|
|
||||||
w-full
|
|
||||||
text-left
|
|
||||||
flex items-center
|
|
||||||
`}
|
|
||||||
onClick={() => chatThreadsService.switchToThread(pastThread.id)}
|
|
||||||
onDoubleClick={() => sidebarStateService.setState({ isHistoryOpen: false })}
|
|
||||||
title={new Date(pastThread.lastModified).toLocaleString()}
|
|
||||||
>
|
|
||||||
<div className='truncate'>{`${firstMsg}`}</div>
|
|
||||||
<div>{`\u00A0(${numMessages})`}</div>
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const truncate = (s: string) => {
|
||||||
|
let len = s.length
|
||||||
|
const TRUNC_AFTER = 16
|
||||||
|
if (len >= TRUNC_AFTER)
|
||||||
|
s = s.substring(0, TRUNC_AFTER) + '...'
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const OldPastThreadsList = () => {
|
||||||
|
|
||||||
|
const accessor = useAccessor()
|
||||||
|
const chatThreadsService = accessor.get('IChatThreadService')
|
||||||
|
const sidebarStateService = accessor.get('ISidebarStateService')
|
||||||
|
|
||||||
|
const threadsState = useChatThreadsState()
|
||||||
|
const { allThreads } = threadsState
|
||||||
|
|
||||||
|
// sorted by most recent to least recent
|
||||||
|
const sortedThreadIds = Object.keys(allThreads ?? {})
|
||||||
|
.sort((threadId1, threadId2) => (allThreads[threadId1]?.lastModified ?? 0) > (allThreads[threadId2]?.lastModified ?? 0) ? -1 : 1)
|
||||||
|
.filter(threadId => (allThreads![threadId]?.messages.length ?? 0) !== 0)
|
||||||
|
|
||||||
|
|
||||||
|
return <div className="px-1">
|
||||||
|
<ul className="flex flex-col gap-y-0.5 overflow-y-auto list-disc">
|
||||||
|
|
||||||
|
{sortedThreadIds.length === 0
|
||||||
|
|
||||||
|
? <div key="nothreads" className="text-center text-void-fg-3 brightness-90 text-root">{`There are no chat threads yet.`}</div>
|
||||||
|
|
||||||
|
: sortedThreadIds.map((threadId) => {
|
||||||
|
if (!allThreads) {
|
||||||
|
return <li key="error" className="text-void-warning">{`Error accessing chat history.`}</li>;
|
||||||
|
}
|
||||||
|
const pastThread = allThreads[threadId];
|
||||||
|
if (!pastThread) {
|
||||||
|
return <li key="error" className="text-void-warning">{`Error accessing chat history.`}</li>;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let firstMsg = null;
|
||||||
|
// let secondMsg = null;
|
||||||
|
|
||||||
|
const firstUserMsgIdx = pastThread.messages.findIndex((msg) => msg.role === 'user');
|
||||||
|
|
||||||
|
if (firstUserMsgIdx !== -1) {
|
||||||
|
// firstMsg = truncate(pastThread.messages[firstMsgIdx].displayContent ?? '');
|
||||||
|
const firsUsertMsgObj = pastThread.messages[firstUserMsgIdx]
|
||||||
|
firstMsg = firsUsertMsgObj.role === 'user' && firsUsertMsgObj.displayContent || '';
|
||||||
|
} else {
|
||||||
|
firstMsg = '""';
|
||||||
|
}
|
||||||
|
|
||||||
|
// const secondMsgIdx = pastThread.messages.findIndex(
|
||||||
|
// (msg, i) => msg.role !== 'system' && !!msg.displayContent && i > firstMsgIdx
|
||||||
|
// );
|
||||||
|
|
||||||
|
// if (secondMsgIdx !== -1) {
|
||||||
|
// secondMsg = truncate(pastThread.messages[secondMsgIdx].displayContent ?? '');
|
||||||
|
// }
|
||||||
|
|
||||||
|
const numMessages = pastThread.messages.filter((msg) => msg.role === 'assistant' || msg.role === 'user').length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li key={pastThread.id}>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className={`
|
||||||
|
hover:bg-void-bg-1
|
||||||
|
${threadsState.currentThreadId === pastThread.id ? 'bg-void-bg-1' : ''}
|
||||||
|
rounded-sm px-2 py-1
|
||||||
|
w-full
|
||||||
|
text-left
|
||||||
|
flex items-center
|
||||||
|
`}
|
||||||
|
onClick={() => {
|
||||||
|
chatThreadsService.switchToThread(pastThread.id);
|
||||||
|
sidebarStateService.setState({ isHistoryOpen: false })
|
||||||
|
}}
|
||||||
|
title={new Date(pastThread.lastModified).toLocaleString()}
|
||||||
|
>
|
||||||
|
<div className='truncate'>{`${firstMsg}`}</div>
|
||||||
|
<div>{`\u00A0(${numMessages})`}</div>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const numInitialThreads = 3
|
||||||
|
|
||||||
|
export const PastThreadsList = ({ className = '' }: { className?: string }) => {
|
||||||
|
const [showAll, setShowAll] = useState(false);
|
||||||
|
|
||||||
|
const [hoveredIdx, setHoveredIdx] = useState<number | null>(null)
|
||||||
|
|
||||||
|
const threadsState = useChatThreadsState()
|
||||||
|
const { allThreads } = threadsState
|
||||||
|
|
||||||
|
if (!allThreads) {
|
||||||
|
return <div key="error" className="p-1">{`Error accessing chat history.`}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// sorted by most recent to least recent
|
||||||
|
const sortedThreadIds = Object.keys(allThreads ?? {})
|
||||||
|
.sort((threadId1, threadId2) => (allThreads[threadId1]?.lastModified ?? 0) > (allThreads[threadId2]?.lastModified ?? 0) ? -1 : 1)
|
||||||
|
.filter(threadId => (allThreads![threadId]?.messages.length ?? 0) !== 0)
|
||||||
|
|
||||||
|
// Get only first 5 threads if not showing all
|
||||||
|
const hasMoreThreads = sortedThreadIds.length > numInitialThreads;
|
||||||
|
const displayThreads = showAll ? sortedThreadIds : sortedThreadIds.slice(0, numInitialThreads);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`flex flex-col mb-2 gap-2 w-full text-nowrap text-void-fg-3 select-none relative ${className}`}>
|
||||||
|
{displayThreads.length === 0
|
||||||
|
? <></> // No chats yet... Suggestion: Tell me about my codebase Suggestion: Create a new .voidrules file in the root of my repo
|
||||||
|
: displayThreads.map((threadId, i) => {
|
||||||
|
const pastThread = allThreads[threadId];
|
||||||
|
if (!pastThread) {
|
||||||
|
return <div key={i} className="p-1">{`Error accessing chat history.`}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PastThreadElement
|
||||||
|
key={pastThread.id}
|
||||||
|
pastThread={pastThread}
|
||||||
|
idx={i}
|
||||||
|
hoveredIdx={hoveredIdx}
|
||||||
|
setHoveredIdx={setHoveredIdx}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
{hasMoreThreads && !showAll && (
|
||||||
|
<div
|
||||||
|
className="text-void-fg-3 opacity-60 hover:opacity-100 hover:brightness-115 cursor-pointer p-1 text-xs"
|
||||||
|
onClick={() => setShowAll(true)}
|
||||||
|
>
|
||||||
|
Show {sortedThreadIds.length - numInitialThreads} more...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{hasMoreThreads && showAll && (
|
||||||
|
<div
|
||||||
|
className="text-void-fg-3 opacity-60 hover:opacity-100 hover:brightness-115 cursor-pointer p-1 text-xs"
|
||||||
|
onClick={() => setShowAll(false)}
|
||||||
|
>
|
||||||
|
Show less
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Format date to display as today, yesterday, or date
|
||||||
|
const formatDate = (date: Date) => {
|
||||||
|
const now = new Date();
|
||||||
|
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||||
|
const yesterday = new Date(today);
|
||||||
|
yesterday.setDate(yesterday.getDate() - 1);
|
||||||
|
|
||||||
|
if (date >= today) {
|
||||||
|
return 'Today';
|
||||||
|
} else if (date >= yesterday) {
|
||||||
|
return 'Yesterday';
|
||||||
|
} else {
|
||||||
|
return `${date.toLocaleString('default', { month: 'short' })} ${date.getDate()}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Format time to 12-hour format
|
||||||
|
const formatTime = (date: Date) => {
|
||||||
|
return date.toLocaleString('en-US', {
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: true
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const TrashButton = ({ threadId }: { threadId: string }) => {
|
||||||
|
|
||||||
|
const accessor = useAccessor()
|
||||||
|
const chatThreadsService = accessor.get('IChatThreadService')
|
||||||
|
|
||||||
|
|
||||||
|
const [isTrashPressed, setIsTrashPressed] = useState(false)
|
||||||
|
|
||||||
|
return (isTrashPressed ?
|
||||||
|
<div className='flex flex-nowrap text-nowrap gap-1'>
|
||||||
|
<IconShell1
|
||||||
|
Icon={X}
|
||||||
|
className='size-[11px]'
|
||||||
|
onClick={() => { setIsTrashPressed(false); }}
|
||||||
|
data-tooltip-id='void-tooltip'
|
||||||
|
data-tooltip-place='top'
|
||||||
|
data-tooltip-content='Cancel'
|
||||||
|
/>
|
||||||
|
<IconShell1
|
||||||
|
Icon={Check}
|
||||||
|
className='size-[11px]'
|
||||||
|
onClick={() => { chatThreadsService.deleteThread(threadId); setIsTrashPressed(false); }}
|
||||||
|
data-tooltip-id='void-tooltip'
|
||||||
|
data-tooltip-place='top'
|
||||||
|
data-tooltip-content='Confirm'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
: <IconShell1
|
||||||
|
Icon={Trash2}
|
||||||
|
className='size-[11px]'
|
||||||
|
onClick={() => { setIsTrashPressed(true); }}
|
||||||
|
data-tooltip-id='void-tooltip'
|
||||||
|
data-tooltip-place='top'
|
||||||
|
data-tooltip-content='Delete thread?'
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const PastThreadElement = ({ pastThread, idx, hoveredIdx, setHoveredIdx }: { pastThread: ThreadType, idx: number, hoveredIdx: number | null, setHoveredIdx: (idx: number | null) => void }) => {
|
||||||
|
|
||||||
|
|
||||||
|
const accessor = useAccessor()
|
||||||
|
const chatThreadsService = accessor.get('IChatThreadService')
|
||||||
|
const sidebarStateService = accessor.get('ISidebarStateService')
|
||||||
|
|
||||||
|
let firstMsg = null;
|
||||||
|
const firstUserMsgIdx = pastThread.messages.findIndex((msg) => msg.role === 'user');
|
||||||
|
|
||||||
|
if (firstUserMsgIdx !== -1) {
|
||||||
|
const firsUsertMsgObj = pastThread.messages[firstUserMsgIdx];
|
||||||
|
firstMsg = firsUsertMsgObj.role === 'user' && firsUsertMsgObj.displayContent || '';
|
||||||
|
} else {
|
||||||
|
firstMsg = '""';
|
||||||
|
}
|
||||||
|
|
||||||
|
const numMessages = pastThread.messages.filter((msg) => msg.role === 'assistant' || msg.role === 'user').length;
|
||||||
|
|
||||||
|
const detailsHTML = <span
|
||||||
|
className='gap-1 inline-flex items-center'
|
||||||
|
// data-tooltip-id='void-tooltip'
|
||||||
|
// data-tooltip-content={`Last modified ${formatTime(new Date(pastThread.lastModified))}`}
|
||||||
|
// data-tooltip-place='top'
|
||||||
|
>
|
||||||
|
{/* <span>{numMessages}</span> */}
|
||||||
|
{formatDate(new Date(pastThread.lastModified))}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
return <div
|
||||||
|
key={pastThread.id}
|
||||||
|
className={`
|
||||||
|
py-1 px-2 rounded text-sm bg-zinc-700/5 hover:bg-zinc-700/10 dark:bg-zinc-300/5 dark:hover:bg-zinc-300/10 cursor-pointer opacity-80 hover:opacity-100
|
||||||
|
`}
|
||||||
|
onClick={() => {
|
||||||
|
chatThreadsService.switchToThread(pastThread.id);
|
||||||
|
sidebarStateService.setState({ isHistoryOpen: false });
|
||||||
|
}}
|
||||||
|
onMouseEnter={() => setHoveredIdx(idx)}
|
||||||
|
onMouseLeave={() => setHoveredIdx(null)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between gap-1">
|
||||||
|
<span className="flex items-center gap-2 min-w-0 overflow-hidden">
|
||||||
|
<span className="truncate overflow-hidden text-ellipsis">{firstMsg}</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 opacity-60">
|
||||||
|
{idx === hoveredIdx ?
|
||||||
|
<TrashButton threadId={pastThread.id} />
|
||||||
|
: detailsHTML
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
--void-bg-1-alt: var(--vscode-badge-background);
|
--void-bg-1-alt: var(--vscode-badge-background);
|
||||||
--void-bg-2: var(--vscode-sideBar-background);
|
--void-bg-2: var(--vscode-sideBar-background);
|
||||||
--void-bg-2-alt: color-mix(in srgb, var(--vscode-editor-background) 30%, var(--vscode-sideBar-background) 70%);
|
--void-bg-2-alt: color-mix(in srgb, var(--vscode-editor-background) 30%, var(--vscode-sideBar-background) 70%);
|
||||||
--void-bg-2-hover: color-mix(in srgb, var(--vscode-editor-foreground) 5%, var(--vscode-sideBar-background) 95%);
|
--void-bg-2-hover: color-mix(in srgb, var(--vscode-editor-foreground) 2%, var(--vscode-sideBar-background) 98%);
|
||||||
--void-bg-3: var(--vscode-editor-background);
|
--void-bg-3: var(--vscode-editor-background);
|
||||||
|
|
||||||
--void-fg-0: color-mix(in srgb, var(--vscode-tab-activeForeground) 90%, black 10%);
|
--void-fg-0: color-mix(in srgb, var(--vscode-tab-activeForeground) 90%, black 10%);
|
||||||
|
|
|
||||||
|
|
@ -664,7 +664,7 @@ export const VoidCustomDropdownBox = <T extends NonNullable<any>>({
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<div
|
<div
|
||||||
ref={refs.setFloating}
|
ref={refs.setFloating}
|
||||||
className="z-10 bg-void-bg-1 border-void-border-3 border rounded shadow-lg"
|
className="z-[100] bg-void-bg-1 border-void-border-3 border rounded shadow-lg"
|
||||||
style={{
|
style={{
|
||||||
position: strategy,
|
position: strategy,
|
||||||
top: y ?? 0,
|
top: y ?? 0,
|
||||||
|
|
@ -689,7 +689,7 @@ export const VoidCustomDropdownBox = <T extends NonNullable<any>>({
|
||||||
key={optionName}
|
key={optionName}
|
||||||
className={`flex items-center px-2 py-1 pr-4 cursor-pointer whitespace-nowrap
|
className={`flex items-center px-2 py-1 pr-4 cursor-pointer whitespace-nowrap
|
||||||
transition-all duration-100
|
transition-all duration-100
|
||||||
${thisOptionIsSelected ? 'bg-void-bg-2-hover' : 'bg-void-bg-2 hover:bg-void-bg-2-hover'}
|
${thisOptionIsSelected ? 'bg-void-bg-2-hover' : 'bg-void-bg-2-alt hover:bg-void-bg-2-hover'}
|
||||||
`}
|
`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onChangeOption(option);
|
onChangeOption(option);
|
||||||
|
|
@ -709,7 +709,7 @@ export const VoidCustomDropdownBox = <T extends NonNullable<any>>({
|
||||||
</svg>
|
</svg>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span className="flex justify-between w-full">
|
<span className="flex justify-between items-center w-full gap-x-1">
|
||||||
<span>{optionName}</span>
|
<span>{optionName}</span>
|
||||||
<span className='text-void-fg-4 opacity-60'>{optionDetail}</span>
|
<span className='text-void-fg-4 opacity-60'>{optionDetail}</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
|
|
@ -307,7 +307,7 @@ const TableOfModelsForProvider = ({ providerName }: { providerName: ProviderName
|
||||||
|
|
||||||
voidSettingsState.settingsOfProvider[providerName].models.forEach(m => {
|
voidSettingsState.settingsOfProvider[providerName].models.forEach(m => {
|
||||||
infoOfModelName[m.modelName] = {
|
infoOfModelName[m.modelName] = {
|
||||||
showAsDefault: m.isDefault,
|
showAsDefault: m.type === 'default',
|
||||||
isDownloaded: true
|
isDownloaded: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -367,7 +367,7 @@ const TableOfModelsForProvider = ({ providerName }: { providerName: ProviderName
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr key={modelName} className="border-b border-void-border-1 hover:bg-void-bg-3/50">
|
<tr key={`${modelName}${providerName}`} className="border-b border-void-border-1 hover:bg-void-bg-3/50">
|
||||||
<td className="py-2 px-3 relative">
|
<td className="py-2 px-3 relative">
|
||||||
{!showAsDefault && removeModelButton}
|
{!showAsDefault && removeModelButton}
|
||||||
{modelName}
|
{modelName}
|
||||||
|
|
@ -497,7 +497,7 @@ const VoidOnboardingContent = () => {
|
||||||
|
|
||||||
const providerNamesOfWantToUseOption: { [wantToUseOption in WantToUseOption]: ProviderName[] } = {
|
const providerNamesOfWantToUseOption: { [wantToUseOption in WantToUseOption]: ProviderName[] } = {
|
||||||
smart: ['anthropic', 'openAI', 'gemini', 'openRouter'],
|
smart: ['anthropic', 'openAI', 'gemini', 'openRouter'],
|
||||||
private: ['ollama', 'vLLM', 'openAICompatible'],
|
private: ['ollama', 'vLLM', 'openAICompatible', 'lmStudio'],
|
||||||
cheap: ['gemini', 'deepseek', 'openRouter', 'ollama', 'vLLM'],
|
cheap: ['gemini', 'deepseek', 'openRouter', 'ollama', 'vLLM'],
|
||||||
all: providerNames,
|
all: providerNames,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
*--------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidStatefulModelInfo, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, nonlocalProviderNames, localProviderNames, GlobalSettingName, featureNames, displayInfoOfFeatureName, isProviderNameDisabled, FeatureName, hasDownloadButtonsOnModelsProviderNames } from '../../../../common/voidSettingsTypes.js'
|
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 ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
|
||||||
import { VoidButtonBgDarken, VoidCustomDropdownBox, VoidInputBox2, VoidSimpleInputBox, VoidSwitch } from '../util/inputs.js'
|
import { VoidButtonBgDarken, VoidCustomDropdownBox, VoidInputBox2, VoidSimpleInputBox, VoidSwitch } from '../util/inputs.js'
|
||||||
import { useAccessor, useIsDark, useRefreshModelListener, useRefreshModelState, useSettingsState } from '../util/services.js'
|
import { useAccessor, useIsDark, useRefreshModelListener, useRefreshModelState, useSettingsState } from '../util/services.js'
|
||||||
|
|
@ -286,7 +286,7 @@ export const ModelDump = () => {
|
||||||
|
|
||||||
return <div className=''>
|
return <div className=''>
|
||||||
{modelDump.map((m, i) => {
|
{modelDump.map((m, i) => {
|
||||||
const { isHidden, isDefault, isAutodetected, modelName, providerName, providerEnabled } = m
|
const { isHidden, type, modelName, providerName, providerEnabled } = m
|
||||||
|
|
||||||
const isNewProviderName = (i > 0 ? modelDump[i - 1] : undefined)?.providerName !== providerName
|
const isNewProviderName = (i > 0 ? modelDump[i - 1] : undefined)?.providerName !== providerName
|
||||||
|
|
||||||
|
|
@ -318,7 +318,7 @@ export const ModelDump = () => {
|
||||||
// : (isHidden ? `'${modelName}' won't appear in dropdowns` : ``)
|
// : (isHidden ? `'${modelName}' won't appear in dropdowns` : ``)
|
||||||
// }
|
// }
|
||||||
>
|
>
|
||||||
<span className='opacity-50 truncate'>{isAutodetected ? '(detected locally)' : isDefault ? '' : '(custom model)'}</span>
|
<span className='opacity-50 truncate'>{type === 'autodetected' ? '(detected locally)' : type === 'default' ? '' : '(custom model)'}</span>
|
||||||
|
|
||||||
<VoidSwitch
|
<VoidSwitch
|
||||||
value={value}
|
value={value}
|
||||||
|
|
@ -332,7 +332,7 @@ export const ModelDump = () => {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={`w-5 flex items-center justify-center`}>
|
<div className={`w-5 flex items-center justify-center`}>
|
||||||
{isDefault ? null : <button onClick={() => { settingsStateService.deleteModel(providerName, modelName) }}><X className='size-4' /></button>}
|
{type === 'default' || type === 'autodetected' ? null : <button onClick={() => { settingsStateService.deleteModel(providerName, modelName) }}><X className='size-4' /></button>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -344,9 +344,9 @@ export const ModelDump = () => {
|
||||||
|
|
||||||
// providers
|
// providers
|
||||||
|
|
||||||
const ProviderSetting = ({ providerName, settingName }: { providerName: ProviderName, settingName: SettingName }) => {
|
const ProviderSetting = ({ providerName, settingName, subTextMd }: { providerName: ProviderName, settingName: SettingName, subTextMd: React.ReactNode }) => {
|
||||||
|
|
||||||
const { title: settingTitle, placeholder, isPasswordField, subTextMd } = displayInfoOfSettingName(providerName, settingName)
|
const { title: settingTitle, placeholder, isPasswordField } = displayInfoOfSettingName(providerName, settingName)
|
||||||
|
|
||||||
const accessor = useAccessor()
|
const accessor = useAccessor()
|
||||||
const voidSettingsService = accessor.get('IVoidSettingsService')
|
const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||||
|
|
@ -370,10 +370,9 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider
|
||||||
passwordBlur={isPasswordField}
|
passwordBlur={isPasswordField}
|
||||||
compact={true}
|
compact={true}
|
||||||
/>
|
/>
|
||||||
{subTextMd === undefined ? null : <div className='py-1 px-3 opacity-50 text-sm'>
|
{!subTextMd ? null : <div className='py-1 px-3 opacity-50 text-sm'>
|
||||||
<ChatMarkdownRender string={subTextMd} chatMessageLocation={undefined} />
|
{subTextMd}
|
||||||
</div>}
|
</div>}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
}
|
}
|
||||||
|
|
@ -456,7 +455,14 @@ export const SettingsForProvider = ({ providerName, showProviderTitle, showProvi
|
||||||
<div className='px-0'>
|
<div className='px-0'>
|
||||||
{/* settings besides models (e.g. api key) */}
|
{/* settings besides models (e.g. api key) */}
|
||||||
{settingNames.map((settingName, i) => {
|
{settingNames.map((settingName, i) => {
|
||||||
return <ProviderSetting key={settingName} providerName={providerName} settingName={settingName} />
|
|
||||||
|
return <ProviderSetting
|
||||||
|
key={settingName}
|
||||||
|
providerName={providerName}
|
||||||
|
settingName={settingName}
|
||||||
|
subTextMd={i !== settingNames.length - 1 ? null
|
||||||
|
: <ChatMarkdownRender string={subTextMdOfProviderName(providerName)} chatMessageLocation={undefined} />}
|
||||||
|
/>
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{showProviderSuggestions && needsModel ?
|
{showProviderSuggestions && needsModel ?
|
||||||
|
|
@ -1025,11 +1031,11 @@ export const Settings = () => {
|
||||||
<div className='mt-12 max-w-[600px]'>
|
<div className='mt-12 max-w-[600px]'>
|
||||||
<h2 className={`text-3xl mb-2`}>AI Instructions</h2>
|
<h2 className={`text-3xl mb-2`}>AI Instructions</h2>
|
||||||
<h4 className={`text-void-fg-3 mb-4`}>
|
<h4 className={`text-void-fg-3 mb-4`}>
|
||||||
<ChatMarkdownRender inPTag={true} string={`
|
<ChatMarkdownRender inPTag={true} string={`
|
||||||
System instructions to include with all AI requests.
|
System instructions to include with all AI requests.
|
||||||
Alternatively, place a \`.voidinstructions\` file in the root of your workspace.
|
Alternatively, place a \`.voidrules\` file in the root of your workspace.
|
||||||
`} chatMessageLocation={undefined} />
|
`} chatMessageLocation={undefined} />
|
||||||
</h4>
|
</h4>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<AIInstructionsBox />
|
<AIInstructionsBox />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
|
|
||||||
|
|
@ -202,7 +202,19 @@ registerAction2(class extends Action2 {
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
const openNewThreadAndFireFocus = (accessor: ServicesAccessor) => {
|
||||||
|
|
||||||
|
const stateService = accessor.get(ISidebarStateService)
|
||||||
|
stateService.setState({ isHistoryOpen: false, currentTab: 'chat' })
|
||||||
|
const chatThreadService = accessor.get(IChatThreadService)
|
||||||
|
chatThreadService.openNewThread()
|
||||||
|
|
||||||
|
// focus
|
||||||
|
stateService.fireFocusChat()
|
||||||
|
const window = getActiveWindow()
|
||||||
|
window.requestAnimationFrame(() => stateService.fireFocusChat())
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// New chat menu button
|
// New chat menu button
|
||||||
|
|
@ -213,6 +225,25 @@ registerAction2(class extends Action2 {
|
||||||
title: 'New Chat',
|
title: 'New Chat',
|
||||||
icon: { id: 'add' },
|
icon: { id: 'add' },
|
||||||
menu: [{ id: MenuId.ViewTitle, group: 'navigation', when: ContextKeyExpr.equals('view', VOID_VIEW_ID), }],
|
menu: [{ id: MenuId.ViewTitle, group: 'navigation', when: ContextKeyExpr.equals('view', VOID_VIEW_ID), }],
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async run(accessor: ServicesAccessor): Promise<void> {
|
||||||
|
|
||||||
|
const metricsService = accessor.get(IMetricsService)
|
||||||
|
metricsService.capture('Chat Navigation', { type: 'New Chat' })
|
||||||
|
|
||||||
|
openNewThreadAndFireFocus(accessor)
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// New chat keybind
|
||||||
|
registerAction2(class extends Action2 {
|
||||||
|
constructor() {
|
||||||
|
super({
|
||||||
|
id: 'void.newChatKeybindAction',
|
||||||
|
title: 'New Chat Keybind',
|
||||||
keybinding: {
|
keybinding: {
|
||||||
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyL,
|
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyL,
|
||||||
weight: KeybindingWeight.VoidExtension,
|
weight: KeybindingWeight.VoidExtension,
|
||||||
|
|
@ -220,19 +251,16 @@ registerAction2(class extends Action2 {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async run(accessor: ServicesAccessor): Promise<void> {
|
async run(accessor: ServicesAccessor): Promise<void> {
|
||||||
const stateService = accessor.get(ISidebarStateService)
|
|
||||||
const metricsService = accessor.get(IMetricsService)
|
const metricsService = accessor.get(IMetricsService)
|
||||||
|
const commandService = accessor.get(ICommandService)
|
||||||
|
metricsService.capture('Chat Navigation', { type: 'New Chat Keybind' })
|
||||||
|
|
||||||
metricsService.capture('Chat Navigation', { type: 'New Chat' })
|
openNewThreadAndFireFocus(accessor)
|
||||||
|
|
||||||
stateService.setState({ isHistoryOpen: false, currentTab: 'chat' })
|
// add user's selection to chat
|
||||||
const chatThreadService = accessor.get(IChatThreadService)
|
await commandService.executeCommand(VOID_CTRL_L_ACTION_ID)
|
||||||
chatThreadService.openNewThread()
|
|
||||||
|
|
||||||
// focus
|
|
||||||
stateService.fireFocusChat()
|
|
||||||
const window = getActiveWindow()
|
|
||||||
window.requestAnimationFrame(() => stateService.fireFocusChat())
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -247,13 +275,27 @@ registerAction2(class extends Action2 {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async run(accessor: ServicesAccessor): Promise<void> {
|
async run(accessor: ServicesAccessor): Promise<void> {
|
||||||
|
|
||||||
|
// do not do anything if there are no messages (without this it clears all of the user's selections if the button is pressed)
|
||||||
|
// TODO the history button should be disabled in this case so we can remove this logic
|
||||||
|
const thread = accessor.get(IChatThreadService).getCurrentThread()
|
||||||
|
if (thread.messages.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const stateService = accessor.get(ISidebarStateService)
|
const stateService = accessor.get(ISidebarStateService)
|
||||||
const metricsService = accessor.get(IMetricsService)
|
const metricsService = accessor.get(IMetricsService)
|
||||||
|
|
||||||
|
|
||||||
metricsService.capture('Chat Navigation', { type: 'History' })
|
metricsService.capture('Chat Navigation', { type: 'History' })
|
||||||
|
|
||||||
|
openNewThreadAndFireFocus(accessor)
|
||||||
|
|
||||||
|
// doesnt do anything right now
|
||||||
stateService.setState({ isHistoryOpen: !stateService.state.isHistoryOpen, currentTab: 'chat' })
|
stateService.setState({ isHistoryOpen: !stateService.state.isHistoryOpen, currentTab: 'chat' })
|
||||||
stateService.fireBlurChat()
|
stateService.fireBlurChat()
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import { VOID_OPEN_SIDEBAR_ACTION_ID } from './sidebarPane.js';
|
||||||
|
|
||||||
// service that manages sidebar's state
|
// service that manages sidebar's state
|
||||||
export type VoidSidebarState = {
|
export type VoidSidebarState = {
|
||||||
isHistoryOpen: boolean;
|
isHistoryOpen: boolean; // this isn't doing anything right now
|
||||||
currentTab: 'chat';
|
currentTab: 'chat';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -263,7 +263,7 @@ export class ToolsService implements IToolsService {
|
||||||
read_file: async ({ uri, startLine, endLine, pageNumber }) => {
|
read_file: async ({ uri, startLine, endLine, pageNumber }) => {
|
||||||
await voidModelService.initializeModel(uri)
|
await voidModelService.initializeModel(uri)
|
||||||
const { model } = await voidModelService.getModelSafe(uri)
|
const { model } = await voidModelService.getModelSafe(uri)
|
||||||
if (model === null) { throw new Error(`Contents were empty. There may have been an error, or the file may not exist.`) }
|
if (model === null) { throw new Error(`No contents; File does not exist.`) }
|
||||||
|
|
||||||
let contents: string
|
let contents: string
|
||||||
if (startLine === null && endLine === null) {
|
if (startLine === null && endLine === null) {
|
||||||
|
|
|
||||||
|
|
@ -153,6 +153,7 @@ export const extractCodeFromFIM = ({ text, recentlyAddedTextLen, midTag, }: { te
|
||||||
const foundMid = pm.removePrefix(`<${midTag}>`)
|
const foundMid = pm.removePrefix(`<${midTag}>`)
|
||||||
|
|
||||||
if (foundMid) {
|
if (foundMid) {
|
||||||
|
pm.removeSuffix(`\n`) // sometimes outputs \n
|
||||||
pm.removeSuffix(`</${midTag}>`)
|
pm.removeSuffix(`</${midTag}>`)
|
||||||
}
|
}
|
||||||
const s = pm.value()
|
const s = pm.value()
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,22 @@ export const defaultProviderSettings = {
|
||||||
},
|
},
|
||||||
mistral: {
|
mistral: {
|
||||||
apiKey: '',
|
apiKey: '',
|
||||||
}
|
},
|
||||||
|
lmStudio: {
|
||||||
|
endpoint: 'http://localhost:1234',
|
||||||
|
},
|
||||||
|
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: '',
|
||||||
|
},
|
||||||
|
microsoftAzure: { // microsoft Azure Foundry
|
||||||
|
project: '', // really 'resource'
|
||||||
|
apiKey: '',
|
||||||
|
azureApiVersion: '2024-05-01-preview',
|
||||||
|
},
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -84,6 +99,8 @@ export const defaultModelsOfProvider = {
|
||||||
],
|
],
|
||||||
vLLM: [ // autodetected
|
vLLM: [ // autodetected
|
||||||
],
|
],
|
||||||
|
lmStudio: [], // autodetected
|
||||||
|
|
||||||
openRouter: [ // https://openrouter.ai/models
|
openRouter: [ // https://openrouter.ai/models
|
||||||
// 'anthropic/claude-3.7-sonnet:thinking',
|
// 'anthropic/claude-3.7-sonnet:thinking',
|
||||||
'anthropic/claude-3.7-sonnet',
|
'anthropic/claude-3.7-sonnet',
|
||||||
|
|
@ -112,6 +129,11 @@ export const defaultModelsOfProvider = {
|
||||||
'ministral-8b-latest',
|
'ministral-8b-latest',
|
||||||
],
|
],
|
||||||
openAICompatible: [], // fallback
|
openAICompatible: [], // fallback
|
||||||
|
googleVertex: [],
|
||||||
|
microsoftAzure: [],
|
||||||
|
liteLLM: [],
|
||||||
|
|
||||||
|
|
||||||
} as const satisfies Record<ProviderName, string[]>
|
} as const satisfies Record<ProviderName, string[]>
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -168,7 +190,7 @@ type VoidStaticProviderInfo = { // doesn't change (not stateful)
|
||||||
|
|
||||||
|
|
||||||
const modelOptionsDefaults: VoidStaticModelInfo = {
|
const modelOptionsDefaults: VoidStaticModelInfo = {
|
||||||
contextWindow: 32_000,
|
contextWindow: 16_000,
|
||||||
maxOutputTokens: 4_096,
|
maxOutputTokens: 4_096,
|
||||||
cost: { input: 0, output: 0 },
|
cost: { input: 0, output: 0 },
|
||||||
downloadable: false,
|
downloadable: false,
|
||||||
|
|
@ -806,6 +828,25 @@ const groqSettings: VoidStaticProviderInfo = {
|
||||||
modelOptionsFallback: (modelName) => { return null }
|
modelOptionsFallback: (modelName) => { return null }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------- GOOGLE VERTEX ----------------
|
||||||
|
const googleVertexModelOptions = {
|
||||||
|
} as const satisfies Record<string, VoidStaticModelInfo>
|
||||||
|
const googleVertexSettings: VoidStaticProviderInfo = {
|
||||||
|
modelOptions: googleVertexModelOptions,
|
||||||
|
modelOptionsFallback: (modelName) => { return null }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- MICROSOFT AZURE ----------------
|
||||||
|
const microsoftAzureModelOptions = {
|
||||||
|
} as const satisfies Record<string, VoidStaticModelInfo>
|
||||||
|
const microsoftAzureSettings: VoidStaticProviderInfo = {
|
||||||
|
modelOptions: microsoftAzureModelOptions,
|
||||||
|
modelOptionsFallback: (modelName) => { return null }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------- VLLM, OLLAMA, OPENAICOMPAT (self-hosted / local) ----------------
|
||||||
const ollamaModelOptions = {
|
const ollamaModelOptions = {
|
||||||
'qwen2.5-coder:1.5b': {
|
'qwen2.5-coder:1.5b': {
|
||||||
contextWindow: 32_000,
|
contextWindow: 32_000,
|
||||||
|
|
@ -858,9 +899,6 @@ const ollamaModelOptions = {
|
||||||
export const ollamaRecommendedModels = ['qwen2.5-coder:1.5b', 'llama3.1', 'qwq', 'deepseek-r1'] as const satisfies (keyof typeof ollamaModelOptions)[]
|
export const ollamaRecommendedModels = ['qwen2.5-coder:1.5b', 'llama3.1', 'qwq', 'deepseek-r1'] as const satisfies (keyof typeof ollamaModelOptions)[]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// ---------------- VLLM, OLLAMA, OPENAICOMPAT (self-hosted / local) ----------------
|
|
||||||
|
|
||||||
const vLLMSettings: VoidStaticProviderInfo = {
|
const vLLMSettings: VoidStaticProviderInfo = {
|
||||||
// reasoning: OAICompat + response.choices[0].delta.reasoning_content // https://docs.vllm.ai/en/stable/features/reasoning_outputs.html#streaming-chat-completions
|
// reasoning: OAICompat + response.choices[0].delta.reasoning_content // https://docs.vllm.ai/en/stable/features/reasoning_outputs.html#streaming-chat-completions
|
||||||
providerReasoningIOSettings: { output: { nameOfFieldInDelta: 'reasoning_content' }, },
|
providerReasoningIOSettings: { output: { nameOfFieldInDelta: 'reasoning_content' }, },
|
||||||
|
|
@ -868,6 +906,12 @@ const vLLMSettings: VoidStaticProviderInfo = {
|
||||||
modelOptions: {}, // TODO
|
modelOptions: {}, // TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const lmStudioSettings: VoidStaticProviderInfo = {
|
||||||
|
providerReasoningIOSettings: { output: { needsManualParse: true }, },
|
||||||
|
modelOptionsFallback: (modelName) => extensiveModelFallback(modelName, { downloadable: { sizeGb: 'not-known' } }),
|
||||||
|
modelOptions: {}, // TODO
|
||||||
|
}
|
||||||
|
|
||||||
const ollamaSettings: VoidStaticProviderInfo = {
|
const ollamaSettings: VoidStaticProviderInfo = {
|
||||||
// reasoning: we need to filter out reasoning <think> tags manually
|
// reasoning: we need to filter out reasoning <think> tags manually
|
||||||
providerReasoningIOSettings: { output: { needsManualParse: true }, },
|
providerReasoningIOSettings: { output: { needsManualParse: true }, },
|
||||||
|
|
@ -881,6 +925,12 @@ const openaiCompatible: VoidStaticProviderInfo = {
|
||||||
modelOptions: {},
|
modelOptions: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const liteLLMSettings: VoidStaticProviderInfo = { // https://docs.litellm.ai/docs/reasoning_content
|
||||||
|
providerReasoningIOSettings: { output: { nameOfFieldInDelta: 'reasoning_content' } },
|
||||||
|
modelOptionsFallback: (modelName) => extensiveModelFallback(modelName, { downloadable: { sizeGb: 'not-known' } }),
|
||||||
|
modelOptions: {}, // TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ---------------- OPENROUTER ----------------
|
// ---------------- OPENROUTER ----------------
|
||||||
const openRouterModelOptions_assumingOpenAICompat = {
|
const openRouterModelOptions_assumingOpenAICompat = {
|
||||||
|
|
@ -1027,9 +1077,12 @@ const modelSettingsOfProvider: { [providerName in ProviderName]: VoidStaticProvi
|
||||||
ollama: ollamaSettings,
|
ollama: ollamaSettings,
|
||||||
openAICompatible: openaiCompatible,
|
openAICompatible: openaiCompatible,
|
||||||
mistral: mistralSettings,
|
mistral: mistralSettings,
|
||||||
// googleVertex: {},
|
|
||||||
// microsoftAzure: {},
|
liteLLM: liteLLMSettings,
|
||||||
// openHands: {},
|
lmStudio: lmStudioSettings,
|
||||||
|
|
||||||
|
googleVertex: googleVertexSettings,
|
||||||
|
microsoftAzure: microsoftAzureSettings,
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -330,6 +330,7 @@ Here's an example of a good edit suggestion:
|
||||||
${fileNameEditExample}.`)
|
${fileNameEditExample}.`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
details.push(`NEVER write the FULL PATH of a file when speaking with the user. Just write the file name ONLY.`)
|
||||||
details.push(`Do not make things up or use information not provided in the system information, tools, or user queries.`)
|
details.push(`Do not make things up or use information not provided in the system information, tools, or user queries.`)
|
||||||
details.push(`Today's date is ${new Date().toDateString()}.`)
|
details.push(`Today's date is ${new Date().toDateString()}.`)
|
||||||
|
|
||||||
|
|
@ -446,9 +447,10 @@ export const DIVIDER = `=======`
|
||||||
export const FINAL = `>>>>>>> UPDATED`
|
export const FINAL = `>>>>>>> UPDATED`
|
||||||
|
|
||||||
export const searchReplace_systemMessage = `\
|
export const searchReplace_systemMessage = `\
|
||||||
You are a coding assistant that generates SEARCH/REPLACE code blocks that will be used to edit a file.
|
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.
|
||||||
|
The diff will be labeled \`DIFF\` and the original file will be labeled \`ORIGINAL_FILE\`.
|
||||||
|
|
||||||
A SEARCH/REPLACE block describes the code before and after a change. Here is the format:
|
Format your SEARCH/REPLACE blocks as follows:
|
||||||
${tripleTick[0]}
|
${tripleTick[0]}
|
||||||
${ORIGINAL}
|
${ORIGINAL}
|
||||||
// ... original code goes here
|
// ... original code goes here
|
||||||
|
|
@ -457,23 +459,28 @@ ${DIVIDER}
|
||||||
${FINAL}
|
${FINAL}
|
||||||
${tripleTick[1]}
|
${tripleTick[1]}
|
||||||
|
|
||||||
You will be given the original file \`ORIGINAL_FILE\` and a diff to apply to the file, \`CHANGE\`.
|
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.
|
||||||
Output SEARCH/REPLACE blocks to edit the file according to the desired change. You may output multiple SEARCH/REPLACE blocks.
|
|
||||||
Be sure to output a change for every single item that changed from the original file to the given change, including comments.
|
|
||||||
|
|
||||||
Directions:
|
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.
|
||||||
1. Your OUTPUT should consist ONLY of SEARCH/REPLACE blocks. Do NOT output any text or explanations before or after this.
|
|
||||||
2. The "ORIGINAL" code in each SEARCH/REPLACE block must EXACTLY match lines in the original file. The original code must NOT includes any new whitespace, comments, or any other modifications from the original code.
|
|
||||||
3. The "ORIGINAL" code in each SEARCH/REPLACE block must include enough text to uniquely identify the change in the file, but please bias towards writing as little as possible.
|
|
||||||
4. The "ORIGINAL" code in each SEARCH/REPLACE block must be disjoint from all other blocks.
|
|
||||||
|
|
||||||
The SEARCH/REPLACE blocks you generate will be applied immediately, and so they **MUST** produce a file that the user can run IMMEDIATELY.
|
3. You are allowed to output multiple SEARCH/REPLACE blocks.
|
||||||
- Make sure you add all necessary imports.
|
|
||||||
- Make sure the "UPDATED" code is ready for production as-is, and fix any relevant lint errors.
|
|
||||||
|
|
||||||
Follow coding conventions of the user (spaces, semilcolons, comments, etc). If the user spaces or formats things a certain way, CONTINUE formatting it that way, even if you prefer otherwise.
|
4. Your output should consist ONLY of SEARCH/REPLACE blocks. Do NOT output any text or explanations before or after this.
|
||||||
|
|
||||||
|
5. The ORIGINAL code in each SEARCH/REPLACE block must EXACTLY match lines in the original file. Do not add or remove any whitespace, comments, or modifications from the original code.
|
||||||
|
|
||||||
|
6. Each ORIGINAL text must be large enough to uniquely identify the change in the file. However; bias towards writing as little as possible.
|
||||||
|
|
||||||
|
7. Each ORIGINAL text must be DISJOINT from all other ORIGINAL text.
|
||||||
|
|
||||||
## EXAMPLE 1
|
## EXAMPLE 1
|
||||||
|
DIFF
|
||||||
|
${tripleTick[0]}
|
||||||
|
// ... existing code
|
||||||
|
let x = 6.5
|
||||||
|
// ... existing code
|
||||||
|
${tripleTick[1]}
|
||||||
|
|
||||||
ORIGINAL_FILE
|
ORIGINAL_FILE
|
||||||
${tripleTick[0]}
|
${tripleTick[0]}
|
||||||
let w = 5
|
let w = 5
|
||||||
|
|
@ -482,15 +489,6 @@ let y = 7
|
||||||
let z = 8
|
let z = 8
|
||||||
${tripleTick[1]}
|
${tripleTick[1]}
|
||||||
|
|
||||||
CHANGE
|
|
||||||
Make x equal to 6.5, not 6.
|
|
||||||
${tripleTick[0]}
|
|
||||||
// ... existing code
|
|
||||||
let x = 6.5
|
|
||||||
// ... existing code
|
|
||||||
${tripleTick[1]}
|
|
||||||
|
|
||||||
|
|
||||||
## ACCEPTED OUTPUT
|
## ACCEPTED OUTPUT
|
||||||
${tripleTick[0]}
|
${tripleTick[0]}
|
||||||
${ORIGINAL}
|
${ORIGINAL}
|
||||||
|
|
@ -502,11 +500,14 @@ ${tripleTick[1]}
|
||||||
`
|
`
|
||||||
|
|
||||||
export const searchReplace_userMessage = ({ originalCode, applyStr }: { originalCode: string, applyStr: string }) => `\
|
export const searchReplace_userMessage = ({ originalCode, applyStr }: { originalCode: string, applyStr: string }) => `\
|
||||||
ORIGINAL_FILE
|
DIFF
|
||||||
${originalCode}
|
${applyStr}
|
||||||
|
|
||||||
CHANGE
|
ORIGINAL_FILE
|
||||||
${applyStr}`
|
${tripleTick[0]}
|
||||||
|
${originalCode}
|
||||||
|
${tripleTick[1]}
|
||||||
|
`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { ILLMMessageService } from './sendLLMMessageService.js';
|
||||||
import { Emitter, Event } from '../../../../base/common/event.js';
|
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||||
import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js';
|
import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js';
|
||||||
import { RefreshableProviderName, refreshableProviderNames, SettingsOfProvider } from './voidSettingsTypes.js';
|
import { RefreshableProviderName, refreshableProviderNames, SettingsOfProvider } from './voidSettingsTypes.js';
|
||||||
import { OllamaModelResponse, VLLMModelResponse } from './sendLLMMessageTypes.js';
|
import { OllamaModelResponse, OpenaiCompatibleModelResponse } from './sendLLMMessageTypes.js';
|
||||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||||
|
|
||||||
|
|
@ -46,6 +46,7 @@ export type RefreshModelStateOfProvider = Record<RefreshableProviderName, Refres
|
||||||
const refreshBasedOn: { [k in RefreshableProviderName]: (keyof SettingsOfProvider[k])[] } = {
|
const refreshBasedOn: { [k in RefreshableProviderName]: (keyof SettingsOfProvider[k])[] } = {
|
||||||
ollama: ['_didFillInProviderSettings', 'endpoint'],
|
ollama: ['_didFillInProviderSettings', 'endpoint'],
|
||||||
vLLM: ['_didFillInProviderSettings', 'endpoint'],
|
vLLM: ['_didFillInProviderSettings', 'endpoint'],
|
||||||
|
lmStudio: ['_didFillInProviderSettings', 'endpoint'],
|
||||||
// openAICompatible: ['_didFillInProviderSettings', 'endpoint', 'apiKey'],
|
// openAICompatible: ['_didFillInProviderSettings', 'endpoint', 'apiKey'],
|
||||||
}
|
}
|
||||||
const REFRESH_INTERVAL = 5_000
|
const REFRESH_INTERVAL = 5_000
|
||||||
|
|
@ -142,6 +143,7 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
|
||||||
state: RefreshModelStateOfProvider = {
|
state: RefreshModelStateOfProvider = {
|
||||||
ollama: { state: 'init', timeoutId: null },
|
ollama: { state: 'init', timeoutId: null },
|
||||||
vLLM: { state: 'init', timeoutId: null },
|
vLLM: { state: 'init', timeoutId: null },
|
||||||
|
lmStudio: { state: 'init', timeoutId: null },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -160,18 +162,18 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const listFn = providerName === 'ollama' ? this.llmMessageService.ollamaList
|
const listFn = providerName === 'ollama' ? this.llmMessageService.ollamaList
|
||||||
: providerName === 'vLLM' ? this.llmMessageService.vLLMList
|
: this.llmMessageService.openAICompatibleList
|
||||||
: () => { }
|
|
||||||
|
|
||||||
listFn({
|
listFn({
|
||||||
|
providerName,
|
||||||
onSuccess: ({ models }) => {
|
onSuccess: ({ models }) => {
|
||||||
|
|
||||||
// set the models to the detected models
|
// set the models to the detected models
|
||||||
this.voidSettingsService.setAutodetectedModels(
|
this.voidSettingsService.setAutodetectedModels(
|
||||||
providerName,
|
providerName,
|
||||||
models.map(model => {
|
models.map(model => {
|
||||||
if (providerName === 'ollama') return (model as OllamaModelResponse).name;
|
if (providerName === 'ollama') return (model as OllamaModelResponse).name;
|
||||||
else if (providerName === 'vLLM') return (model as VLLMModelResponse).id;
|
else if (providerName === 'vLLM') return (model as OpenaiCompatibleModelResponse).id;
|
||||||
|
else if (providerName === 'lmStudio') return (model as OpenaiCompatibleModelResponse).id;
|
||||||
else throw new Error('refreshMode fn: unknown provider', providerName);
|
else throw new Error('refreshMode fn: unknown provider', providerName);
|
||||||
}),
|
}),
|
||||||
{ enableProviderOnSuccess: options.enableProviderOnSuccess, hideRefresh: options.doNotFire }
|
{ enableProviderOnSuccess: options.enableProviderOnSuccess, hideRefresh: options.doNotFire }
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||||
*--------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, ServiceSendLLMMessageParams, MainSendLLMMessageParams, MainLLMMessageAbortParams, ServiceModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, MainModelListParams, OllamaModelResponse, VLLMModelResponse, } from './sendLLMMessageTypes.js';
|
import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, ServiceSendLLMMessageParams, MainSendLLMMessageParams, MainLLMMessageAbortParams, ServiceModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, MainModelListParams, OllamaModelResponse, OpenaiCompatibleModelResponse, } from './sendLLMMessageTypes.js';
|
||||||
|
|
||||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||||
|
|
@ -22,7 +22,7 @@ export interface ILLMMessageService {
|
||||||
sendLLMMessage: (params: ServiceSendLLMMessageParams) => string | null;
|
sendLLMMessage: (params: ServiceSendLLMMessageParams) => string | null;
|
||||||
abort: (requestId: string) => void;
|
abort: (requestId: string) => void;
|
||||||
ollamaList: (params: ServiceModelListParams<OllamaModelResponse>) => void;
|
ollamaList: (params: ServiceModelListParams<OllamaModelResponse>) => void;
|
||||||
vLLMList: (params: ServiceModelListParams<VLLMModelResponse>) => void;
|
openAICompatibleList: (params: ServiceModelListParams<OpenaiCompatibleModelResponse>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -46,12 +46,12 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
|
||||||
success: {} as { [eventId: string]: ((params: EventModelListOnSuccessParams<OllamaModelResponse>) => void) },
|
success: {} as { [eventId: string]: ((params: EventModelListOnSuccessParams<OllamaModelResponse>) => void) },
|
||||||
error: {} as { [eventId: string]: ((params: EventModelListOnErrorParams<OllamaModelResponse>) => void) },
|
error: {} as { [eventId: string]: ((params: EventModelListOnErrorParams<OllamaModelResponse>) => void) },
|
||||||
},
|
},
|
||||||
vLLM: {
|
openAICompat: {
|
||||||
success: {} as { [eventId: string]: ((params: EventModelListOnSuccessParams<VLLMModelResponse>) => void) },
|
success: {} as { [eventId: string]: ((params: EventModelListOnSuccessParams<OpenaiCompatibleModelResponse>) => void) },
|
||||||
error: {} as { [eventId: string]: ((params: EventModelListOnErrorParams<VLLMModelResponse>) => void) },
|
error: {} as { [eventId: string]: ((params: EventModelListOnErrorParams<OpenaiCompatibleModelResponse>) => void) },
|
||||||
}
|
}
|
||||||
} satisfies {
|
} satisfies {
|
||||||
[providerName: string]: {
|
[providerName in 'ollama' | 'openAICompat']: {
|
||||||
success: { [eventId: string]: ((params: EventModelListOnSuccessParams<any>) => void) },
|
success: { [eventId: string]: ((params: EventModelListOnSuccessParams<any>) => void) },
|
||||||
error: { [eventId: string]: ((params: EventModelListOnErrorParams<any>) => void) },
|
error: { [eventId: string]: ((params: EventModelListOnErrorParams<any>) => void) },
|
||||||
}
|
}
|
||||||
|
|
@ -70,14 +70,31 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
|
||||||
|
|
||||||
// .listen sets up an IPC channel and takes a few ms, so we set up listeners immediately and add hooks to them instead
|
// .listen sets up an IPC channel and takes a few ms, so we set up listeners immediately and add hooks to them instead
|
||||||
// llm
|
// llm
|
||||||
this._register((this.channel.listen('onText_sendLLMMessage') satisfies Event<EventLLMMessageOnTextParams>)(e => { this.llmMessageHooks.onText[e.requestId]?.(e) }))
|
this._register((this.channel.listen('onText_sendLLMMessage') satisfies Event<EventLLMMessageOnTextParams>)(e => {
|
||||||
this._register((this.channel.listen('onFinalMessage_sendLLMMessage') satisfies Event<EventLLMMessageOnFinalMessageParams>)(e => { this.llmMessageHooks.onFinalMessage[e.requestId]?.(e); this._clearChannelHooks(e.requestId) }))
|
this.llmMessageHooks.onText[e.requestId]?.(e)
|
||||||
this._register((this.channel.listen('onError_sendLLMMessage') satisfies Event<EventLLMMessageOnErrorParams>)(e => { this.llmMessageHooks.onError[e.requestId]?.(e); this._clearChannelHooks(e.requestId); console.error('Error in LLMMessageService:', JSON.stringify(e)) }))
|
}))
|
||||||
// ollama .list()
|
this._register((this.channel.listen('onFinalMessage_sendLLMMessage') satisfies Event<EventLLMMessageOnFinalMessageParams>)(e => {
|
||||||
this._register((this.channel.listen('onSuccess_list_ollama') satisfies Event<EventModelListOnSuccessParams<OllamaModelResponse>>)(e => { this.listHooks.ollama.success[e.requestId]?.(e) }))
|
this.llmMessageHooks.onFinalMessage[e.requestId]?.(e);
|
||||||
this._register((this.channel.listen('onError_list_ollama') satisfies Event<EventModelListOnErrorParams<OllamaModelResponse>>)(e => { this.listHooks.ollama.error[e.requestId]?.(e) }))
|
this._clearChannelHooks(e.requestId)
|
||||||
this._register((this.channel.listen('onSuccess_list_vLLM') satisfies Event<EventModelListOnSuccessParams<VLLMModelResponse>>)(e => { this.listHooks.vLLM.success[e.requestId]?.(e) }))
|
}))
|
||||||
this._register((this.channel.listen('onError_list_vLLM') satisfies Event<EventModelListOnErrorParams<VLLMModelResponse>>)(e => { this.listHooks.vLLM.error[e.requestId]?.(e) }))
|
this._register((this.channel.listen('onError_sendLLMMessage') satisfies Event<EventLLMMessageOnErrorParams>)(e => {
|
||||||
|
this.llmMessageHooks.onError[e.requestId]?.(e);
|
||||||
|
this._clearChannelHooks(e.requestId);
|
||||||
|
console.error('Error in LLMMessageService:', JSON.stringify(e))
|
||||||
|
}))
|
||||||
|
// .list()
|
||||||
|
this._register((this.channel.listen('onSuccess_list_ollama') satisfies Event<EventModelListOnSuccessParams<OllamaModelResponse>>)(e => {
|
||||||
|
this.listHooks.ollama.success[e.requestId]?.(e)
|
||||||
|
}))
|
||||||
|
this._register((this.channel.listen('onError_list_ollama') satisfies Event<EventModelListOnErrorParams<OllamaModelResponse>>)(e => {
|
||||||
|
this.listHooks.ollama.error[e.requestId]?.(e)
|
||||||
|
}))
|
||||||
|
this._register((this.channel.listen('onSuccess_list_openAICompatible') satisfies Event<EventModelListOnSuccessParams<OpenaiCompatibleModelResponse>>)(e => {
|
||||||
|
this.listHooks.openAICompat.success[e.requestId]?.(e)
|
||||||
|
}))
|
||||||
|
this._register((this.channel.listen('onError_list_openAICompatible') satisfies Event<EventModelListOnErrorParams<OpenaiCompatibleModelResponse>>)(e => {
|
||||||
|
this.listHooks.openAICompat.error[e.requestId]?.(e)
|
||||||
|
}))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -143,25 +160,24 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
vLLMList = (params: ServiceModelListParams<VLLMModelResponse>) => {
|
openAICompatibleList = (params: ServiceModelListParams<OpenaiCompatibleModelResponse>) => {
|
||||||
const { onSuccess, onError, ...proxyParams } = params
|
const { onSuccess, onError, ...proxyParams } = params
|
||||||
|
|
||||||
const { settingsOfProvider } = this.voidSettingsService.state
|
const { settingsOfProvider } = this.voidSettingsService.state
|
||||||
|
|
||||||
// add state for request id
|
// add state for request id
|
||||||
const requestId_ = generateUuid();
|
const requestId_ = generateUuid();
|
||||||
this.listHooks.vLLM.success[requestId_] = onSuccess
|
this.listHooks.openAICompat.success[requestId_] = onSuccess
|
||||||
this.listHooks.vLLM.error[requestId_] = onError
|
this.listHooks.openAICompat.error[requestId_] = onError
|
||||||
|
|
||||||
this.channel.call('vLLMList', {
|
this.channel.call('openAICompatibleList', {
|
||||||
...proxyParams,
|
...proxyParams,
|
||||||
settingsOfProvider,
|
settingsOfProvider,
|
||||||
providerName: 'vLLM',
|
|
||||||
requestId: requestId_,
|
requestId: requestId_,
|
||||||
} satisfies MainModelListParams<VLLMModelResponse>)
|
} satisfies MainModelListParams<OpenaiCompatibleModelResponse>)
|
||||||
}
|
}
|
||||||
|
|
||||||
_clearChannelHooks(requestId: string) {
|
private _clearChannelHooks(requestId: string) {
|
||||||
delete this.llmMessageHooks.onText[requestId]
|
delete this.llmMessageHooks.onText[requestId]
|
||||||
delete this.llmMessageHooks.onFinalMessage[requestId]
|
delete this.llmMessageHooks.onFinalMessage[requestId]
|
||||||
delete this.llmMessageHooks.onError[requestId]
|
delete this.llmMessageHooks.onError[requestId]
|
||||||
|
|
@ -169,8 +185,8 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
|
||||||
delete this.listHooks.ollama.success[requestId]
|
delete this.listHooks.ollama.success[requestId]
|
||||||
delete this.listHooks.ollama.error[requestId]
|
delete this.listHooks.ollama.error[requestId]
|
||||||
|
|
||||||
delete this.listHooks.vLLM.success[requestId]
|
delete this.listHooks.openAICompat.success[requestId]
|
||||||
delete this.listHooks.vLLM.error[requestId]
|
delete this.listHooks.openAICompat.error[requestId]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
*--------------------------------------------------------------------------------------*/
|
*--------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
import { ToolName, ToolParamName } from './prompt/prompts.js'
|
import { ToolName, ToolParamName } from './prompt/prompts.js'
|
||||||
import { ChatMode, ModelSelection, ModelSelectionOptions, ProviderName, SettingsOfProvider } from './voidSettingsTypes.js'
|
import { ChatMode, ModelSelection, ModelSelectionOptions, ProviderName, RefreshableProviderName, SettingsOfProvider } from './voidSettingsTypes.js'
|
||||||
|
|
||||||
|
|
||||||
export const errorDetails = (fullError: Error | null): string | null => {
|
export const errorDetails = (fullError: Error | null): string | null => {
|
||||||
|
|
@ -162,15 +162,13 @@ export type OllamaModelResponse = {
|
||||||
size_vram: number;
|
size_vram: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
type OpenaiCompatibleModelResponse = {
|
export type OpenaiCompatibleModelResponse = {
|
||||||
id: string;
|
id: string;
|
||||||
created: number;
|
created: number;
|
||||||
object: 'model';
|
object: 'model';
|
||||||
owned_by: string;
|
owned_by: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type VLLMModelResponse = OpenaiCompatibleModelResponse
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// params to the true list fn
|
// params to the true list fn
|
||||||
|
|
@ -183,12 +181,13 @@ export type ModelListParams<ModelResponse> = {
|
||||||
|
|
||||||
// params to the service
|
// params to the service
|
||||||
export type ServiceModelListParams<modelResponse> = {
|
export type ServiceModelListParams<modelResponse> = {
|
||||||
|
providerName: RefreshableProviderName;
|
||||||
onSuccess: (param: { models: modelResponse[] }) => void;
|
onSuccess: (param: { models: modelResponse[] }) => void;
|
||||||
onError: (param: { error: any }) => void;
|
onError: (param: { error: any }) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
type BlockedMainModelListParams = 'onSuccess' | 'onError'
|
type BlockedMainModelListParams = 'onSuccess' | 'onError'
|
||||||
export type MainModelListParams<modelResponse> = Omit<ModelListParams<modelResponse>, BlockedMainModelListParams> & { requestId: string }
|
export type MainModelListParams<modelResponse> = Omit<ModelListParams<modelResponse>, BlockedMainModelListParams> & { providerName: RefreshableProviderName, requestId: string }
|
||||||
|
|
||||||
export type EventModelListOnSuccessParams<modelResponse> = Parameters<ModelListParams<modelResponse>['onSuccess']>[0] & { requestId: string }
|
export type EventModelListOnSuccessParams<modelResponse> = Parameters<ModelListParams<modelResponse>['onSuccess']>[0] & { requestId: string }
|
||||||
export type EventModelListOnErrorParams<modelResponse> = Parameters<ModelListParams<modelResponse>['onError']>[0] & { requestId: string }
|
export type EventModelListOnErrorParams<modelResponse> = Parameters<ModelListParams<modelResponse>['onError']>[0] & { requestId: string }
|
||||||
|
|
|
||||||
|
|
@ -42,10 +42,15 @@ class VoidModelService extends Disposable implements IVoidModelService {
|
||||||
}
|
}
|
||||||
|
|
||||||
initializeModel = async (uri: URI) => {
|
initializeModel = async (uri: URI) => {
|
||||||
if (uri.fsPath in this._modelRefOfURI) return;
|
try {
|
||||||
const editorModelRef = await this._textModelService.createModelReference(uri);
|
if (uri.fsPath in this._modelRefOfURI) return;
|
||||||
// Keep a strong reference to prevent disposal
|
const editorModelRef = await this._textModelService.createModelReference(uri);
|
||||||
this._modelRefOfURI[uri.fsPath] = editorModelRef;
|
// Keep a strong reference to prevent disposal
|
||||||
|
this._modelRefOfURI[uri.fsPath] = editorModelRef;
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
console.log('InitializeModel error:', e)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
getModelFromFsPath = (fsPath: string): VoidModelType => {
|
getModelFromFsPath = (fsPath: string): VoidModelType => {
|
||||||
|
|
|
||||||
|
|
@ -71,24 +71,22 @@ export interface IVoidSettingsService {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const _updatedModelsAfterDefaultModelsChange = (defaultModelNames: string[], options: { existingModels: VoidStatefulModelInfo[], didAutoDetect: boolean }) => {
|
const _modelsWithSwappedInNewModels = (options: { existingModels: VoidStatefulModelInfo[], models: string[], type: 'autodetected' | 'default' }) => {
|
||||||
const { existingModels, didAutoDetect } = options
|
const { existingModels, models, type } = options
|
||||||
|
|
||||||
const existingModelsMap: Record<string, VoidStatefulModelInfo> = {}
|
const existingModelsMap: Record<string, VoidStatefulModelInfo> = {}
|
||||||
for (const existingModel of existingModels) {
|
for (const existingModel of existingModels) {
|
||||||
existingModelsMap[existingModel.modelName] = existingModel
|
existingModelsMap[existingModel.modelName] = existingModel
|
||||||
}
|
}
|
||||||
|
|
||||||
const newDefaultModels = defaultModelNames.map((modelName, i) => ({
|
const newDefaultModels = models.map((modelName, i) => ({ modelName, type, isHidden: !!existingModelsMap[modelName]?.isHidden, }))
|
||||||
modelName,
|
|
||||||
isDefault: true,
|
|
||||||
isAutodetected: didAutoDetect,
|
|
||||||
isHidden: !!existingModelsMap[modelName]?.isHidden,
|
|
||||||
}))
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
...newDefaultModels, // swap out all the default models for the new default models
|
...newDefaultModels, // swap out all the models of this type for the new models of this type
|
||||||
...existingModels.filter(m => !m.isDefault), // keep any non-default (custom) models
|
...existingModels.filter(m => {
|
||||||
|
const keep = m.type !== type
|
||||||
|
return keep
|
||||||
|
})
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -101,7 +99,7 @@ export const modelFilterOfFeatureName: { [featureName in FeatureName]: { filter:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const _stateWithUpdatedDefaultModels = (state: VoidSettingsState): VoidSettingsState => {
|
const _stateWithMergedDefaultModels = (state: VoidSettingsState): VoidSettingsState => {
|
||||||
let newSettingsOfProvider = state.settingsOfProvider
|
let newSettingsOfProvider = state.settingsOfProvider
|
||||||
|
|
||||||
// recompute default models
|
// recompute default models
|
||||||
|
|
@ -109,7 +107,7 @@ const _stateWithUpdatedDefaultModels = (state: VoidSettingsState): VoidSettingsS
|
||||||
const defaultModels = defaultSettingsOfProvider[providerName]?.models ?? []
|
const defaultModels = defaultSettingsOfProvider[providerName]?.models ?? []
|
||||||
const currentModels = newSettingsOfProvider[providerName]?.models ?? []
|
const currentModels = newSettingsOfProvider[providerName]?.models ?? []
|
||||||
const defaultModelNames = defaultModels.map(m => m.modelName)
|
const defaultModelNames = defaultModels.map(m => m.modelName)
|
||||||
const newModels = _updatedModelsAfterDefaultModelsChange(defaultModelNames, { existingModels: currentModels, didAutoDetect: false })
|
const newModels = _modelsWithSwappedInNewModels({ existingModels: currentModels, models: defaultModelNames, type: 'default' })
|
||||||
newSettingsOfProvider = {
|
newSettingsOfProvider = {
|
||||||
...newSettingsOfProvider,
|
...newSettingsOfProvider,
|
||||||
[providerName]: {
|
[providerName]: {
|
||||||
|
|
@ -245,24 +243,45 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// the stored data structure might be outdated, so we need to update it here
|
// the stored data structure might be outdated, so we need to update it here
|
||||||
readS = {
|
try {
|
||||||
...readS,
|
readS = {
|
||||||
settingsOfProvider: {
|
...readS,
|
||||||
...defaultSettingsOfProvider,
|
...defaultSettingsOfProvider,
|
||||||
...readS.settingsOfProvider,
|
...readS.settingsOfProvider,
|
||||||
mistral: { // we added mistral
|
}
|
||||||
...defaultSettingsOfProvider.mistral,
|
|
||||||
...readS.settingsOfProvider.mistral,
|
for (const providerName of providerNames) {
|
||||||
},
|
readS.settingsOfProvider[providerName] = {
|
||||||
} // we added mistral
|
...defaultSettingsOfProvider[providerName],
|
||||||
|
...readS.settingsOfProvider[providerName],
|
||||||
|
} as any
|
||||||
|
|
||||||
|
// conversion from 1.0.3 to 1.2.5 (can remove this when enough people update)
|
||||||
|
for (const m of readS.settingsOfProvider[providerName].models) {
|
||||||
|
if (!m.type) {
|
||||||
|
const old = (m as { isAutodetected?: boolean; isDefault?: boolean })
|
||||||
|
if (old.isAutodetected)
|
||||||
|
m.type = 'autodetected'
|
||||||
|
else if (old.isDefault)
|
||||||
|
m.type = 'default'
|
||||||
|
else m.type = 'custom'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
catch (e) {
|
||||||
|
readS = defaultState()
|
||||||
|
}
|
||||||
|
|
||||||
this.state = readS
|
this.state = readS
|
||||||
this.state = _stateWithUpdatedDefaultModels(this.state)
|
this.state = _stateWithMergedDefaultModels(this.state)
|
||||||
this.state = _validatedModelState(this.state);
|
this.state = _validatedModelState(this.state);
|
||||||
|
|
||||||
|
|
||||||
this._resolver();
|
this._resolver();
|
||||||
this._onDidChangeState.fire();
|
this._onDidChangeState.fire();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -389,7 +408,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||||
const { models } = this.state.settingsOfProvider[providerName]
|
const { models } = this.state.settingsOfProvider[providerName]
|
||||||
const oldModelNames = models.map(m => m.modelName)
|
const oldModelNames = models.map(m => m.modelName)
|
||||||
|
|
||||||
const newModels = _updatedModelsAfterDefaultModelsChange(autodetectedModelNames, { existingModels: models, didAutoDetect: true })
|
const newModels = _modelsWithSwappedInNewModels({ existingModels: models, models: autodetectedModelNames, type: 'autodetected' })
|
||||||
this.setSettingOfProvider(providerName, 'models', newModels)
|
this.setSettingOfProvider(providerName, 'models', newModels)
|
||||||
|
|
||||||
// if the models changed, log it
|
// if the models changed, log it
|
||||||
|
|
@ -423,7 +442,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||||
if (existingIdx !== -1) return // if exists, do nothing
|
if (existingIdx !== -1) return // if exists, do nothing
|
||||||
const newModels = [
|
const newModels = [
|
||||||
...models,
|
...models,
|
||||||
{ modelName, isDefault: false, isHidden: false }
|
{ modelName, type: 'custom', isHidden: false } as const
|
||||||
]
|
]
|
||||||
this.setSettingOfProvider(providerName, 'models', newModels)
|
this.setSettingOfProvider(providerName, 'models', newModels)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ type UnionOfKeys<T> = T extends T ? keyof T : never;
|
||||||
export type ProviderName = keyof typeof defaultProviderSettings
|
export type ProviderName = keyof typeof defaultProviderSettings
|
||||||
export const providerNames = Object.keys(defaultProviderSettings) as ProviderName[]
|
export const providerNames = Object.keys(defaultProviderSettings) as ProviderName[]
|
||||||
|
|
||||||
export const localProviderNames = ['ollama', 'vLLM'] satisfies ProviderName[] // all local names
|
export const localProviderNames = ['ollama', 'vLLM', 'lmStudio'] satisfies ProviderName[] // all local names
|
||||||
export const nonlocalProviderNames = providerNames.filter((name) => !(localProviderNames as string[]).includes(name)) // all non-local names
|
export const nonlocalProviderNames = providerNames.filter((name) => !(localProviderNames as string[]).includes(name)) // all non-local names
|
||||||
|
|
||||||
type CustomSettingName = UnionOfKeys<typeof defaultProviderSettings[ProviderName]>
|
type CustomSettingName = UnionOfKeys<typeof defaultProviderSettings[ProviderName]>
|
||||||
|
|
@ -30,9 +30,8 @@ export const customSettingNamesOfProvider = (providerName: ProviderName) => {
|
||||||
|
|
||||||
export type VoidStatefulModelInfo = { // <-- STATEFUL
|
export type VoidStatefulModelInfo = { // <-- STATEFUL
|
||||||
modelName: string,
|
modelName: string,
|
||||||
isDefault: boolean, // whether or not it's a default for its provider
|
type: 'default' | 'autodetected' | 'custom';
|
||||||
isHidden: boolean, // whether or not the user is hiding it (switched off)
|
isHidden: boolean, // whether or not the user is hiding it (switched off)
|
||||||
isAutodetected?: boolean, // whether the model was autodetected by polling
|
|
||||||
} // TODO!!! eventually we'd want to let the user change supportsFIM, etc on the model themselves
|
} // TODO!!! eventually we'd want to let the user change supportsFIM, etc on the model themselves
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -59,74 +58,78 @@ type DisplayInfoForProviderName = {
|
||||||
|
|
||||||
export const displayInfoOfProviderName = (providerName: ProviderName): DisplayInfoForProviderName => {
|
export const displayInfoOfProviderName = (providerName: ProviderName): DisplayInfoForProviderName => {
|
||||||
if (providerName === 'anthropic') {
|
if (providerName === 'anthropic') {
|
||||||
return {
|
return { title: 'Anthropic', }
|
||||||
title: 'Anthropic',
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if (providerName === 'openAI') {
|
else if (providerName === 'openAI') {
|
||||||
return {
|
return { title: 'OpenAI', }
|
||||||
title: 'OpenAI',
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if (providerName === 'deepseek') {
|
else if (providerName === 'deepseek') {
|
||||||
return {
|
return { title: 'DeepSeek', }
|
||||||
// title: 'DeepSeek.com API',
|
|
||||||
title: 'DeepSeek',
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if (providerName === 'openRouter') {
|
else if (providerName === 'openRouter') {
|
||||||
return {
|
return { title: 'OpenRouter', }
|
||||||
title: 'OpenRouter',
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if (providerName === 'ollama') {
|
else if (providerName === 'ollama') {
|
||||||
return {
|
return { title: 'Ollama', }
|
||||||
title: 'Ollama',
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if (providerName === 'vLLM') {
|
else if (providerName === 'vLLM') {
|
||||||
return {
|
return { title: 'vLLM', }
|
||||||
title: 'vLLM',
|
}
|
||||||
}
|
else if (providerName === 'liteLLM') {
|
||||||
|
return { title: 'LiteLLM', }
|
||||||
|
}
|
||||||
|
else if (providerName === 'lmStudio') {
|
||||||
|
return { title: 'LM Studio', }
|
||||||
}
|
}
|
||||||
else if (providerName === 'openAICompatible') {
|
else if (providerName === 'openAICompatible') {
|
||||||
return {
|
return { title: 'OpenAI-Compatible', }
|
||||||
title: 'OpenAI-Compatible',
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if (providerName === 'gemini') {
|
else if (providerName === 'gemini') {
|
||||||
return {
|
return { title: 'Gemini', }
|
||||||
// title: 'Gemini API',
|
|
||||||
title: 'Gemini',
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if (providerName === 'groq') {
|
else if (providerName === 'groq') {
|
||||||
return {
|
return { title: 'Groq', }
|
||||||
// title: 'Groq.com API',
|
|
||||||
title: 'Groq',
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if (providerName === 'xAI') {
|
else if (providerName === 'xAI') {
|
||||||
return {
|
return { title: 'xAI', }
|
||||||
// title: 'Grok (xAI)',
|
|
||||||
title: 'xAI',
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if (providerName === 'mistral') {
|
else if (providerName === 'mistral') {
|
||||||
return {
|
return { title: 'Mistral', }
|
||||||
// title: 'Mistral API',
|
}
|
||||||
title: 'Mistral',
|
else if (providerName === 'googleVertex') {
|
||||||
}
|
return { title: 'Google Vertex AI', }
|
||||||
|
}
|
||||||
|
else if (providerName === 'microsoftAzure') {
|
||||||
|
return { title: 'Microsoft Azure OpenAI', }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
throw new Error(`descOfProviderName: Unknown provider name: "${providerName}"`)
|
throw new Error(`descOfProviderName: Unknown provider name: "${providerName}"`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const subTextMdOfProviderName = (providerName: ProviderName): string => {
|
||||||
|
|
||||||
|
if (providerName === 'anthropic') return 'Get your [API Key here](https://console.anthropic.com/settings/keys).'
|
||||||
|
if (providerName === 'openAI') return 'Get your [API Key here](https://platform.openai.com/api-keys).'
|
||||||
|
if (providerName === 'deepseek') return 'Get your [API Key here](https://platform.deepseek.com/api_keys).'
|
||||||
|
if (providerName === 'openRouter') return 'Get your [API Key here](https://openrouter.ai/settings/keys).'
|
||||||
|
if (providerName === 'gemini') return 'Get your [API Key here](https://aistudio.google.com/apikey).'
|
||||||
|
if (providerName === 'groq') return 'Get your [API Key here](https://console.groq.com/keys).'
|
||||||
|
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 === '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).'
|
||||||
|
if (providerName === 'lmStudio') return 'If you would like to change this endpoint, please more about [Endpoints here](https://lmstudio.ai/docs/app/api/endpoints/openai).'
|
||||||
|
if (providerName === 'liteLLM') return 'Read more about endpoints [here](https://docs.litellm.ai/docs/providers/openai_compatible).'
|
||||||
|
|
||||||
|
throw new Error(`subTextMdOfProviderName: Unknown provider name: "${providerName}"`)
|
||||||
|
}
|
||||||
|
|
||||||
type DisplayInfo = {
|
type DisplayInfo = {
|
||||||
title: string;
|
title: string;
|
||||||
placeholder: string;
|
placeholder: string;
|
||||||
subTextMd?: string;
|
|
||||||
isPasswordField?: boolean;
|
isPasswordField?: boolean;
|
||||||
}
|
}
|
||||||
export const displayInfoOfSettingName = (providerName: ProviderName, settingName: SettingName): DisplayInfo => {
|
export const displayInfoOfSettingName = (providerName: ProviderName, settingName: SettingName): DisplayInfo => {
|
||||||
|
|
@ -140,23 +143,15 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
|
||||||
providerName === 'openAI' ? 'sk-proj-key...' :
|
providerName === 'openAI' ? 'sk-proj-key...' :
|
||||||
providerName === 'deepseek' ? 'sk-key...' :
|
providerName === 'deepseek' ? 'sk-key...' :
|
||||||
providerName === 'openRouter' ? 'sk-or-key...' : // sk-or-v1-key
|
providerName === 'openRouter' ? 'sk-or-key...' : // sk-or-v1-key
|
||||||
providerName === 'gemini' ? 'key...' :
|
providerName === 'gemini' ? 'AIzaSy...' :
|
||||||
providerName === 'groq' ? 'gsk_key...' :
|
providerName === 'groq' ? 'gsk_key...' :
|
||||||
providerName === 'openAICompatible' ? 'sk-key...' :
|
providerName === 'openAICompatible' ? 'sk-key...' :
|
||||||
providerName === 'xAI' ? 'xai-key...' :
|
providerName === 'xAI' ? 'xai-key...' :
|
||||||
providerName === 'mistral' ? 'api-key...' :
|
providerName === 'mistral' ? 'api-key...' :
|
||||||
'',
|
providerName === 'googleVertex' ? 'AIzaSy...' :
|
||||||
|
providerName === 'microsoftAzure' ? 'key-...' :
|
||||||
|
'',
|
||||||
|
|
||||||
subTextMd: providerName === 'anthropic' ? 'Get your [API Key here](https://console.anthropic.com/settings/keys).' :
|
|
||||||
providerName === 'openAI' ? 'Get your [API Key here](https://platform.openai.com/api-keys).' :
|
|
||||||
providerName === 'deepseek' ? 'Get your [API Key here](https://platform.deepseek.com/api_keys).' :
|
|
||||||
providerName === 'openRouter' ? 'Get your [API Key here](https://openrouter.ai/settings/keys).' :
|
|
||||||
providerName === 'gemini' ? 'Get your [API Key here](https://aistudio.google.com/apikey).' :
|
|
||||||
providerName === 'groq' ? 'Get your [API Key here](https://console.groq.com/keys).' :
|
|
||||||
providerName === 'xAI' ? 'Get your [API Key here](https://console.x.ai).' :
|
|
||||||
providerName === 'mistral' ? 'Get your [API Key here](https://console.mistral.ai/api-keys).' :
|
|
||||||
providerName === 'openAICompatible' ? `Use any OpenAI-compatible endpoint (LM Studio, LiteLM, etc).` :
|
|
||||||
'',
|
|
||||||
isPasswordField: true,
|
isPasswordField: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -164,19 +159,51 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
|
||||||
return {
|
return {
|
||||||
title: providerName === 'ollama' ? 'Endpoint' :
|
title: providerName === 'ollama' ? 'Endpoint' :
|
||||||
providerName === 'vLLM' ? 'Endpoint' :
|
providerName === 'vLLM' ? 'Endpoint' :
|
||||||
providerName === 'openAICompatible' ? 'baseURL' : // (do not include /chat/completions)
|
providerName === 'lmStudio' ? 'Endpoint' :
|
||||||
'(never)',
|
providerName === 'openAICompatible' ? 'baseURL' : // (do not include /chat/completions)
|
||||||
|
providerName === 'googleVertex' ? 'baseURL' :
|
||||||
|
providerName === 'microsoftAzure' ? 'baseURL' :
|
||||||
|
providerName === 'liteLLM' ? 'baseURL' :
|
||||||
|
'(never)',
|
||||||
|
|
||||||
placeholder: providerName === 'ollama' ? defaultProviderSettings.ollama.endpoint
|
placeholder: providerName === 'ollama' ? defaultProviderSettings.ollama.endpoint
|
||||||
: providerName === 'vLLM' ? defaultProviderSettings.vLLM.endpoint
|
: providerName === 'vLLM' ? defaultProviderSettings.vLLM.endpoint
|
||||||
: providerName === 'openAICompatible' ? 'https://my-website.com/v1'
|
: providerName === 'openAICompatible' ? 'https://my-website.com/v1'
|
||||||
: '(never)',
|
: providerName === 'lmStudio' ? defaultProviderSettings.lmStudio.endpoint
|
||||||
|
: providerName === 'liteLLM' ? 'http://localhost:4000'
|
||||||
|
: '(never)',
|
||||||
|
|
||||||
|
|
||||||
subTextMd: providerName === 'ollama' ? '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).' :
|
|
||||||
providerName === 'vLLM' ? '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).' :
|
|
||||||
undefined,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (settingName === 'region') {
|
||||||
|
// vertex only
|
||||||
|
return {
|
||||||
|
title: 'Region',
|
||||||
|
placeholder: providerName === 'googleVertex' ? defaultProviderSettings.googleVertex.region
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (settingName === 'azureApiVersion') {
|
||||||
|
// azure only
|
||||||
|
return {
|
||||||
|
title: 'API Version',
|
||||||
|
placeholder: providerName === 'microsoftAzure' ? defaultProviderSettings.microsoftAzure.azureApiVersion
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (settingName === 'project') {
|
||||||
|
return {
|
||||||
|
title: providerName === 'googleVertex' ? 'Project'
|
||||||
|
: providerName === 'microsoftAzure' ? 'Resource'
|
||||||
|
: '',
|
||||||
|
placeholder: providerName === 'googleVertex' ? 'my-project'
|
||||||
|
: providerName === 'microsoftAzure' ? 'my-resource'
|
||||||
|
: ''
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
else if (settingName === '_didFillInProviderSettings') {
|
else if (settingName === '_didFillInProviderSettings') {
|
||||||
return {
|
return {
|
||||||
title: '(never)',
|
title: '(never)',
|
||||||
|
|
@ -200,6 +227,9 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
|
||||||
const defaultCustomSettings: Record<CustomSettingName, undefined> = {
|
const defaultCustomSettings: Record<CustomSettingName, undefined> = {
|
||||||
apiKey: undefined,
|
apiKey: undefined,
|
||||||
endpoint: undefined,
|
endpoint: undefined,
|
||||||
|
region: undefined,
|
||||||
|
project: undefined,
|
||||||
|
azureApiVersion: undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -207,8 +237,7 @@ const modelInfoOfDefaultModelNames = (defaultModelNames: string[]): { models: Vo
|
||||||
return {
|
return {
|
||||||
models: defaultModelNames.map((modelName, i) => ({
|
models: defaultModelNames.map((modelName, i) => ({
|
||||||
modelName,
|
modelName,
|
||||||
isDefault: true,
|
type: 'default',
|
||||||
isAutodetected: false,
|
|
||||||
isHidden: defaultModelNames.length >= 10, // hide all models if there are a ton of them, and make user enable them individually
|
isHidden: defaultModelNames.length >= 10, // hide all models if there are a ton of them, and make user enable them individually
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
@ -252,6 +281,18 @@ export const defaultSettingsOfProvider: SettingsOfProvider = {
|
||||||
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.mistral),
|
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.mistral),
|
||||||
_didFillInProviderSettings: undefined,
|
_didFillInProviderSettings: undefined,
|
||||||
},
|
},
|
||||||
|
liteLLM: {
|
||||||
|
...defaultCustomSettings,
|
||||||
|
...defaultProviderSettings.liteLLM,
|
||||||
|
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.liteLLM),
|
||||||
|
_didFillInProviderSettings: undefined,
|
||||||
|
},
|
||||||
|
lmStudio: {
|
||||||
|
...defaultCustomSettings,
|
||||||
|
...defaultProviderSettings.lmStudio,
|
||||||
|
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.lmStudio),
|
||||||
|
_didFillInProviderSettings: undefined,
|
||||||
|
},
|
||||||
groq: { // aggregator (serves models from multiple providers)
|
groq: { // aggregator (serves models from multiple providers)
|
||||||
...defaultCustomSettings,
|
...defaultCustomSettings,
|
||||||
...defaultProviderSettings.groq,
|
...defaultProviderSettings.groq,
|
||||||
|
|
@ -282,6 +323,18 @@ export const defaultSettingsOfProvider: SettingsOfProvider = {
|
||||||
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.vLLM),
|
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.vLLM),
|
||||||
_didFillInProviderSettings: undefined,
|
_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,
|
||||||
|
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.microsoftAzure),
|
||||||
|
_didFillInProviderSettings: undefined,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import { Ollama } from 'ollama';
|
||||||
import OpenAI, { ClientOptions } from 'openai';
|
import OpenAI, { ClientOptions } from 'openai';
|
||||||
import { MistralCore } from '@mistralai/mistralai/core.js';
|
import { MistralCore } from '@mistralai/mistralai/core.js';
|
||||||
import { fimComplete } from '@mistralai/mistralai/funcs/fimComplete.js';
|
import { fimComplete } from '@mistralai/mistralai/funcs/fimComplete.js';
|
||||||
|
import { GoogleAuth } from 'google-auth-library'
|
||||||
/* eslint-enable */
|
/* eslint-enable */
|
||||||
|
|
||||||
import { AnthropicLLMChatMessage, LLMChatMessage, LLMFIMMessage, ModelListParams, OllamaModelResponse, OnError, OnFinalMessage, OnText, RawToolCallObj, RawToolParamsObj } from '../../common/sendLLMMessageTypes.js';
|
import { AnthropicLLMChatMessage, LLMChatMessage, LLMFIMMessage, ModelListParams, OllamaModelResponse, OnError, OnFinalMessage, OnText, RawToolCallObj, RawToolParamsObj } from '../../common/sendLLMMessageTypes.js';
|
||||||
|
|
@ -19,6 +20,8 @@ import { extractReasoningWrapper, extractXMLToolsWrapper } from './extractGramma
|
||||||
import { availableTools, InternalToolInfo, isAToolName, ToolParamName, voidTools } from '../../common/prompt/prompts.js';
|
import { availableTools, InternalToolInfo, isAToolName, ToolParamName, voidTools } from '../../common/prompt/prompts.js';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
type InternalCommonMessageParams = {
|
type InternalCommonMessageParams = {
|
||||||
onText: OnText;
|
onText: OnText;
|
||||||
onFinalMessage: OnFinalMessage;
|
onFinalMessage: OnFinalMessage;
|
||||||
|
|
@ -39,7 +42,16 @@ const invalidApiKeyMessage = (providerName: ProviderName) => `Invalid ${displayI
|
||||||
|
|
||||||
// ------------ OPENAI-COMPATIBLE (HELPERS) ------------
|
// ------------ OPENAI-COMPATIBLE (HELPERS) ------------
|
||||||
|
|
||||||
const newOpenAICompatibleSDK = ({ settingsOfProvider, providerName, includeInPayload }: { settingsOfProvider: SettingsOfProvider, providerName: ProviderName, includeInPayload?: { [s: string]: any } }) => {
|
const getGoogleApiKey = async () => {
|
||||||
|
// module‑level 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 } }) => {
|
||||||
const commonPayloadOpts: ClientOptions = {
|
const commonPayloadOpts: ClientOptions = {
|
||||||
dangerouslyAllowBrowser: true,
|
dangerouslyAllowBrowser: true,
|
||||||
...includeInPayload,
|
...includeInPayload,
|
||||||
|
|
@ -56,6 +68,14 @@ const newOpenAICompatibleSDK = ({ settingsOfProvider, providerName, includeInPay
|
||||||
const thisConfig = settingsOfProvider[providerName]
|
const thisConfig = settingsOfProvider[providerName]
|
||||||
return new OpenAI({ baseURL: `${thisConfig.endpoint}/v1`, apiKey: 'noop', ...commonPayloadOpts })
|
return new OpenAI({ baseURL: `${thisConfig.endpoint}/v1`, apiKey: 'noop', ...commonPayloadOpts })
|
||||||
}
|
}
|
||||||
|
else if (providerName === 'liteLLM') {
|
||||||
|
const thisConfig = settingsOfProvider[providerName]
|
||||||
|
return new OpenAI({ baseURL: `${thisConfig.endpoint}/v1`, apiKey: 'noop', ...commonPayloadOpts })
|
||||||
|
}
|
||||||
|
else if (providerName === 'lmStudio') {
|
||||||
|
const thisConfig = settingsOfProvider[providerName]
|
||||||
|
return new OpenAI({ baseURL: `${thisConfig.endpoint}/v1`, apiKey: 'noop', ...commonPayloadOpts })
|
||||||
|
}
|
||||||
else if (providerName === 'openRouter') {
|
else if (providerName === 'openRouter') {
|
||||||
const thisConfig = settingsOfProvider[providerName]
|
const thisConfig = settingsOfProvider[providerName]
|
||||||
return new OpenAI({
|
return new OpenAI({
|
||||||
|
|
@ -70,8 +90,22 @@ const newOpenAICompatibleSDK = ({ settingsOfProvider, providerName, includeInPay
|
||||||
}
|
}
|
||||||
else if (providerName === 'gemini') {
|
else if (providerName === 'gemini') {
|
||||||
const thisConfig = settingsOfProvider[providerName]
|
const thisConfig = settingsOfProvider[providerName]
|
||||||
return new OpenAI({ baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai/', apiKey: thisConfig.apiKey, ...commonPayloadOpts })
|
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 === '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]
|
||||||
|
const baseURL = `https://${thisConfig.project}.services.ai.azure.com/api/models/chat/completions??api-version=${thisConfig.azureApiVersion}`
|
||||||
|
return new OpenAI({ baseURL: baseURL, apiKey: thisConfig.apiKey, ...commonPayloadOpts })
|
||||||
|
}
|
||||||
|
|
||||||
else if (providerName === 'deepseek') {
|
else if (providerName === 'deepseek') {
|
||||||
const thisConfig = settingsOfProvider[providerName]
|
const thisConfig = settingsOfProvider[providerName]
|
||||||
return new OpenAI({ baseURL: 'https://api.deepseek.com/v1', apiKey: thisConfig.apiKey, ...commonPayloadOpts })
|
return new OpenAI({ baseURL: 'https://api.deepseek.com/v1', apiKey: thisConfig.apiKey, ...commonPayloadOpts })
|
||||||
|
|
@ -97,7 +131,7 @@ const newOpenAICompatibleSDK = ({ settingsOfProvider, providerName, includeInPay
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const _sendOpenAICompatibleFIM = ({ messages: { prefix, suffix, stopTokens }, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, }: SendFIMParams_Internal) => {
|
const _sendOpenAICompatibleFIM = async ({ messages: { prefix, suffix, stopTokens }, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, }: SendFIMParams_Internal) => {
|
||||||
const { modelName, supportsFIM } = getModelCapabilities(providerName, modelName_)
|
const { modelName, supportsFIM } = getModelCapabilities(providerName, modelName_)
|
||||||
if (!supportsFIM) {
|
if (!supportsFIM) {
|
||||||
if (modelName === modelName_)
|
if (modelName === modelName_)
|
||||||
|
|
@ -107,7 +141,7 @@ const _sendOpenAICompatibleFIM = ({ messages: { prefix, suffix, stopTokens }, on
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const openai = newOpenAICompatibleSDK({ providerName, settingsOfProvider })
|
const openai = await newOpenAICompatibleSDK({ providerName, settingsOfProvider })
|
||||||
openai.completions
|
openai.completions
|
||||||
.create({
|
.create({
|
||||||
model: modelName,
|
model: modelName,
|
||||||
|
|
@ -178,7 +212,7 @@ const openAIToolToRawToolCallObj = (name: string, toolParamsStr: string, id: str
|
||||||
// ------------ OPENAI-COMPATIBLE ------------
|
// ------------ OPENAI-COMPATIBLE ------------
|
||||||
|
|
||||||
|
|
||||||
const _sendOpenAICompatibleChat = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, providerName, chatMode, separateSystemMessage }: SendChatParams_Internal) => {
|
const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, providerName, chatMode, separateSystemMessage }: SendChatParams_Internal) => {
|
||||||
const {
|
const {
|
||||||
modelName,
|
modelName,
|
||||||
specialToolFormat,
|
specialToolFormat,
|
||||||
|
|
@ -199,7 +233,7 @@ const _sendOpenAICompatibleChat = ({ messages, onText, onFinalMessage, onError,
|
||||||
: {}
|
: {}
|
||||||
|
|
||||||
// instance
|
// instance
|
||||||
const openai: OpenAI = newOpenAICompatibleSDK({ providerName, settingsOfProvider, includeInPayload })
|
const openai: OpenAI = await newOpenAICompatibleSDK({ providerName, settingsOfProvider, includeInPayload })
|
||||||
const options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = {
|
const options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = {
|
||||||
model: modelName,
|
model: modelName,
|
||||||
messages: messages as any,
|
messages: messages as any,
|
||||||
|
|
@ -300,7 +334,7 @@ const _openaiCompatibleList = async ({ onSuccess: onSuccess_, onError: onError_,
|
||||||
onError_({ error })
|
onError_({ error })
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const openai = newOpenAICompatibleSDK({ providerName, settingsOfProvider })
|
const openai = await newOpenAICompatibleSDK({ providerName, settingsOfProvider })
|
||||||
openai.models.list()
|
openai.models.list()
|
||||||
.then(async (response) => {
|
.then(async (response) => {
|
||||||
const models: OpenAIModel[] = []
|
const models: OpenAIModel[] = []
|
||||||
|
|
@ -360,7 +394,7 @@ const anthropicToolToRawToolCallObj = (toolBlock: Anthropic.Messages.ToolUseBloc
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------ ANTHROPIC ------------
|
// ------------ ANTHROPIC ------------
|
||||||
const sendAnthropicChat = ({ messages, providerName, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, separateSystemMessage, chatMode }: SendChatParams_Internal) => {
|
const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, separateSystemMessage, chatMode }: SendChatParams_Internal) => {
|
||||||
const {
|
const {
|
||||||
modelName,
|
modelName,
|
||||||
specialToolFormat,
|
specialToolFormat,
|
||||||
|
|
@ -505,6 +539,7 @@ const sendMistralFIM = ({ messages, onFinalMessage, onError, settingsOfProvider,
|
||||||
stop: messages.stopTokens,
|
stop: messages.stopTokens,
|
||||||
})
|
})
|
||||||
.then(async response => {
|
.then(async response => {
|
||||||
|
// unfortunately, _setAborter() does not exist
|
||||||
let content = response?.ok ? response.value.choices?.[0]?.message?.content ?? '' : '';
|
let content = response?.ok ? response.value.choices?.[0]?.message?.content ?? '' : '';
|
||||||
const fullText = typeof content === 'string' ? content
|
const fullText = typeof content === 'string' ? content
|
||||||
: content.map(chunk => (chunk.type === 'text' ? chunk.text : '')).join('')
|
: content.map(chunk => (chunk.type === 'text' ? chunk.text : '')).join('')
|
||||||
|
|
@ -584,7 +619,7 @@ const sendOllamaFIM = ({ messages, onFinalMessage, onError, settingsOfProvider,
|
||||||
|
|
||||||
type CallFnOfProvider = {
|
type CallFnOfProvider = {
|
||||||
[providerName in ProviderName]: {
|
[providerName in ProviderName]: {
|
||||||
sendChat: (params: SendChatParams_Internal) => void;
|
sendChat: (params: SendChatParams_Internal) => Promise<void>;
|
||||||
sendFIM: ((params: SendFIMParams_Internal) => void) | null;
|
sendFIM: ((params: SendFIMParams_Internal) => void) | null;
|
||||||
list: ((params: ListParams_Internal<any>) => void) | null;
|
list: ((params: ListParams_Internal<any>) => void) | null;
|
||||||
}
|
}
|
||||||
|
|
@ -646,6 +681,27 @@ export const sendLLMMessageToProviderImplementation = {
|
||||||
sendFIM: null,
|
sendFIM: null,
|
||||||
list: null,
|
list: null,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
lmStudio: {
|
||||||
|
sendChat: (params) => _sendOpenAICompatibleChat(params),
|
||||||
|
sendFIM: null, // lmStudio has no suffix parameter in /completions
|
||||||
|
list: (params) => _openaiCompatibleList(params),
|
||||||
|
},
|
||||||
|
liteLLM: {
|
||||||
|
sendChat: (params) => _sendOpenAICompatibleChat(params),
|
||||||
|
sendFIM: null,
|
||||||
|
list: null,
|
||||||
|
},
|
||||||
|
googleVertex: {
|
||||||
|
sendChat: (params) => _sendOpenAICompatibleChat(params),
|
||||||
|
sendFIM: null,
|
||||||
|
list: null,
|
||||||
|
},
|
||||||
|
microsoftAzure: {
|
||||||
|
sendChat: (params) => _sendOpenAICompatibleChat(params),
|
||||||
|
sendFIM: null,
|
||||||
|
list: null,
|
||||||
|
},
|
||||||
} satisfies CallFnOfProvider
|
} satisfies CallFnOfProvider
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import { displayInfoOfProviderName } from '../../common/voidSettingsTypes.js';
|
||||||
import { sendLLMMessageToProviderImplementation } from './sendLLMMessage.impl.js';
|
import { sendLLMMessageToProviderImplementation } from './sendLLMMessage.impl.js';
|
||||||
|
|
||||||
|
|
||||||
export const sendLLMMessage = ({
|
export const sendLLMMessage = async ({
|
||||||
messagesType,
|
messagesType,
|
||||||
messages: messages_,
|
messages: messages_,
|
||||||
onText: onText_,
|
onText: onText_,
|
||||||
|
|
@ -108,12 +108,12 @@ export const sendLLMMessage = ({
|
||||||
}
|
}
|
||||||
const { sendFIM, sendChat } = implementation
|
const { sendFIM, sendChat } = implementation
|
||||||
if (messagesType === 'chatMessages') {
|
if (messagesType === 'chatMessages') {
|
||||||
sendChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName, _setAborter, providerName, separateSystemMessage, chatMode })
|
await sendChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName, _setAborter, providerName, separateSystemMessage, chatMode })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (messagesType === 'FIMMessage') {
|
if (messagesType === 'FIMMessage') {
|
||||||
if (sendFIM) {
|
if (sendFIM) {
|
||||||
sendFIM({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName, _setAborter, providerName, separateSystemMessage })
|
await sendFIM({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName, _setAborter, providerName, separateSystemMessage })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
onError({ message: `Error: This provider does not support Autocomplete yet.`, fullError: null })
|
onError({ message: `Error: This provider does not support Autocomplete yet.`, fullError: null })
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
import { IServerChannel } from '../../../../base/parts/ipc/common/ipc.js';
|
import { IServerChannel } from '../../../../base/parts/ipc/common/ipc.js';
|
||||||
import { Emitter, Event } from '../../../../base/common/event.js';
|
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||||
import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, MainSendLLMMessageParams, AbortRef, SendLLMMessageParams, MainLLMMessageAbortParams, ModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, OllamaModelResponse, VLLMModelResponse, MainModelListParams, } from '../common/sendLLMMessageTypes.js';
|
import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, MainSendLLMMessageParams, AbortRef, SendLLMMessageParams, MainLLMMessageAbortParams, ModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, OllamaModelResponse, OpenaiCompatibleModelResponse, MainModelListParams, } from '../common/sendLLMMessageTypes.js';
|
||||||
import { sendLLMMessage } from './llmMessage/sendLLMMessage.js'
|
import { sendLLMMessage } from './llmMessage/sendLLMMessage.js'
|
||||||
import { IMetricsService } from '../common/metricsService.js';
|
import { IMetricsService } from '../common/metricsService.js';
|
||||||
import { sendLLMMessageToProviderImplementation } from './llmMessage/sendLLMMessage.impl.js';
|
import { sendLLMMessageToProviderImplementation } from './llmMessage/sendLLMMessage.impl.js';
|
||||||
|
|
@ -25,7 +25,7 @@ export class LLMMessageChannel implements IServerChannel {
|
||||||
}
|
}
|
||||||
|
|
||||||
// aborters for above
|
// aborters for above
|
||||||
private readonly abortRefOfRequestId: Record<string, AbortRef> = {}
|
private readonly _infoOfRunningRequest: Record<string, { waitForSend: Promise<void> | undefined, abortRef: AbortRef }> = {}
|
||||||
|
|
||||||
|
|
||||||
// list
|
// list
|
||||||
|
|
@ -34,12 +34,12 @@ export class LLMMessageChannel implements IServerChannel {
|
||||||
success: new Emitter<EventModelListOnSuccessParams<OllamaModelResponse>>(),
|
success: new Emitter<EventModelListOnSuccessParams<OllamaModelResponse>>(),
|
||||||
error: new Emitter<EventModelListOnErrorParams<OllamaModelResponse>>(),
|
error: new Emitter<EventModelListOnErrorParams<OllamaModelResponse>>(),
|
||||||
},
|
},
|
||||||
vLLM: {
|
openaiCompat: {
|
||||||
success: new Emitter<EventModelListOnSuccessParams<VLLMModelResponse>>(),
|
success: new Emitter<EventModelListOnSuccessParams<OpenaiCompatibleModelResponse>>(),
|
||||||
error: new Emitter<EventModelListOnErrorParams<VLLMModelResponse>>(),
|
error: new Emitter<EventModelListOnErrorParams<OpenaiCompatibleModelResponse>>(),
|
||||||
}
|
},
|
||||||
} satisfies {
|
} satisfies {
|
||||||
[providerName: string]: {
|
[providerName in 'ollama' | 'openaiCompat']: {
|
||||||
success: Emitter<EventModelListOnSuccessParams<any>>,
|
success: Emitter<EventModelListOnSuccessParams<any>>,
|
||||||
error: Emitter<EventModelListOnErrorParams<any>>,
|
error: Emitter<EventModelListOnErrorParams<any>>,
|
||||||
}
|
}
|
||||||
|
|
@ -59,8 +59,8 @@ export class LLMMessageChannel implements IServerChannel {
|
||||||
// list
|
// list
|
||||||
else if (event === 'onSuccess_list_ollama') return this.listEmitters.ollama.success.event;
|
else if (event === 'onSuccess_list_ollama') return this.listEmitters.ollama.success.event;
|
||||||
else if (event === 'onError_list_ollama') return this.listEmitters.ollama.error.event;
|
else if (event === 'onError_list_ollama') return this.listEmitters.ollama.error.event;
|
||||||
else if (event === 'onSuccess_list_vLLM') return this.listEmitters.vLLM.success.event;
|
else if (event === 'onSuccess_list_openAICompatible') return this.listEmitters.openaiCompat.success.event;
|
||||||
else if (event === 'onError_list_vLLM') return this.listEmitters.vLLM.error.event;
|
else if (event === 'onError_list_openAICompatible') return this.listEmitters.openaiCompat.error.event;
|
||||||
|
|
||||||
else throw new Error(`Event not found: ${event}`);
|
else throw new Error(`Event not found: ${event}`);
|
||||||
}
|
}
|
||||||
|
|
@ -72,13 +72,13 @@ export class LLMMessageChannel implements IServerChannel {
|
||||||
this._callSendLLMMessage(params)
|
this._callSendLLMMessage(params)
|
||||||
}
|
}
|
||||||
else if (command === 'abort') {
|
else if (command === 'abort') {
|
||||||
this._callAbort(params)
|
await this._callAbort(params)
|
||||||
}
|
}
|
||||||
else if (command === 'ollamaList') {
|
else if (command === 'ollamaList') {
|
||||||
this._callOllamaList(params)
|
this._callOllamaList(params)
|
||||||
}
|
}
|
||||||
else if (command === 'vLLMList') {
|
else if (command === 'openAICompatibleList') {
|
||||||
this._callVLLMList(params)
|
this._callOpenAICompatibleList(params)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
throw new Error(`Void sendLLM: command "${command}" not recognized.`)
|
throw new Error(`Void sendLLM: command "${command}" not recognized.`)
|
||||||
|
|
@ -90,27 +90,37 @@ export class LLMMessageChannel implements IServerChannel {
|
||||||
}
|
}
|
||||||
|
|
||||||
// the only place sendLLMMessage is actually called
|
// the only place sendLLMMessage is actually called
|
||||||
private async _callSendLLMMessage(params: MainSendLLMMessageParams) {
|
private _callSendLLMMessage(params: MainSendLLMMessageParams) {
|
||||||
const { requestId } = params;
|
const { requestId } = params;
|
||||||
|
|
||||||
if (!(requestId in this.abortRefOfRequestId))
|
if (!(requestId in this._infoOfRunningRequest))
|
||||||
this.abortRefOfRequestId[requestId] = { current: null }
|
this._infoOfRunningRequest[requestId] = { waitForSend: undefined, abortRef: { current: null } }
|
||||||
|
|
||||||
const mainThreadParams: SendLLMMessageParams = {
|
const mainThreadParams: SendLLMMessageParams = {
|
||||||
...params,
|
...params,
|
||||||
onText: (p) => { this.llmMessageEmitters.onText.fire({ requestId, ...p }); },
|
onText: (p) => {
|
||||||
onFinalMessage: (p) => { this.llmMessageEmitters.onFinalMessage.fire({ requestId, ...p }); },
|
this.llmMessageEmitters.onText.fire({ requestId, ...p });
|
||||||
onError: (p) => { console.log('sendLLM: firing err'); this.llmMessageEmitters.onError.fire({ requestId, ...p }); },
|
},
|
||||||
abortRef: this.abortRefOfRequestId[requestId],
|
onFinalMessage: (p) => {
|
||||||
|
this.llmMessageEmitters.onFinalMessage.fire({ requestId, ...p });
|
||||||
|
},
|
||||||
|
onError: (p) => {
|
||||||
|
console.log('sendLLM: firing err');
|
||||||
|
this.llmMessageEmitters.onError.fire({ requestId, ...p });
|
||||||
|
},
|
||||||
|
abortRef: this._infoOfRunningRequest[requestId].abortRef,
|
||||||
}
|
}
|
||||||
sendLLMMessage(mainThreadParams, this.metricsService);
|
const p = sendLLMMessage(mainThreadParams, this.metricsService);
|
||||||
|
this._infoOfRunningRequest[requestId].waitForSend = p
|
||||||
}
|
}
|
||||||
|
|
||||||
private _callAbort(params: MainLLMMessageAbortParams) {
|
private async _callAbort(params: MainLLMMessageAbortParams) {
|
||||||
const { requestId } = params;
|
const { requestId } = params;
|
||||||
if (!(requestId in this.abortRefOfRequestId)) return
|
if (!(requestId in this._infoOfRunningRequest)) return
|
||||||
this.abortRefOfRequestId[requestId].current?.()
|
const { waitForSend, abortRef } = this._infoOfRunningRequest[requestId]
|
||||||
delete this.abortRefOfRequestId[requestId]
|
await waitForSend // wait for the send to finish so we know abortRef was set
|
||||||
|
abortRef?.current?.()
|
||||||
|
delete this._infoOfRunningRequest[requestId]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -128,15 +138,15 @@ export class LLMMessageChannel implements IServerChannel {
|
||||||
sendLLMMessageToProviderImplementation.ollama.list(mainThreadParams)
|
sendLLMMessageToProviderImplementation.ollama.list(mainThreadParams)
|
||||||
}
|
}
|
||||||
|
|
||||||
_callVLLMList = (params: MainModelListParams<VLLMModelResponse>) => {
|
_callOpenAICompatibleList = (params: MainModelListParams<OpenaiCompatibleModelResponse>) => {
|
||||||
const { requestId } = params
|
const { requestId, providerName } = params
|
||||||
const emitters = this.listEmitters.vLLM
|
const emitters = this.listEmitters.openaiCompat
|
||||||
const mainThreadParams: ModelListParams<VLLMModelResponse> = {
|
const mainThreadParams: ModelListParams<OpenaiCompatibleModelResponse> = {
|
||||||
...params,
|
...params,
|
||||||
onSuccess: (p) => { emitters.success.fire({ requestId, ...p }); },
|
onSuccess: (p) => { emitters.success.fire({ requestId, ...p }); },
|
||||||
onError: (p) => { emitters.error.fire({ requestId, ...p }); },
|
onError: (p) => { emitters.error.fire({ requestId, ...p }); },
|
||||||
}
|
}
|
||||||
sendLLMMessageToProviderImplementation.vLLM.list(mainThreadParams)
|
sendLLMMessageToProviderImplementation[providerName].list(mainThreadParams)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue