mirror of
https://github.com/voideditor/void
synced 2026-05-24 01:48:25 +00:00
commit
c775d34d75
31 changed files with 1401 additions and 1891 deletions
1589
package-lock.json
generated
1589
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -79,6 +79,7 @@
|
|||
"@mistralai/mistralai": "^1.5.0",
|
||||
"@modelcontextprotocol/sdk": "^1.9.0",
|
||||
"@parcel/watcher": "2.5.1",
|
||||
"@types/katex": "^0.16.7",
|
||||
"@types/semver": "^7.5.8",
|
||||
"@vscode/deviceid": "^0.1.1",
|
||||
"@vscode/iconv-lite-umd": "0.7.0",
|
||||
|
|
@ -106,10 +107,13 @@
|
|||
"cross-spawn": "^7.0.6",
|
||||
"diff": "^7.0.0",
|
||||
"eslint-plugin-react": "^7.37.4",
|
||||
"fast-json-stable-stringify": "^2.1.0",
|
||||
"google-auth-library": "^9.15.1",
|
||||
"groq-sdk": "^0.15.0",
|
||||
"http-proxy-agent": "^7.0.0",
|
||||
"https-proxy-agent": "^7.0.2",
|
||||
"jschardet": "3.1.4",
|
||||
"katex": "^0.16.22",
|
||||
"kerberos": "2.1.1",
|
||||
"lucide-react": "^0.477.0",
|
||||
"marked": "^15.0.7",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"nameShort": "Void",
|
||||
"nameLong": "Void",
|
||||
"voidVersion": "1.2.5",
|
||||
"voidVersion": "1.2.6",
|
||||
"applicationName": "void",
|
||||
"dataFolderName": ".void-editor",
|
||||
"win32MutexName": "voideditor",
|
||||
|
|
|
|||
|
|
@ -59,6 +59,6 @@ class DummyService extends Disposable implements IWorkbenchContribution, IDummyS
|
|||
|
||||
|
||||
// pick one and delete the other:
|
||||
registerSingleton(IDummyService, DummyService, InstantiationType.Eager);
|
||||
registerSingleton(IDummyService, DummyService, InstantiationType.Eager); // lazily loaded, even if Eager
|
||||
|
||||
registerWorkbenchContribution2(DummyService.ID, DummyService, WorkbenchPhase.BlockRestore);
|
||||
registerWorkbenchContribution2(DummyService.ID, DummyService, WorkbenchPhase.BlockRestore); // mounts on start
|
||||
|
|
|
|||
|
|
@ -32,6 +32,9 @@ import { INotificationService, Severity } from '../../../../platform/notificatio
|
|||
import { truncate } from '../../../../base/common/strings.js';
|
||||
import { THREAD_STORAGE_KEY } from '../common/storageKeys.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 => {
|
||||
if (!currentSelections) return null
|
||||
|
|
@ -91,7 +94,7 @@ const defaultMessageState: UserMessageState = {
|
|||
|
||||
// a 'thread' means a chat message history
|
||||
|
||||
type ThreadType = {
|
||||
export type ThreadType = {
|
||||
id: string; // store the id here too
|
||||
createdAt: string; // ISO string
|
||||
lastModified: string; // ISO string
|
||||
|
|
@ -177,6 +180,7 @@ export interface IChatThreadService {
|
|||
|
||||
getCurrentThread(): ThreadType;
|
||||
openNewThread(): void;
|
||||
deleteThread(threadId: string): void;
|
||||
switchToThread(threadId: string): void;
|
||||
|
||||
// exposed getters/setters
|
||||
|
|
@ -564,7 +568,6 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
let nMessagesSent = 0
|
||||
let shouldSendAnotherMessage = true
|
||||
let isRunningWhenEnd: IsRunningType = undefined
|
||||
let aborted = false
|
||||
|
||||
// before enter loop, call tool
|
||||
if (callThisToolFirst) {
|
||||
|
|
@ -592,69 +595,94 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
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
|
||||
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
|
||||
let aborted = false
|
||||
|
||||
// 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 })
|
||||
let shouldRetry = true
|
||||
let nAttempts = 0
|
||||
|
||||
// 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 }
|
||||
while (shouldRetry) {
|
||||
shouldRetry = false
|
||||
|
||||
if (awaitingUserApproval) {
|
||||
isRunningWhenEnd = 'awaiting_user'
|
||||
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 ?? ''
|
||||
|
||||
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 {
|
||||
shouldSendAnotherMessage = true
|
||||
this._setStreamState(threadId, { streamingToken: llmCancelToken }, 'merge') // new stream token for the new message
|
||||
const toolCall = await messageIsDonePromise // wait for message to complete
|
||||
if (shouldRetry) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if (aborted) {
|
||||
return
|
||||
}
|
||||
this._setStreamState(threadId, { streamingToken: undefined }, 'merge') // streaming message is done
|
||||
|
||||
} // 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
|
||||
|
|
@ -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) {
|
||||
const { allThreads } = this.state
|
||||
const oldThread = allThreads[threadId]
|
||||
|
|
|
|||
|
|
@ -438,27 +438,33 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess
|
|||
super()
|
||||
}
|
||||
|
||||
// Read .voidinstructions files from workspace folders
|
||||
private _getVoidInstructionsFileContents(): string {
|
||||
const workspaceFolders = this.workspaceContextService.getWorkspace().folders;
|
||||
let voidInstructions = '';
|
||||
for (const folder of workspaceFolders) {
|
||||
const uri = URI.joinPath(folder.uri, '.voidinstructions')
|
||||
const { model } = this.voidModelService.getModel(uri)
|
||||
if (!model) continue
|
||||
voidInstructions += model.getValue() + '\n\n';
|
||||
// Read .voidrules files from workspace folders
|
||||
private _getVoidRulesFileContents(): string {
|
||||
try {
|
||||
const workspaceFolders = this.workspaceContextService.getWorkspace().folders;
|
||||
let voidRules = '';
|
||||
for (const folder of workspaceFolders) {
|
||||
const uri = URI.joinPath(folder.uri, '.voidrules')
|
||||
const { model } = this.voidModelService.getModel(uri)
|
||||
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 {
|
||||
const globalAIInstructions = this.voidSettingsService.state.globalSettings.aiInstructions;
|
||||
const voidInstructionsFileContent = this._getVoidInstructionsFileContents();
|
||||
const voidRulesFileContent = this._getVoidRulesFileContents();
|
||||
|
||||
const ans: string[] = []
|
||||
if (globalAIInstructions) ans.push(globalAIInstructions)
|
||||
if (voidInstructionsFileContent) ans.push(voidInstructionsFileContent)
|
||||
if (voidRulesFileContent) ans.push(voidRulesFileContent)
|
||||
return ans.join('\n\n')
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@ class ConvertContribWorkbenchContribution extends Disposable implements IWorkben
|
|||
|
||||
const initializeURI = (uri: URI) => {
|
||||
this.workspaceContext.getWorkspace()
|
||||
const voidInstrsURI = URI.joinPath(uri, '.voidinstructions')
|
||||
this.voidModelService.initializeModel(voidInstrsURI)
|
||||
const voidRulesURI = URI.joinPath(uri, '.voidrules')
|
||||
this.voidModelService.initializeModel(voidRulesURI)
|
||||
}
|
||||
|
||||
// call
|
||||
|
|
|
|||
|
|
@ -327,7 +327,7 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService {
|
|||
|
||||
async getDirectoryStrTool(uri: URI, options?: { maxItemsPerDir?: number }) {
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export const IconShell1 = ({ onClick, Icon, disabled, className, ...props }: Ico
|
|||
size-[18px]
|
||||
p-[2px]
|
||||
flex items-center justify-center
|
||||
text-sm bg-void-bg-3 text-void-fg-3
|
||||
text-sm text-void-fg-3
|
||||
hover:brightness-110
|
||||
disabled:opacity-50 disabled:cursor-not-allowed
|
||||
${className}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@
|
|||
|
||||
import React, { JSX, useMemo, useState } from 'react'
|
||||
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 { 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 //
|
||||
}
|
||||
|
||||
// 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 }) => {
|
||||
|
||||
// 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 }
|
||||
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()
|
||||
|
|
@ -189,24 +348,25 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
|
|||
}
|
||||
|
||||
if (t.type === 'table') {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
{t.header.map((cell: any, index: number) => (
|
||||
<th key={index}>
|
||||
{cell.raw}
|
||||
{t.header.map((h, hIdx: number) => (
|
||||
<th key={hIdx}>
|
||||
{h.text}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{t.rows.map((row: any[], rowIndex: number) => (
|
||||
<tr key={rowIndex}>
|
||||
{row.map((cell: any, cellIndex: number) => (
|
||||
<td key={cellIndex} >
|
||||
{cell.raw}
|
||||
{t.rows.map((row, rowIdx: number) => (
|
||||
<tr key={rowIdx}>
|
||||
{row.map((r, rIdx: number) => (
|
||||
<td key={rIdx} >
|
||||
{r.text}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
|
|
@ -288,6 +448,17 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
|
|||
}
|
||||
|
||||
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 = <>
|
||||
{t.tokens.map((token, index) => (
|
||||
<RenderToken key={index}
|
||||
|
|
@ -384,4 +555,3 @@ export const ChatMarkdownRender = ({ string, inPTag = false, chatMessageLocation
|
|||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import { IDisposable } from '../../../../../../../base/common/lifecycle.js';
|
|||
import { ErrorDisplay } from './ErrorDisplay.js';
|
||||
import { BlockCode, TextAreaFns, VoidCustomDropdownBox, VoidInputBox2, VoidSlider, VoidSwitch } from '../util/inputs.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_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.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 { RawToolCallObj } from '../../../../common/sendLLMMessageTypes.js';
|
||||
import { MAX_FILE_CHARS_PAGE } from '../../../toolsService.js';
|
||||
import jsonStringify from 'fast-json-stable-stringify'
|
||||
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:
|
||||
const ReasoningOptionSlider = ({ featureName }: { featureName: FeatureName }) => {
|
||||
|
|
@ -554,8 +552,7 @@ export const SelectedFiles = (
|
|||
{allSelections.map((selection, i) => {
|
||||
|
||||
const isThisSelectionProspective = i > selections.length - 1
|
||||
|
||||
const thisKey = `${isThisSelectionProspective}-${i}-${selections.length}`
|
||||
const thisKey = jsonStringify(selection)
|
||||
|
||||
return <div // container for summarybox and code
|
||||
key={thisKey}
|
||||
|
|
@ -1979,7 +1976,13 @@ type ChatBubbleProps = {
|
|||
_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 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}
|
||||
/>
|
||||
}
|
||||
// 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') {
|
||||
|
||||
if (chatMessage.type === 'invalid_params') {
|
||||
|
|
@ -2537,8 +2513,8 @@ export const SidebarChat = () => {
|
|||
// const lastMessageIdx = previousMessages.findLastIndex(v => v.role !== 'checkpoint')
|
||||
// tool request shows up as Editing... if in progress
|
||||
return previousMessages.map((message, i) => {
|
||||
return <ErrorBoundary><ChatBubble
|
||||
key={getChatBubbleId(threadId, i)}
|
||||
return <ChatBubble
|
||||
key={i}
|
||||
currCheckpointIdx={currCheckpointIdx}
|
||||
chatMessage={message}
|
||||
messageIdx={i}
|
||||
|
|
@ -2546,14 +2522,14 @@ export const SidebarChat = () => {
|
|||
chatIsRunning={isRunning}
|
||||
threadId={threadId}
|
||||
_scrollToBottom={() => scrollToBottom(scrollContainerRef)}
|
||||
/></ErrorBoundary>
|
||||
/>
|
||||
})
|
||||
}, [previousMessages, threadId, currCheckpointIdx, isRunning])
|
||||
|
||||
const streamingChatIdx = previousMessagesHTML.length
|
||||
const currStreamingMessageHTML = reasoningSoFar || displayContentSoFar || isRunning ?
|
||||
<ErrorBoundary><ChatBubble
|
||||
key={getChatBubbleId(threadId, streamingChatIdx)}
|
||||
<ChatBubble
|
||||
key={'curr-streaming-msg'}
|
||||
currCheckpointIdx={currCheckpointIdx}
|
||||
chatMessage={{
|
||||
role: 'assistant',
|
||||
|
|
@ -2567,13 +2543,13 @@ export const SidebarChat = () => {
|
|||
|
||||
threadId={threadId}
|
||||
_scrollToBottom={null}
|
||||
/></ErrorBoundary> : null
|
||||
/> : null
|
||||
|
||||
|
||||
// the tool currently being generated
|
||||
const generatingTool = toolIsGenerating ?
|
||||
toolCallSoFar.name === 'edit_file' ? <EditToolSoFar
|
||||
key={getChatBubbleId(threadId, streamingChatIdx + 1)}
|
||||
key={'curr-streaming-tool'}
|
||||
toolCallSoFar={toolCallSoFar}
|
||||
/>
|
||||
: null
|
||||
|
|
@ -2631,61 +2607,106 @@ export const SidebarChat = () => {
|
|||
}
|
||||
}, [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>
|
||||
|
||||
return (
|
||||
<div ref={sidebarRef} className='w-full h-full flex flex-col overflow-hidden'>
|
||||
{/* History selector */}
|
||||
<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>
|
||||
const landingPageInput = <div>
|
||||
<div className='pt-8'>
|
||||
{inputChatArea}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
const landingPageContent = <div
|
||||
ref={sidebarRef}
|
||||
className='w-full h-full max-h-full flex flex-col overflow-auto px-4'
|
||||
>
|
||||
<ErrorBoundary>
|
||||
{landingPageInput}
|
||||
</ErrorBoundary>
|
||||
|
||||
{Object.values(chatThreadsState.allThreads).length > 0 && // show if there are threads
|
||||
<ErrorBoundary>
|
||||
<div className='pt-8 mb-2 text-void-fg-1 text-root'>Previous Threads</div>
|
||||
<PastThreadsList />
|
||||
</ErrorBoundary>
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
// const threadPageContent = <div>
|
||||
// {/* Thread content */}
|
||||
// <div className='flex flex-col overflow-hidden'>
|
||||
// <div className={`overflow-hidden ${previousMessages.length === 0 ? 'h-0 max-h-0 pb-2' : ''}`}>
|
||||
// <ErrorBoundary>
|
||||
// {messagesHTML}
|
||||
// </ErrorBoundary>
|
||||
// </div>
|
||||
// <ErrorBoundary>
|
||||
// {inputForm}
|
||||
// </ErrorBoundary>
|
||||
// </div>
|
||||
// </div>
|
||||
const threadPageContent = <div
|
||||
ref={sidebarRef}
|
||||
className='w-full h-full flex flex-col overflow-hidden'
|
||||
>
|
||||
|
||||
<ErrorBoundary>
|
||||
{messagesHTML}
|
||||
</ErrorBoundary>
|
||||
<ErrorBoundary>
|
||||
{threadPageInput}
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
|
||||
return (
|
||||
<Fragment key={threadId} // force rerender when change thread
|
||||
>
|
||||
{isLandingPage ?
|
||||
landingPageContent
|
||||
: threadPageContent}
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -3,35 +3,20 @@
|
|||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import React from "react";
|
||||
import { useState } from 'react';
|
||||
import { IconShell1 } from '../markdown/ApplyBlockHoverButtons.js';
|
||||
import { useAccessor, useChatThreadsState } from '../util/services.js';
|
||||
import { ISidebarStateService } from '../../../sidebarStateService.js';
|
||||
import { IconX } from './SidebarChat.js';
|
||||
import { Check, Trash2, X } from 'lucide-react';
|
||||
import { ThreadType } from '../../../chatThreadService.js';
|
||||
|
||||
|
||||
const truncate = (s: string) => {
|
||||
let len = s.length
|
||||
const TRUNC_AFTER = 16
|
||||
if (len >= TRUNC_AFTER)
|
||||
s = s.substring(0, TRUNC_AFTER) + '...'
|
||||
return s
|
||||
}
|
||||
export const OldSidebarThreadSelector = () => {
|
||||
|
||||
|
||||
export const SidebarThreadSelector = () => {
|
||||
const threadsState = useChatThreadsState()
|
||||
|
||||
const accessor = useAccessor()
|
||||
const chatThreadsService = accessor.get('IChatThreadService')
|
||||
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 (
|
||||
<div className="flex p-2 flex-col gap-y-1 max-h-[200px] overflow-y-auto">
|
||||
|
||||
|
|
@ -52,72 +37,297 @@ export const SidebarThreadSelector = () => {
|
|||
</div>
|
||||
|
||||
{/* a list of all the past threads */}
|
||||
<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-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>
|
||||
{/* <OldPastThreadsList /> */}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const truncate = (s: string) => {
|
||||
let len = s.length
|
||||
const TRUNC_AFTER = 16
|
||||
if (len >= TRUNC_AFTER)
|
||||
s = s.substring(0, TRUNC_AFTER) + '...'
|
||||
return s
|
||||
}
|
||||
|
||||
|
||||
|
||||
const OldPastThreadsList = () => {
|
||||
|
||||
const accessor = useAccessor()
|
||||
const chatThreadsService = accessor.get('IChatThreadService')
|
||||
const sidebarStateService = accessor.get('ISidebarStateService')
|
||||
|
||||
const threadsState = useChatThreadsState()
|
||||
const { allThreads } = threadsState
|
||||
|
||||
// sorted by most recent to least recent
|
||||
const sortedThreadIds = Object.keys(allThreads ?? {})
|
||||
.sort((threadId1, threadId2) => (allThreads[threadId1]?.lastModified ?? 0) > (allThreads[threadId2]?.lastModified ?? 0) ? -1 : 1)
|
||||
.filter(threadId => (allThreads![threadId]?.messages.length ?? 0) !== 0)
|
||||
|
||||
|
||||
return <div className="px-1">
|
||||
<ul className="flex flex-col gap-y-0.5 overflow-y-auto list-disc">
|
||||
|
||||
{sortedThreadIds.length === 0
|
||||
|
||||
? <div key="nothreads" className="text-center text-void-fg-3 brightness-90 text-root">{`There are no chat threads yet.`}</div>
|
||||
|
||||
: sortedThreadIds.map((threadId) => {
|
||||
if (!allThreads) {
|
||||
return <li key="error" className="text-void-warning">{`Error accessing chat history.`}</li>;
|
||||
}
|
||||
const pastThread = allThreads[threadId];
|
||||
if (!pastThread) {
|
||||
return <li key="error" className="text-void-warning">{`Error accessing chat history.`}</li>;
|
||||
}
|
||||
|
||||
|
||||
let firstMsg = null;
|
||||
// let secondMsg = null;
|
||||
|
||||
const firstUserMsgIdx = pastThread.messages.findIndex((msg) => msg.role === 'user');
|
||||
|
||||
if (firstUserMsgIdx !== -1) {
|
||||
// firstMsg = truncate(pastThread.messages[firstMsgIdx].displayContent ?? '');
|
||||
const firsUsertMsgObj = pastThread.messages[firstUserMsgIdx]
|
||||
firstMsg = firsUsertMsgObj.role === 'user' && firsUsertMsgObj.displayContent || '';
|
||||
} else {
|
||||
firstMsg = '""';
|
||||
}
|
||||
|
||||
// const secondMsgIdx = pastThread.messages.findIndex(
|
||||
// (msg, i) => msg.role !== 'system' && !!msg.displayContent && i > firstMsgIdx
|
||||
// );
|
||||
|
||||
// if (secondMsgIdx !== -1) {
|
||||
// secondMsg = truncate(pastThread.messages[secondMsgIdx].displayContent ?? '');
|
||||
// }
|
||||
|
||||
const numMessages = pastThread.messages.filter((msg) => msg.role === 'assistant' || msg.role === 'user').length;
|
||||
|
||||
return (
|
||||
<li key={pastThread.id}>
|
||||
<button
|
||||
type='button'
|
||||
className={`
|
||||
hover:bg-void-bg-1
|
||||
${threadsState.currentThreadId === pastThread.id ? 'bg-void-bg-1' : ''}
|
||||
rounded-sm px-2 py-1
|
||||
w-full
|
||||
text-left
|
||||
flex items-center
|
||||
`}
|
||||
onClick={() => {
|
||||
chatThreadsService.switchToThread(pastThread.id);
|
||||
sidebarStateService.setState({ isHistoryOpen: false })
|
||||
}}
|
||||
title={new Date(pastThread.lastModified).toLocaleString()}
|
||||
>
|
||||
<div className='truncate'>{`${firstMsg}`}</div>
|
||||
<div>{`\u00A0(${numMessages})`}</div>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
const numInitialThreads = 3
|
||||
|
||||
export const PastThreadsList = ({ className = '' }: { className?: string }) => {
|
||||
const [showAll, setShowAll] = useState(false);
|
||||
|
||||
const [hoveredIdx, setHoveredIdx] = useState<number | null>(null)
|
||||
|
||||
const threadsState = useChatThreadsState()
|
||||
const { allThreads } = threadsState
|
||||
|
||||
if (!allThreads) {
|
||||
return <div key="error" className="p-1">{`Error accessing chat history.`}</div>;
|
||||
}
|
||||
|
||||
// sorted by most recent to least recent
|
||||
const sortedThreadIds = Object.keys(allThreads ?? {})
|
||||
.sort((threadId1, threadId2) => (allThreads[threadId1]?.lastModified ?? 0) > (allThreads[threadId2]?.lastModified ?? 0) ? -1 : 1)
|
||||
.filter(threadId => (allThreads![threadId]?.messages.length ?? 0) !== 0)
|
||||
|
||||
// Get only first 5 threads if not showing all
|
||||
const hasMoreThreads = sortedThreadIds.length > numInitialThreads;
|
||||
const displayThreads = showAll ? sortedThreadIds : sortedThreadIds.slice(0, numInitialThreads);
|
||||
|
||||
return (
|
||||
<div className={`flex flex-col mb-2 gap-2 w-full text-nowrap text-void-fg-3 select-none relative ${className}`}>
|
||||
{displayThreads.length === 0
|
||||
? <></> // No chats yet... Suggestion: Tell me about my codebase Suggestion: Create a new .voidrules file in the root of my repo
|
||||
: displayThreads.map((threadId, i) => {
|
||||
const pastThread = allThreads[threadId];
|
||||
if (!pastThread) {
|
||||
return <div key={i} className="p-1">{`Error accessing chat history.`}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<PastThreadElement
|
||||
key={pastThread.id}
|
||||
pastThread={pastThread}
|
||||
idx={i}
|
||||
hoveredIdx={hoveredIdx}
|
||||
setHoveredIdx={setHoveredIdx}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
{hasMoreThreads && !showAll && (
|
||||
<div
|
||||
className="text-void-fg-3 opacity-60 hover:opacity-100 hover:brightness-115 cursor-pointer p-1 text-xs"
|
||||
onClick={() => setShowAll(true)}
|
||||
>
|
||||
Show {sortedThreadIds.length - numInitialThreads} more...
|
||||
</div>
|
||||
)}
|
||||
{hasMoreThreads && showAll && (
|
||||
<div
|
||||
className="text-void-fg-3 opacity-60 hover:opacity-100 hover:brightness-115 cursor-pointer p-1 text-xs"
|
||||
onClick={() => setShowAll(false)}
|
||||
>
|
||||
Show less
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// Format date to display as today, yesterday, or date
|
||||
const formatDate = (date: Date) => {
|
||||
const now = new Date();
|
||||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
const yesterday = new Date(today);
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
|
||||
if (date >= today) {
|
||||
return 'Today';
|
||||
} else if (date >= yesterday) {
|
||||
return 'Yesterday';
|
||||
} else {
|
||||
return `${date.toLocaleString('default', { month: 'short' })} ${date.getDate()}`;
|
||||
}
|
||||
};
|
||||
|
||||
// Format time to 12-hour format
|
||||
const formatTime = (date: Date) => {
|
||||
return date.toLocaleString('en-US', {
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
hour12: true
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
const TrashButton = ({ threadId }: { threadId: string }) => {
|
||||
|
||||
const accessor = useAccessor()
|
||||
const chatThreadsService = accessor.get('IChatThreadService')
|
||||
|
||||
|
||||
const [isTrashPressed, setIsTrashPressed] = useState(false)
|
||||
|
||||
return (isTrashPressed ?
|
||||
<div className='flex flex-nowrap text-nowrap gap-1'>
|
||||
<IconShell1
|
||||
Icon={X}
|
||||
className='size-[11px]'
|
||||
onClick={() => { setIsTrashPressed(false); }}
|
||||
data-tooltip-id='void-tooltip'
|
||||
data-tooltip-place='top'
|
||||
data-tooltip-content='Cancel'
|
||||
/>
|
||||
<IconShell1
|
||||
Icon={Check}
|
||||
className='size-[11px]'
|
||||
onClick={() => { chatThreadsService.deleteThread(threadId); setIsTrashPressed(false); }}
|
||||
data-tooltip-id='void-tooltip'
|
||||
data-tooltip-place='top'
|
||||
data-tooltip-content='Confirm'
|
||||
/>
|
||||
</div>
|
||||
: <IconShell1
|
||||
Icon={Trash2}
|
||||
className='size-[11px]'
|
||||
onClick={() => { setIsTrashPressed(true); }}
|
||||
data-tooltip-id='void-tooltip'
|
||||
data-tooltip-place='top'
|
||||
data-tooltip-content='Delete thread?'
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const PastThreadElement = ({ pastThread, idx, hoveredIdx, setHoveredIdx }: { pastThread: ThreadType, idx: number, hoveredIdx: number | null, setHoveredIdx: (idx: number | null) => void }) => {
|
||||
|
||||
|
||||
const accessor = useAccessor()
|
||||
const chatThreadsService = accessor.get('IChatThreadService')
|
||||
const sidebarStateService = accessor.get('ISidebarStateService')
|
||||
|
||||
let firstMsg = null;
|
||||
const firstUserMsgIdx = pastThread.messages.findIndex((msg) => msg.role === 'user');
|
||||
|
||||
if (firstUserMsgIdx !== -1) {
|
||||
const firsUsertMsgObj = pastThread.messages[firstUserMsgIdx];
|
||||
firstMsg = firsUsertMsgObj.role === 'user' && firsUsertMsgObj.displayContent || '';
|
||||
} else {
|
||||
firstMsg = '""';
|
||||
}
|
||||
|
||||
const numMessages = pastThread.messages.filter((msg) => msg.role === 'assistant' || msg.role === 'user').length;
|
||||
|
||||
const detailsHTML = <span
|
||||
className='gap-1 inline-flex items-center'
|
||||
// data-tooltip-id='void-tooltip'
|
||||
// data-tooltip-content={`Last modified ${formatTime(new Date(pastThread.lastModified))}`}
|
||||
// data-tooltip-place='top'
|
||||
>
|
||||
{/* <span>{numMessages}</span> */}
|
||||
{formatDate(new Date(pastThread.lastModified))}
|
||||
</span>
|
||||
|
||||
return <div
|
||||
key={pastThread.id}
|
||||
className={`
|
||||
py-1 px-2 rounded text-sm bg-zinc-700/5 hover:bg-zinc-700/10 dark:bg-zinc-300/5 dark:hover:bg-zinc-300/10 cursor-pointer opacity-80 hover:opacity-100
|
||||
`}
|
||||
onClick={() => {
|
||||
chatThreadsService.switchToThread(pastThread.id);
|
||||
sidebarStateService.setState({ isHistoryOpen: false });
|
||||
}}
|
||||
onMouseEnter={() => setHoveredIdx(idx)}
|
||||
onMouseLeave={() => setHoveredIdx(null)}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-1">
|
||||
<span className="flex items-center gap-2 min-w-0 overflow-hidden">
|
||||
<span className="truncate overflow-hidden text-ellipsis">{firstMsg}</span>
|
||||
</span>
|
||||
|
||||
<div className="flex items-center gap-2 opacity-60">
|
||||
{idx === hoveredIdx ?
|
||||
<TrashButton threadId={pastThread.id} />
|
||||
: detailsHTML
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
--void-bg-1-alt: var(--vscode-badge-background);
|
||||
--void-bg-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-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-fg-0: color-mix(in srgb, var(--vscode-tab-activeForeground) 90%, black 10%);
|
||||
|
|
|
|||
|
|
@ -664,7 +664,7 @@ export const VoidCustomDropdownBox = <T extends NonNullable<any>>({
|
|||
{isOpen && (
|
||||
<div
|
||||
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={{
|
||||
position: strategy,
|
||||
top: y ?? 0,
|
||||
|
|
@ -689,7 +689,7 @@ export const VoidCustomDropdownBox = <T extends NonNullable<any>>({
|
|||
key={optionName}
|
||||
className={`flex items-center px-2 py-1 pr-4 cursor-pointer whitespace-nowrap
|
||||
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={() => {
|
||||
onChangeOption(option);
|
||||
|
|
@ -709,7 +709,7 @@ export const VoidCustomDropdownBox = <T extends NonNullable<any>>({
|
|||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<span className="flex justify-between w-full">
|
||||
<span className="flex justify-between items-center w-full gap-x-1">
|
||||
<span>{optionName}</span>
|
||||
<span className='text-void-fg-4 opacity-60'>{optionDetail}</span>
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -307,7 +307,7 @@ const TableOfModelsForProvider = ({ providerName }: { providerName: ProviderName
|
|||
|
||||
voidSettingsState.settingsOfProvider[providerName].models.forEach(m => {
|
||||
infoOfModelName[m.modelName] = {
|
||||
showAsDefault: m.isDefault,
|
||||
showAsDefault: m.type === 'default',
|
||||
isDownloaded: true
|
||||
}
|
||||
})
|
||||
|
|
@ -367,7 +367,7 @@ const TableOfModelsForProvider = ({ providerName }: { providerName: ProviderName
|
|||
|
||||
|
||||
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">
|
||||
{!showAsDefault && removeModelButton}
|
||||
{modelName}
|
||||
|
|
@ -497,7 +497,7 @@ const VoidOnboardingContent = () => {
|
|||
|
||||
const providerNamesOfWantToUseOption: { [wantToUseOption in WantToUseOption]: ProviderName[] } = {
|
||||
smart: ['anthropic', 'openAI', 'gemini', 'openRouter'],
|
||||
private: ['ollama', 'vLLM', 'openAICompatible'],
|
||||
private: ['ollama', 'vLLM', 'openAICompatible', 'lmStudio'],
|
||||
cheap: ['gemini', 'deepseek', 'openRouter', 'ollama', 'vLLM'],
|
||||
all: providerNames,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
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 { VoidButtonBgDarken, VoidCustomDropdownBox, VoidInputBox2, VoidSimpleInputBox, VoidSwitch } from '../util/inputs.js'
|
||||
import { useAccessor, useIsDark, useRefreshModelListener, useRefreshModelState, useSettingsState } from '../util/services.js'
|
||||
|
|
@ -286,7 +286,7 @@ export const ModelDump = () => {
|
|||
|
||||
return <div className=''>
|
||||
{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
|
||||
|
||||
|
|
@ -318,7 +318,7 @@ export const ModelDump = () => {
|
|||
// : (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
|
||||
value={value}
|
||||
|
|
@ -332,7 +332,7 @@ export const ModelDump = () => {
|
|||
/>
|
||||
|
||||
<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>
|
||||
|
|
@ -344,9 +344,9 @@ export const ModelDump = () => {
|
|||
|
||||
// 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 voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
|
|
@ -370,10 +370,9 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider
|
|||
passwordBlur={isPasswordField}
|
||||
compact={true}
|
||||
/>
|
||||
{subTextMd === undefined ? null : <div className='py-1 px-3 opacity-50 text-sm'>
|
||||
<ChatMarkdownRender string={subTextMd} chatMessageLocation={undefined} />
|
||||
{!subTextMd ? null : <div className='py-1 px-3 opacity-50 text-sm'>
|
||||
{subTextMd}
|
||||
</div>}
|
||||
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
}
|
||||
|
|
@ -456,7 +455,14 @@ export const SettingsForProvider = ({ providerName, showProviderTitle, showProvi
|
|||
<div className='px-0'>
|
||||
{/* settings besides models (e.g. api key) */}
|
||||
{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 ?
|
||||
|
|
@ -1025,11 +1031,11 @@ export const Settings = () => {
|
|||
<div className='mt-12 max-w-[600px]'>
|
||||
<h2 className={`text-3xl mb-2`}>AI Instructions</h2>
|
||||
<h4 className={`text-void-fg-3 mb-4`}>
|
||||
<ChatMarkdownRender inPTag={true} string={`
|
||||
<ChatMarkdownRender inPTag={true} string={`
|
||||
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} />
|
||||
</h4>
|
||||
</h4>
|
||||
<ErrorBoundary>
|
||||
<AIInstructionsBox />
|
||||
</ErrorBoundary>
|
||||
|
|
|
|||
|
|
@ -202,7 +202,19 @@ registerAction2(class extends Action2 {
|
|||
})
|
||||
|
||||
|
||||
const openNewThreadAndFireFocus = (accessor: ServicesAccessor) => {
|
||||
|
||||
const stateService = accessor.get(ISidebarStateService)
|
||||
stateService.setState({ isHistoryOpen: false, currentTab: 'chat' })
|
||||
const chatThreadService = accessor.get(IChatThreadService)
|
||||
chatThreadService.openNewThread()
|
||||
|
||||
// focus
|
||||
stateService.fireFocusChat()
|
||||
const window = getActiveWindow()
|
||||
window.requestAnimationFrame(() => stateService.fireFocusChat())
|
||||
|
||||
}
|
||||
|
||||
|
||||
// New chat menu button
|
||||
|
|
@ -213,6 +225,25 @@ registerAction2(class extends Action2 {
|
|||
title: 'New Chat',
|
||||
icon: { id: 'add' },
|
||||
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: {
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyL,
|
||||
weight: KeybindingWeight.VoidExtension,
|
||||
|
|
@ -220,19 +251,16 @@ registerAction2(class extends Action2 {
|
|||
});
|
||||
}
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const stateService = accessor.get(ISidebarStateService)
|
||||
|
||||
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' })
|
||||
const chatThreadService = accessor.get(IChatThreadService)
|
||||
chatThreadService.openNewThread()
|
||||
// add user's selection to chat
|
||||
await commandService.executeCommand(VOID_CTRL_L_ACTION_ID)
|
||||
|
||||
// focus
|
||||
stateService.fireFocusChat()
|
||||
const window = getActiveWindow()
|
||||
window.requestAnimationFrame(() => stateService.fireFocusChat())
|
||||
}
|
||||
})
|
||||
|
||||
|
|
@ -247,13 +275,27 @@ registerAction2(class extends Action2 {
|
|||
});
|
||||
}
|
||||
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 metricsService = accessor.get(IMetricsService)
|
||||
|
||||
|
||||
metricsService.capture('Chat Navigation', { type: 'History' })
|
||||
|
||||
openNewThreadAndFireFocus(accessor)
|
||||
|
||||
// doesnt do anything right now
|
||||
stateService.setState({ isHistoryOpen: !stateService.state.isHistoryOpen, currentTab: 'chat' })
|
||||
stateService.fireBlurChat()
|
||||
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import { VOID_OPEN_SIDEBAR_ACTION_ID } from './sidebarPane.js';
|
|||
|
||||
// service that manages sidebar's state
|
||||
export type VoidSidebarState = {
|
||||
isHistoryOpen: boolean;
|
||||
isHistoryOpen: boolean; // this isn't doing anything right now
|
||||
currentTab: 'chat';
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -263,7 +263,7 @@ export class ToolsService implements IToolsService {
|
|||
read_file: async ({ uri, startLine, endLine, pageNumber }) => {
|
||||
await voidModelService.initializeModel(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
|
||||
if (startLine === null && endLine === null) {
|
||||
|
|
|
|||
|
|
@ -153,6 +153,7 @@ export const extractCodeFromFIM = ({ text, recentlyAddedTextLen, midTag, }: { te
|
|||
const foundMid = pm.removePrefix(`<${midTag}>`)
|
||||
|
||||
if (foundMid) {
|
||||
pm.removeSuffix(`\n`) // sometimes outputs \n
|
||||
pm.removeSuffix(`</${midTag}>`)
|
||||
}
|
||||
const s = pm.value()
|
||||
|
|
|
|||
|
|
@ -43,7 +43,22 @@ export const defaultProviderSettings = {
|
|||
},
|
||||
mistral: {
|
||||
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
|
||||
|
||||
|
||||
|
|
@ -84,6 +99,8 @@ export const defaultModelsOfProvider = {
|
|||
],
|
||||
vLLM: [ // autodetected
|
||||
],
|
||||
lmStudio: [], // autodetected
|
||||
|
||||
openRouter: [ // https://openrouter.ai/models
|
||||
// 'anthropic/claude-3.7-sonnet:thinking',
|
||||
'anthropic/claude-3.7-sonnet',
|
||||
|
|
@ -112,6 +129,11 @@ export const defaultModelsOfProvider = {
|
|||
'ministral-8b-latest',
|
||||
],
|
||||
openAICompatible: [], // fallback
|
||||
googleVertex: [],
|
||||
microsoftAzure: [],
|
||||
liteLLM: [],
|
||||
|
||||
|
||||
} as const satisfies Record<ProviderName, string[]>
|
||||
|
||||
|
||||
|
|
@ -168,7 +190,7 @@ type VoidStaticProviderInfo = { // doesn't change (not stateful)
|
|||
|
||||
|
||||
const modelOptionsDefaults: VoidStaticModelInfo = {
|
||||
contextWindow: 32_000,
|
||||
contextWindow: 16_000,
|
||||
maxOutputTokens: 4_096,
|
||||
cost: { input: 0, output: 0 },
|
||||
downloadable: false,
|
||||
|
|
@ -806,6 +828,25 @@ const groqSettings: VoidStaticProviderInfo = {
|
|||
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 = {
|
||||
'qwen2.5-coder:1.5b': {
|
||||
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)[]
|
||||
|
||||
|
||||
|
||||
// ---------------- VLLM, OLLAMA, OPENAICOMPAT (self-hosted / local) ----------------
|
||||
|
||||
const vLLMSettings: VoidStaticProviderInfo = {
|
||||
// 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' }, },
|
||||
|
|
@ -868,6 +906,12 @@ const vLLMSettings: VoidStaticProviderInfo = {
|
|||
modelOptions: {}, // TODO
|
||||
}
|
||||
|
||||
const lmStudioSettings: VoidStaticProviderInfo = {
|
||||
providerReasoningIOSettings: { output: { needsManualParse: true }, },
|
||||
modelOptionsFallback: (modelName) => extensiveModelFallback(modelName, { downloadable: { sizeGb: 'not-known' } }),
|
||||
modelOptions: {}, // TODO
|
||||
}
|
||||
|
||||
const ollamaSettings: VoidStaticProviderInfo = {
|
||||
// reasoning: we need to filter out reasoning <think> tags manually
|
||||
providerReasoningIOSettings: { output: { needsManualParse: true }, },
|
||||
|
|
@ -881,6 +925,12 @@ const openaiCompatible: VoidStaticProviderInfo = {
|
|||
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 ----------------
|
||||
const openRouterModelOptions_assumingOpenAICompat = {
|
||||
|
|
@ -1027,9 +1077,12 @@ const modelSettingsOfProvider: { [providerName in ProviderName]: VoidStaticProvi
|
|||
ollama: ollamaSettings,
|
||||
openAICompatible: openaiCompatible,
|
||||
mistral: mistralSettings,
|
||||
// googleVertex: {},
|
||||
// microsoftAzure: {},
|
||||
// openHands: {},
|
||||
|
||||
liteLLM: liteLLMSettings,
|
||||
lmStudio: lmStudioSettings,
|
||||
|
||||
googleVertex: googleVertexSettings,
|
||||
microsoftAzure: microsoftAzureSettings,
|
||||
} as const
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -330,6 +330,7 @@ Here's an example of a good edit suggestion:
|
|||
${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(`Today's date is ${new Date().toDateString()}.`)
|
||||
|
||||
|
|
@ -446,9 +447,10 @@ export const DIVIDER = `=======`
|
|||
export const FINAL = `>>>>>>> UPDATED`
|
||||
|
||||
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]}
|
||||
${ORIGINAL}
|
||||
// ... original code goes here
|
||||
|
|
@ -457,23 +459,28 @@ ${DIVIDER}
|
|||
${FINAL}
|
||||
${tripleTick[1]}
|
||||
|
||||
You will be given the original file \`ORIGINAL_FILE\` and a diff to apply to the file, \`CHANGE\`.
|
||||
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.
|
||||
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.
|
||||
|
||||
Directions:
|
||||
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.
|
||||
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.
|
||||
|
||||
The SEARCH/REPLACE blocks you generate will be applied immediately, and so they **MUST** produce a file that the user can run IMMEDIATELY.
|
||||
- Make sure you add all necessary imports.
|
||||
- Make sure the "UPDATED" code is ready for production as-is, and fix any relevant lint errors.
|
||||
3. You are allowed to output multiple SEARCH/REPLACE blocks.
|
||||
|
||||
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
|
||||
DIFF
|
||||
${tripleTick[0]}
|
||||
// ... existing code
|
||||
let x = 6.5
|
||||
// ... existing code
|
||||
${tripleTick[1]}
|
||||
|
||||
ORIGINAL_FILE
|
||||
${tripleTick[0]}
|
||||
let w = 5
|
||||
|
|
@ -482,15 +489,6 @@ let y = 7
|
|||
let z = 8
|
||||
${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
|
||||
${tripleTick[0]}
|
||||
${ORIGINAL}
|
||||
|
|
@ -502,11 +500,14 @@ ${tripleTick[1]}
|
|||
`
|
||||
|
||||
export const searchReplace_userMessage = ({ originalCode, applyStr }: { originalCode: string, applyStr: string }) => `\
|
||||
ORIGINAL_FILE
|
||||
${originalCode}
|
||||
DIFF
|
||||
${applyStr}
|
||||
|
||||
CHANGE
|
||||
${applyStr}`
|
||||
ORIGINAL_FILE
|
||||
${tripleTick[0]}
|
||||
${originalCode}
|
||||
${tripleTick[1]}
|
||||
`
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { ILLMMessageService } from './sendLLMMessageService.js';
|
|||
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||
import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js';
|
||||
import { RefreshableProviderName, refreshableProviderNames, SettingsOfProvider } from './voidSettingsTypes.js';
|
||||
import { OllamaModelResponse, VLLMModelResponse } from './sendLLMMessageTypes.js';
|
||||
import { OllamaModelResponse, OpenaiCompatibleModelResponse } from './sendLLMMessageTypes.js';
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.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])[] } = {
|
||||
ollama: ['_didFillInProviderSettings', 'endpoint'],
|
||||
vLLM: ['_didFillInProviderSettings', 'endpoint'],
|
||||
lmStudio: ['_didFillInProviderSettings', 'endpoint'],
|
||||
// openAICompatible: ['_didFillInProviderSettings', 'endpoint', 'apiKey'],
|
||||
}
|
||||
const REFRESH_INTERVAL = 5_000
|
||||
|
|
@ -142,6 +143,7 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
|
|||
state: RefreshModelStateOfProvider = {
|
||||
ollama: { 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
|
||||
: providerName === 'vLLM' ? this.llmMessageService.vLLMList
|
||||
: () => { }
|
||||
: this.llmMessageService.openAICompatibleList
|
||||
|
||||
listFn({
|
||||
providerName,
|
||||
onSuccess: ({ models }) => {
|
||||
|
||||
// set the models to the detected models
|
||||
this.voidSettingsService.setAutodetectedModels(
|
||||
providerName,
|
||||
models.map(model => {
|
||||
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);
|
||||
}),
|
||||
{ enableProviderOnSuccess: options.enableProviderOnSuccess, hideRefresh: options.doNotFire }
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, ServiceSendLLMMessageParams, MainSendLLMMessageParams, MainLLMMessageAbortParams, ServiceModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, MainModelListParams, OllamaModelResponse, 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 { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
|
|
@ -22,7 +22,7 @@ export interface ILLMMessageService {
|
|||
sendLLMMessage: (params: ServiceSendLLMMessageParams) => string | null;
|
||||
abort: (requestId: string) => 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) },
|
||||
error: {} as { [eventId: string]: ((params: EventModelListOnErrorParams<OllamaModelResponse>) => void) },
|
||||
},
|
||||
vLLM: {
|
||||
success: {} as { [eventId: string]: ((params: EventModelListOnSuccessParams<VLLMModelResponse>) => void) },
|
||||
error: {} as { [eventId: string]: ((params: EventModelListOnErrorParams<VLLMModelResponse>) => void) },
|
||||
openAICompat: {
|
||||
success: {} as { [eventId: string]: ((params: EventModelListOnSuccessParams<OpenaiCompatibleModelResponse>) => void) },
|
||||
error: {} as { [eventId: string]: ((params: EventModelListOnErrorParams<OpenaiCompatibleModelResponse>) => void) },
|
||||
}
|
||||
} satisfies {
|
||||
[providerName: string]: {
|
||||
[providerName in 'ollama' | 'openAICompat']: {
|
||||
success: { [eventId: string]: ((params: EventModelListOnSuccessParams<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
|
||||
// llm
|
||||
this._register((this.channel.listen('onText_sendLLMMessage') satisfies Event<EventLLMMessageOnTextParams>)(e => { this.llmMessageHooks.onText[e.requestId]?.(e) }))
|
||||
this._register((this.channel.listen('onFinalMessage_sendLLMMessage') satisfies Event<EventLLMMessageOnFinalMessageParams>)(e => { this.llmMessageHooks.onFinalMessage[e.requestId]?.(e); this._clearChannelHooks(e.requestId) }))
|
||||
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('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_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('onText_sendLLMMessage') satisfies Event<EventLLMMessageOnTextParams>)(e => {
|
||||
this.llmMessageHooks.onText[e.requestId]?.(e)
|
||||
}))
|
||||
this._register((this.channel.listen('onFinalMessage_sendLLMMessage') satisfies Event<EventLLMMessageOnFinalMessageParams>)(e => {
|
||||
this.llmMessageHooks.onFinalMessage[e.requestId]?.(e);
|
||||
this._clearChannelHooks(e.requestId)
|
||||
}))
|
||||
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 { settingsOfProvider } = this.voidSettingsService.state
|
||||
|
||||
// add state for request id
|
||||
const requestId_ = generateUuid();
|
||||
this.listHooks.vLLM.success[requestId_] = onSuccess
|
||||
this.listHooks.vLLM.error[requestId_] = onError
|
||||
this.listHooks.openAICompat.success[requestId_] = onSuccess
|
||||
this.listHooks.openAICompat.error[requestId_] = onError
|
||||
|
||||
this.channel.call('vLLMList', {
|
||||
this.channel.call('openAICompatibleList', {
|
||||
...proxyParams,
|
||||
settingsOfProvider,
|
||||
providerName: 'vLLM',
|
||||
requestId: requestId_,
|
||||
} satisfies MainModelListParams<VLLMModelResponse>)
|
||||
} satisfies MainModelListParams<OpenaiCompatibleModelResponse>)
|
||||
}
|
||||
|
||||
_clearChannelHooks(requestId: string) {
|
||||
private _clearChannelHooks(requestId: string) {
|
||||
delete this.llmMessageHooks.onText[requestId]
|
||||
delete this.llmMessageHooks.onFinalMessage[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.error[requestId]
|
||||
|
||||
delete this.listHooks.vLLM.success[requestId]
|
||||
delete this.listHooks.vLLM.error[requestId]
|
||||
delete this.listHooks.openAICompat.success[requestId]
|
||||
delete this.listHooks.openAICompat.error[requestId]
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
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 => {
|
||||
|
|
@ -162,15 +162,13 @@ export type OllamaModelResponse = {
|
|||
size_vram: number;
|
||||
}
|
||||
|
||||
type OpenaiCompatibleModelResponse = {
|
||||
export type OpenaiCompatibleModelResponse = {
|
||||
id: string;
|
||||
created: number;
|
||||
object: 'model';
|
||||
owned_by: string;
|
||||
}
|
||||
|
||||
export type VLLMModelResponse = OpenaiCompatibleModelResponse
|
||||
|
||||
|
||||
|
||||
// params to the true list fn
|
||||
|
|
@ -183,12 +181,13 @@ export type ModelListParams<ModelResponse> = {
|
|||
|
||||
// params to the service
|
||||
export type ServiceModelListParams<modelResponse> = {
|
||||
providerName: RefreshableProviderName;
|
||||
onSuccess: (param: { models: modelResponse[] }) => void;
|
||||
onError: (param: { error: any }) => void;
|
||||
}
|
||||
|
||||
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 EventModelListOnErrorParams<modelResponse> = Parameters<ModelListParams<modelResponse>['onError']>[0] & { requestId: string }
|
||||
|
|
|
|||
|
|
@ -42,10 +42,15 @@ class VoidModelService extends Disposable implements IVoidModelService {
|
|||
}
|
||||
|
||||
initializeModel = async (uri: URI) => {
|
||||
if (uri.fsPath in this._modelRefOfURI) return;
|
||||
const editorModelRef = await this._textModelService.createModelReference(uri);
|
||||
// Keep a strong reference to prevent disposal
|
||||
this._modelRefOfURI[uri.fsPath] = editorModelRef;
|
||||
try {
|
||||
if (uri.fsPath in this._modelRefOfURI) return;
|
||||
const editorModelRef = await this._textModelService.createModelReference(uri);
|
||||
// Keep a strong reference to prevent disposal
|
||||
this._modelRefOfURI[uri.fsPath] = editorModelRef;
|
||||
}
|
||||
catch (e) {
|
||||
console.log('InitializeModel error:', e)
|
||||
}
|
||||
};
|
||||
|
||||
getModelFromFsPath = (fsPath: string): VoidModelType => {
|
||||
|
|
|
|||
|
|
@ -71,24 +71,22 @@ export interface IVoidSettingsService {
|
|||
|
||||
|
||||
|
||||
const _updatedModelsAfterDefaultModelsChange = (defaultModelNames: string[], options: { existingModels: VoidStatefulModelInfo[], didAutoDetect: boolean }) => {
|
||||
const { existingModels, didAutoDetect } = options
|
||||
const _modelsWithSwappedInNewModels = (options: { existingModels: VoidStatefulModelInfo[], models: string[], type: 'autodetected' | 'default' }) => {
|
||||
const { existingModels, models, type } = options
|
||||
|
||||
const existingModelsMap: Record<string, VoidStatefulModelInfo> = {}
|
||||
for (const existingModel of existingModels) {
|
||||
existingModelsMap[existingModel.modelName] = existingModel
|
||||
}
|
||||
|
||||
const newDefaultModels = defaultModelNames.map((modelName, i) => ({
|
||||
modelName,
|
||||
isDefault: true,
|
||||
isAutodetected: didAutoDetect,
|
||||
isHidden: !!existingModelsMap[modelName]?.isHidden,
|
||||
}))
|
||||
const newDefaultModels = models.map((modelName, i) => ({ modelName, type, isHidden: !!existingModelsMap[modelName]?.isHidden, }))
|
||||
|
||||
return [
|
||||
...newDefaultModels, // swap out all the default models for the new default models
|
||||
...existingModels.filter(m => !m.isDefault), // keep any non-default (custom) models
|
||||
...newDefaultModels, // swap out all the models of this type for the new models of this type
|
||||
...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
|
||||
|
||||
// recompute default models
|
||||
|
|
@ -109,7 +107,7 @@ const _stateWithUpdatedDefaultModels = (state: VoidSettingsState): VoidSettingsS
|
|||
const defaultModels = defaultSettingsOfProvider[providerName]?.models ?? []
|
||||
const currentModels = newSettingsOfProvider[providerName]?.models ?? []
|
||||
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,
|
||||
[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
|
||||
readS = {
|
||||
...readS,
|
||||
settingsOfProvider: {
|
||||
try {
|
||||
readS = {
|
||||
...readS,
|
||||
...defaultSettingsOfProvider,
|
||||
...readS.settingsOfProvider,
|
||||
mistral: { // we added mistral
|
||||
...defaultSettingsOfProvider.mistral,
|
||||
...readS.settingsOfProvider.mistral,
|
||||
},
|
||||
} // we added mistral
|
||||
}
|
||||
|
||||
for (const providerName of providerNames) {
|
||||
readS.settingsOfProvider[providerName] = {
|
||||
...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 = _stateWithUpdatedDefaultModels(this.state)
|
||||
this.state = _stateWithMergedDefaultModels(this.state)
|
||||
this.state = _validatedModelState(this.state);
|
||||
|
||||
|
||||
this._resolver();
|
||||
this._onDidChangeState.fire();
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -389,7 +408,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
|||
const { models } = this.state.settingsOfProvider[providerName]
|
||||
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)
|
||||
|
||||
// if the models changed, log it
|
||||
|
|
@ -423,7 +442,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
|||
if (existingIdx !== -1) return // if exists, do nothing
|
||||
const newModels = [
|
||||
...models,
|
||||
{ modelName, isDefault: false, isHidden: false }
|
||||
{ modelName, type: 'custom', isHidden: false } as const
|
||||
]
|
||||
this.setSettingOfProvider(providerName, 'models', newModels)
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ type UnionOfKeys<T> = T extends T ? keyof T : never;
|
|||
export type ProviderName = keyof typeof defaultProviderSettings
|
||||
export 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
|
||||
|
||||
type CustomSettingName = UnionOfKeys<typeof defaultProviderSettings[ProviderName]>
|
||||
|
|
@ -30,9 +30,8 @@ export const customSettingNamesOfProvider = (providerName: ProviderName) => {
|
|||
|
||||
export type VoidStatefulModelInfo = { // <-- STATEFUL
|
||||
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)
|
||||
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
|
||||
|
||||
|
||||
|
|
@ -59,74 +58,78 @@ type DisplayInfoForProviderName = {
|
|||
|
||||
export const displayInfoOfProviderName = (providerName: ProviderName): DisplayInfoForProviderName => {
|
||||
if (providerName === 'anthropic') {
|
||||
return {
|
||||
title: 'Anthropic',
|
||||
}
|
||||
return { title: 'Anthropic', }
|
||||
}
|
||||
else if (providerName === 'openAI') {
|
||||
return {
|
||||
title: 'OpenAI',
|
||||
}
|
||||
return { title: 'OpenAI', }
|
||||
}
|
||||
else if (providerName === 'deepseek') {
|
||||
return {
|
||||
// title: 'DeepSeek.com API',
|
||||
title: 'DeepSeek',
|
||||
}
|
||||
return { title: 'DeepSeek', }
|
||||
}
|
||||
else if (providerName === 'openRouter') {
|
||||
return {
|
||||
title: 'OpenRouter',
|
||||
}
|
||||
return { title: 'OpenRouter', }
|
||||
}
|
||||
else if (providerName === 'ollama') {
|
||||
return {
|
||||
title: 'Ollama',
|
||||
}
|
||||
return { title: 'Ollama', }
|
||||
}
|
||||
else if (providerName === 'vLLM') {
|
||||
return {
|
||||
title: 'vLLM',
|
||||
}
|
||||
return { title: 'vLLM', }
|
||||
}
|
||||
else if (providerName === 'liteLLM') {
|
||||
return { title: 'LiteLLM', }
|
||||
}
|
||||
else if (providerName === 'lmStudio') {
|
||||
return { title: 'LM Studio', }
|
||||
}
|
||||
else if (providerName === 'openAICompatible') {
|
||||
return {
|
||||
title: 'OpenAI-Compatible',
|
||||
}
|
||||
return { title: 'OpenAI-Compatible', }
|
||||
}
|
||||
else if (providerName === 'gemini') {
|
||||
return {
|
||||
// title: 'Gemini API',
|
||||
title: 'Gemini',
|
||||
}
|
||||
return { title: 'Gemini', }
|
||||
}
|
||||
else if (providerName === 'groq') {
|
||||
return {
|
||||
// title: 'Groq.com API',
|
||||
title: 'Groq',
|
||||
}
|
||||
return { title: 'Groq', }
|
||||
}
|
||||
else if (providerName === 'xAI') {
|
||||
return {
|
||||
// title: 'Grok (xAI)',
|
||||
title: 'xAI',
|
||||
}
|
||||
return { title: 'xAI', }
|
||||
}
|
||||
else if (providerName === 'mistral') {
|
||||
return {
|
||||
// title: 'Mistral API',
|
||||
title: 'Mistral',
|
||||
}
|
||||
return { 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}"`)
|
||||
}
|
||||
|
||||
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 = {
|
||||
title: string;
|
||||
placeholder: string;
|
||||
subTextMd?: string;
|
||||
isPasswordField?: boolean;
|
||||
}
|
||||
export const displayInfoOfSettingName = (providerName: ProviderName, settingName: SettingName): DisplayInfo => {
|
||||
|
|
@ -140,23 +143,15 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
|
|||
providerName === 'openAI' ? 'sk-proj-key...' :
|
||||
providerName === 'deepseek' ? 'sk-key...' :
|
||||
providerName === 'openRouter' ? 'sk-or-key...' : // sk-or-v1-key
|
||||
providerName === 'gemini' ? 'key...' :
|
||||
providerName === 'gemini' ? 'AIzaSy...' :
|
||||
providerName === 'groq' ? 'gsk_key...' :
|
||||
providerName === 'openAICompatible' ? 'sk-key...' :
|
||||
providerName === 'xAI' ? 'xai-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,
|
||||
}
|
||||
}
|
||||
|
|
@ -164,19 +159,51 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
|
|||
return {
|
||||
title: providerName === 'ollama' ? 'Endpoint' :
|
||||
providerName === 'vLLM' ? 'Endpoint' :
|
||||
providerName === 'openAICompatible' ? 'baseURL' : // (do not include /chat/completions)
|
||||
'(never)',
|
||||
providerName === 'lmStudio' ? 'Endpoint' :
|
||||
providerName === 'openAICompatible' ? 'baseURL' : // (do not include /chat/completions)
|
||||
providerName === 'googleVertex' ? 'baseURL' :
|
||||
providerName === 'microsoftAzure' ? 'baseURL' :
|
||||
providerName === 'liteLLM' ? 'baseURL' :
|
||||
'(never)',
|
||||
|
||||
placeholder: providerName === 'ollama' ? defaultProviderSettings.ollama.endpoint
|
||||
: providerName === 'vLLM' ? defaultProviderSettings.vLLM.endpoint
|
||||
: 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') {
|
||||
return {
|
||||
title: '(never)',
|
||||
|
|
@ -200,6 +227,9 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
|
|||
const defaultCustomSettings: Record<CustomSettingName, undefined> = {
|
||||
apiKey: undefined,
|
||||
endpoint: undefined,
|
||||
region: undefined,
|
||||
project: undefined,
|
||||
azureApiVersion: undefined,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -207,8 +237,7 @@ const modelInfoOfDefaultModelNames = (defaultModelNames: string[]): { models: Vo
|
|||
return {
|
||||
models: defaultModelNames.map((modelName, i) => ({
|
||||
modelName,
|
||||
isDefault: true,
|
||||
isAutodetected: false,
|
||||
type: 'default',
|
||||
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),
|
||||
_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)
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.groq,
|
||||
|
|
@ -282,6 +323,18 @@ export const defaultSettingsOfProvider: SettingsOfProvider = {
|
|||
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.vLLM),
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
googleVertex: { // aggregator (serves models from multiple providers)
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.googleVertex,
|
||||
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.googleVertex),
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
microsoftAzure: { // aggregator (serves models from multiple providers)
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.microsoftAzure,
|
||||
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.microsoftAzure),
|
||||
_didFillInProviderSettings: undefined,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { Ollama } from 'ollama';
|
|||
import OpenAI, { ClientOptions } from 'openai';
|
||||
import { MistralCore } from '@mistralai/mistralai/core.js';
|
||||
import { fimComplete } from '@mistralai/mistralai/funcs/fimComplete.js';
|
||||
import { GoogleAuth } from 'google-auth-library'
|
||||
/* eslint-enable */
|
||||
|
||||
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';
|
||||
|
||||
|
||||
|
||||
|
||||
type InternalCommonMessageParams = {
|
||||
onText: OnText;
|
||||
onFinalMessage: OnFinalMessage;
|
||||
|
|
@ -39,7 +42,16 @@ const invalidApiKeyMessage = (providerName: ProviderName) => `Invalid ${displayI
|
|||
|
||||
// ------------ OPENAI-COMPATIBLE (HELPERS) ------------
|
||||
|
||||
const newOpenAICompatibleSDK = ({ settingsOfProvider, providerName, includeInPayload }: { settingsOfProvider: SettingsOfProvider, providerName: ProviderName, includeInPayload?: { [s: string]: any } }) => {
|
||||
const getGoogleApiKey = async () => {
|
||||
// module‑level singleton
|
||||
const auth = new GoogleAuth({ scopes: `https://www.googleapis.com/auth/cloud-platform` });
|
||||
const key = await auth.getAccessToken()
|
||||
if (!key) throw new Error(`Google API failed to generate a key.`)
|
||||
return key
|
||||
}
|
||||
|
||||
|
||||
const newOpenAICompatibleSDK = async ({ settingsOfProvider, providerName, includeInPayload }: { settingsOfProvider: SettingsOfProvider, providerName: ProviderName, includeInPayload?: { [s: string]: any } }) => {
|
||||
const commonPayloadOpts: ClientOptions = {
|
||||
dangerouslyAllowBrowser: true,
|
||||
...includeInPayload,
|
||||
|
|
@ -56,6 +68,14 @@ const newOpenAICompatibleSDK = ({ settingsOfProvider, providerName, includeInPay
|
|||
const thisConfig = settingsOfProvider[providerName]
|
||||
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') {
|
||||
const thisConfig = settingsOfProvider[providerName]
|
||||
return new OpenAI({
|
||||
|
|
@ -70,8 +90,22 @@ const newOpenAICompatibleSDK = ({ settingsOfProvider, providerName, includeInPay
|
|||
}
|
||||
else if (providerName === 'gemini') {
|
||||
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') {
|
||||
const thisConfig = settingsOfProvider[providerName]
|
||||
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_)
|
||||
if (!supportsFIM) {
|
||||
if (modelName === modelName_)
|
||||
|
|
@ -107,7 +141,7 @@ const _sendOpenAICompatibleFIM = ({ messages: { prefix, suffix, stopTokens }, on
|
|||
return
|
||||
}
|
||||
|
||||
const openai = newOpenAICompatibleSDK({ providerName, settingsOfProvider })
|
||||
const openai = await newOpenAICompatibleSDK({ providerName, settingsOfProvider })
|
||||
openai.completions
|
||||
.create({
|
||||
model: modelName,
|
||||
|
|
@ -178,7 +212,7 @@ const openAIToolToRawToolCallObj = (name: string, toolParamsStr: string, id: str
|
|||
// ------------ 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 {
|
||||
modelName,
|
||||
specialToolFormat,
|
||||
|
|
@ -199,7 +233,7 @@ const _sendOpenAICompatibleChat = ({ messages, onText, onFinalMessage, onError,
|
|||
: {}
|
||||
|
||||
// instance
|
||||
const openai: OpenAI = newOpenAICompatibleSDK({ providerName, settingsOfProvider, includeInPayload })
|
||||
const openai: OpenAI = await newOpenAICompatibleSDK({ providerName, settingsOfProvider, includeInPayload })
|
||||
const options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = {
|
||||
model: modelName,
|
||||
messages: messages as any,
|
||||
|
|
@ -300,7 +334,7 @@ const _openaiCompatibleList = async ({ onSuccess: onSuccess_, onError: onError_,
|
|||
onError_({ error })
|
||||
}
|
||||
try {
|
||||
const openai = newOpenAICompatibleSDK({ providerName, settingsOfProvider })
|
||||
const openai = await newOpenAICompatibleSDK({ providerName, settingsOfProvider })
|
||||
openai.models.list()
|
||||
.then(async (response) => {
|
||||
const models: OpenAIModel[] = []
|
||||
|
|
@ -360,7 +394,7 @@ const anthropicToolToRawToolCallObj = (toolBlock: Anthropic.Messages.ToolUseBloc
|
|||
}
|
||||
|
||||
// ------------ 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 {
|
||||
modelName,
|
||||
specialToolFormat,
|
||||
|
|
@ -505,6 +539,7 @@ const sendMistralFIM = ({ messages, onFinalMessage, onError, settingsOfProvider,
|
|||
stop: messages.stopTokens,
|
||||
})
|
||||
.then(async response => {
|
||||
// unfortunately, _setAborter() does not exist
|
||||
let content = response?.ok ? response.value.choices?.[0]?.message?.content ?? '' : '';
|
||||
const fullText = typeof content === 'string' ? content
|
||||
: content.map(chunk => (chunk.type === 'text' ? chunk.text : '')).join('')
|
||||
|
|
@ -584,7 +619,7 @@ const sendOllamaFIM = ({ messages, onFinalMessage, onError, settingsOfProvider,
|
|||
|
||||
type CallFnOfProvider = {
|
||||
[providerName in ProviderName]: {
|
||||
sendChat: (params: SendChatParams_Internal) => void;
|
||||
sendChat: (params: SendChatParams_Internal) => Promise<void>;
|
||||
sendFIM: ((params: SendFIMParams_Internal) => void) | null;
|
||||
list: ((params: ListParams_Internal<any>) => void) | null;
|
||||
}
|
||||
|
|
@ -646,6 +681,27 @@ export const sendLLMMessageToProviderImplementation = {
|
|||
sendFIM: 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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { displayInfoOfProviderName } from '../../common/voidSettingsTypes.js';
|
|||
import { sendLLMMessageToProviderImplementation } from './sendLLMMessage.impl.js';
|
||||
|
||||
|
||||
export const sendLLMMessage = ({
|
||||
export const sendLLMMessage = async ({
|
||||
messagesType,
|
||||
messages: messages_,
|
||||
onText: onText_,
|
||||
|
|
@ -108,12 +108,12 @@ export const sendLLMMessage = ({
|
|||
}
|
||||
const { sendFIM, sendChat } = implementation
|
||||
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
|
||||
}
|
||||
if (messagesType === 'FIMMessage') {
|
||||
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
|
||||
}
|
||||
onError({ message: `Error: This provider does not support Autocomplete yet.`, fullError: null })
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import { IServerChannel } from '../../../../base/parts/ipc/common/ipc.js';
|
||||
import { 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 { IMetricsService } from '../common/metricsService.js';
|
||||
import { sendLLMMessageToProviderImplementation } from './llmMessage/sendLLMMessage.impl.js';
|
||||
|
|
@ -25,7 +25,7 @@ export class LLMMessageChannel implements IServerChannel {
|
|||
}
|
||||
|
||||
// aborters for above
|
||||
private readonly abortRefOfRequestId: Record<string, AbortRef> = {}
|
||||
private readonly _infoOfRunningRequest: Record<string, { waitForSend: Promise<void> | undefined, abortRef: AbortRef }> = {}
|
||||
|
||||
|
||||
// list
|
||||
|
|
@ -34,12 +34,12 @@ export class LLMMessageChannel implements IServerChannel {
|
|||
success: new Emitter<EventModelListOnSuccessParams<OllamaModelResponse>>(),
|
||||
error: new Emitter<EventModelListOnErrorParams<OllamaModelResponse>>(),
|
||||
},
|
||||
vLLM: {
|
||||
success: new Emitter<EventModelListOnSuccessParams<VLLMModelResponse>>(),
|
||||
error: new Emitter<EventModelListOnErrorParams<VLLMModelResponse>>(),
|
||||
}
|
||||
openaiCompat: {
|
||||
success: new Emitter<EventModelListOnSuccessParams<OpenaiCompatibleModelResponse>>(),
|
||||
error: new Emitter<EventModelListOnErrorParams<OpenaiCompatibleModelResponse>>(),
|
||||
},
|
||||
} satisfies {
|
||||
[providerName: string]: {
|
||||
[providerName in 'ollama' | 'openaiCompat']: {
|
||||
success: Emitter<EventModelListOnSuccessParams<any>>,
|
||||
error: Emitter<EventModelListOnErrorParams<any>>,
|
||||
}
|
||||
|
|
@ -59,8 +59,8 @@ export class LLMMessageChannel implements IServerChannel {
|
|||
// list
|
||||
else if (event === 'onSuccess_list_ollama') return this.listEmitters.ollama.success.event;
|
||||
else if (event === 'onError_list_ollama') return this.listEmitters.ollama.error.event;
|
||||
else if (event === 'onSuccess_list_vLLM') return this.listEmitters.vLLM.success.event;
|
||||
else if (event === 'onError_list_vLLM') return this.listEmitters.vLLM.error.event;
|
||||
else if (event === 'onSuccess_list_openAICompatible') return this.listEmitters.openaiCompat.success.event;
|
||||
else if (event === 'onError_list_openAICompatible') return this.listEmitters.openaiCompat.error.event;
|
||||
|
||||
else throw new Error(`Event not found: ${event}`);
|
||||
}
|
||||
|
|
@ -72,13 +72,13 @@ export class LLMMessageChannel implements IServerChannel {
|
|||
this._callSendLLMMessage(params)
|
||||
}
|
||||
else if (command === 'abort') {
|
||||
this._callAbort(params)
|
||||
await this._callAbort(params)
|
||||
}
|
||||
else if (command === 'ollamaList') {
|
||||
this._callOllamaList(params)
|
||||
}
|
||||
else if (command === 'vLLMList') {
|
||||
this._callVLLMList(params)
|
||||
else if (command === 'openAICompatibleList') {
|
||||
this._callOpenAICompatibleList(params)
|
||||
}
|
||||
else {
|
||||
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
|
||||
private async _callSendLLMMessage(params: MainSendLLMMessageParams) {
|
||||
private _callSendLLMMessage(params: MainSendLLMMessageParams) {
|
||||
const { requestId } = params;
|
||||
|
||||
if (!(requestId in this.abortRefOfRequestId))
|
||||
this.abortRefOfRequestId[requestId] = { current: null }
|
||||
if (!(requestId in this._infoOfRunningRequest))
|
||||
this._infoOfRunningRequest[requestId] = { waitForSend: undefined, abortRef: { current: null } }
|
||||
|
||||
const mainThreadParams: SendLLMMessageParams = {
|
||||
...params,
|
||||
onText: (p) => { this.llmMessageEmitters.onText.fire({ requestId, ...p }); },
|
||||
onFinalMessage: (p) => { this.llmMessageEmitters.onFinalMessage.fire({ requestId, ...p }); },
|
||||
onError: (p) => { console.log('sendLLM: firing err'); this.llmMessageEmitters.onError.fire({ requestId, ...p }); },
|
||||
abortRef: this.abortRefOfRequestId[requestId],
|
||||
onText: (p) => {
|
||||
this.llmMessageEmitters.onText.fire({ requestId, ...p });
|
||||
},
|
||||
onFinalMessage: (p) => {
|
||||
this.llmMessageEmitters.onFinalMessage.fire({ requestId, ...p });
|
||||
},
|
||||
onError: (p) => {
|
||||
console.log('sendLLM: firing err');
|
||||
this.llmMessageEmitters.onError.fire({ requestId, ...p });
|
||||
},
|
||||
abortRef: this._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;
|
||||
if (!(requestId in this.abortRefOfRequestId)) return
|
||||
this.abortRefOfRequestId[requestId].current?.()
|
||||
delete this.abortRefOfRequestId[requestId]
|
||||
if (!(requestId in this._infoOfRunningRequest)) return
|
||||
const { waitForSend, abortRef } = this._infoOfRunningRequest[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)
|
||||
}
|
||||
|
||||
_callVLLMList = (params: MainModelListParams<VLLMModelResponse>) => {
|
||||
const { requestId } = params
|
||||
const emitters = this.listEmitters.vLLM
|
||||
const mainThreadParams: ModelListParams<VLLMModelResponse> = {
|
||||
_callOpenAICompatibleList = (params: MainModelListParams<OpenaiCompatibleModelResponse>) => {
|
||||
const { requestId, providerName } = params
|
||||
const emitters = this.listEmitters.openaiCompat
|
||||
const mainThreadParams: ModelListParams<OpenaiCompatibleModelResponse> = {
|
||||
...params,
|
||||
onSuccess: (p) => { emitters.success.fire({ requestId, ...p }); },
|
||||
onError: (p) => { emitters.error.fire({ requestId, ...p }); },
|
||||
}
|
||||
sendLLMMessageToProviderImplementation.vLLM.list(mainThreadParams)
|
||||
sendLLMMessageToProviderImplementation[providerName].list(mainThreadParams)
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue