Merge pull request #417 from voideditor/model-selection

Misc updates
This commit is contained in:
Andrew Pareles 2025-04-18 20:58:55 -07:00 committed by GitHub
commit c775d34d75
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 1401 additions and 1891 deletions

1589
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -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",

View file

@ -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",

View file

@ -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

View file

@ -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]

View file

@ -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')
} }

View file

@ -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

View file

@ -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

View file

@ -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}

View file

@ -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
</> </>
) )
} }

View file

@ -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>
) )
} }

View file

@ -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>
}

View file

@ -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%);

View file

@ -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>

View file

@ -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,
} }

View file

@ -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>

View file

@ -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()
} }
}) })

View file

@ -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';
} }

View file

@ -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) {

View file

@ -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()

View file

@ -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

View file

@ -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]}
`

View file

@ -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 }

View file

@ -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]
} }
} }

View file

@ -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 }

View file

@ -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 => {

View file

@ -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)

View file

@ -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,
},
} }

View file

@ -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 () => {
// modulelevel singleton
const auth = new GoogleAuth({ scopes: `https://www.googleapis.com/auth/cloud-platform` });
const key = await auth.getAccessToken()
if (!key) throw new Error(`Google API failed to generate a key.`)
return key
}
const newOpenAICompatibleSDK = async ({ settingsOfProvider, providerName, includeInPayload }: { settingsOfProvider: SettingsOfProvider, providerName: ProviderName, includeInPayload?: { [s: string]: any } }) => {
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

View file

@ -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 })

View file

@ -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)
} }