model and provider error handling

This commit is contained in:
Andrew Pareles 2025-02-06 01:32:31 -08:00
parent 1be9300dc4
commit 2f49650e15
17 changed files with 296 additions and 224 deletions

View file

@ -12,7 +12,7 @@ import { createDecorator } from '../../instantiation/common/instantiation.js';
import { Event } from '../../../base/common/event.js';
import { Disposable } from '../../../base/common/lifecycle.js';
import { IVoidSettingsService } from './voidSettingsService.js';
import { getProvidersWithoutModels } from './voidSettingsTypes.js';
import { displayInfoOfProviderName, isFeatureNameDisabled } from './voidSettingsTypes.js';
// import { INotificationService } from '../../notification/common/notification.js';
// calls channel to implement features
@ -91,19 +91,24 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
const { onText, onFinalMessage, onError, ...proxyParams } = params;
const { useProviderFor: featureName } = proxyParams
// end early if no provider
// throw an error if no model/provider selected (this should usually never be reached, the UI should check this first, but might happen in cases like Apply where we haven't built much UI/checks yet, good practice to have check logic on backend)
const isDisabled = isFeatureNameDisabled(featureName, this.voidSettingsService.state)
const modelSelection = this.voidSettingsService.state.modelSelectionOfFeature[featureName]
if (isDisabled || modelSelection === null) {
let message: string
// throw an error for providers without models
const providersWithoutModels = getProvidersWithoutModels(this.voidSettingsService.state.settingsOfProvider)
if (providersWithoutModels.length !== 0) {
onError({ message: `You haven't added any models for ${providersWithoutModels.join(', ')}.`, fullError: null })
return null
}
if (isDisabled === 'addProvider' || isDisabled === 'providerNotAutoDetected')
message = `Please add a provider in Void Settings.`
else if (isDisabled === 'addModel')
message = `Please add a model.`
else if (isDisabled === 'needToEnableModel')
message = `Please enable a model.`
else if (isDisabled === 'notFilledIn')
message = `Please fill in Void Settings${modelSelection !== null ? ` for ${displayInfoOfProviderName(modelSelection.providerName).title}` : ''}.`
else
message = 'Please add a provider in Void Settings.'
// throw an error if no models
if (modelSelection === null) {
onError({ message: 'Please add a Provider in Settings!', fullError: null })
onError({ message, fullError: null })
return null
}

View file

@ -11,6 +11,7 @@ export const errorDetails = (fullError: Error | null): string | null => {
return null
}
else if (typeof fullError === 'object') {
if (Object.keys(fullError).length === 0) return null
return JSON.stringify(fullError, null, 2)
}
else if (typeof fullError === 'string') {
@ -41,10 +42,10 @@ type _InternalSendFIMMessage = {
}
type SendLLMType = {
type: 'sendChatMessage';
messagesType: 'chatMessages';
messages: LLMChatMessage[];
} | {
type: 'sendFIMMessage';
messagesType: 'FIMMessage';
messages: _InternalSendFIMMessage;
}

View file

@ -44,8 +44,8 @@ export type RefreshModelStateOfProvider = Record<RefreshableProviderName, Refres
const refreshBasedOn: { [k in RefreshableProviderName]: (keyof SettingsOfProvider[k])[] } = {
ollama: ['_enabled', 'endpoint'],
// openAICompatible: ['_enabled', 'endpoint', 'apiKey'],
ollama: ['_didFillInProviderSettings', 'endpoint'],
// openAICompatible: ['_didFillInProviderSettings', 'endpoint', 'apiKey'],
}
const REFRESH_INTERVAL = 5_000
// const COOLDOWN_TIMEOUT = 300
@ -95,7 +95,7 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
for (const providerName of refreshableProviderNames) {
// const { _enabled: enabled } = this.voidSettingsService.state.settingsOfProvider[providerName]
// const { '_didFillInProviderSettings': enabled } = this.voidSettingsService.state.settingsOfProvider[providerName]
this.startRefreshingModels(providerName, autoOptions)
// every time providerName.enabled changes, refresh models too, like a useEffect
@ -175,7 +175,7 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
{ enableProviderOnSuccess: options.enableProviderOnSuccess, hideRefresh: options.doNotFire }
)
if (options.enableProviderOnSuccess) this.voidSettingsService.setSettingOfProvider(providerName, '_enabled', true)
if (options.enableProviderOnSuccess) this.voidSettingsService.setSettingOfProvider(providerName, '_didFillInProviderSettings', true)
this._setRefreshState(providerName, 'finished', options)
autoPoll()

View file

@ -11,7 +11,7 @@ import { registerSingleton, InstantiationType } from '../../instantiation/common
import { createDecorator } from '../../instantiation/common/instantiation.js';
import { IStorageService, StorageScope, StorageTarget } from '../../storage/common/storage.js';
import { IMetricsService } from './metricsService.js';
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, modelInfoOfDefaultModelNames, VoidModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings } from './voidSettingsTypes.js';
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, modelInfoOfDefaultModelNames, VoidModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, displayInfoOfProviderName, defaultProviderSettings } from './voidSettingsTypes.js';
const STORAGE_KEY = 'void.settingsServiceStorage'
@ -42,8 +42,8 @@ export type VoidSettingsState = {
readonly _modelOptions: ModelOption[] // computed based on the two above items
}
type RealVoidSettings = Exclude<keyof VoidSettingsState, '_modelOptions'>
type EventProp<T extends RealVoidSettings = RealVoidSettings> = T extends 'globalSettings' ? [T, keyof VoidSettingsState[T]] : T | 'all'
// type RealVoidSettings = Exclude<keyof VoidSettingsState, '_modelOptions'>
// type EventProp<T extends RealVoidSettings = RealVoidSettings> = T extends 'globalSettings' ? [T, keyof VoidSettingsState[T]] : T | 'all'
export interface IVoidSettingsService {
@ -51,7 +51,7 @@ export interface IVoidSettingsService {
readonly state: VoidSettingsState; // in order to play nicely with react, you should immutably change state
readonly waitForInitState: Promise<void>;
onDidChangeState: Event<EventProp>;
onDidChangeState: Event<void>;
setSettingOfProvider: SetSettingOfProviderFn;
setModelSelectionOfFeature: SetModelSelectionOfFeatureFn;
@ -64,26 +64,76 @@ export interface IVoidSettingsService {
}
let _computeModelOptions = (settingsOfProvider: SettingsOfProvider) => {
let modelOptions: ModelOption[] = []
const _updatedValidatedState = (state: Omit<VoidSettingsState, '_modelOptions'>) => {
let newSettingsOfProvider = state.settingsOfProvider
// recompute _didFillInProviderSettings
for (const providerName of providerNames) {
const providerConfig = settingsOfProvider[providerName]
if (!providerConfig._enabled) continue // if disabled, don't display model options
for (const { modelName, isHidden } of providerConfig.models) {
if (isHidden) continue
modelOptions.push({ name: `${modelName} (${providerName})`, selection: { providerName, modelName } })
const settingsAtProvider = newSettingsOfProvider[providerName]
const didFillInProviderSettings = Object.keys(defaultProviderSettings[providerName]).every(key => !!settingsAtProvider[key as keyof typeof settingsAtProvider])
if (didFillInProviderSettings === settingsAtProvider._didFillInProviderSettings) continue
newSettingsOfProvider = {
...newSettingsOfProvider,
[providerName]: {
...settingsAtProvider,
_didFillInProviderSettings: didFillInProviderSettings,
},
}
}
return modelOptions
// update model options
let newModelOptions: ModelOption[] = []
for (const providerName of providerNames) {
const providerTitle = displayInfoOfProviderName(providerName).title.toLowerCase() // looks better lowercase, best practice to not use raw providerName
if (!newSettingsOfProvider[providerName]._didFillInProviderSettings) continue // if disabled, don't display model options
for (const { modelName, isHidden } of newSettingsOfProvider[providerName].models) {
if (isHidden) continue
newModelOptions.push({ name: `${modelName} (${providerTitle})`, selection: { providerName, modelName } })
}
}
// now that model options are updated, make sure the selection is valid
// if the user-selected model is no longer in the list, update the selection for each feature that needs it to something relevant (the 0th model available, or null)
let newModelSelectionOfFeature = state.modelSelectionOfFeature
for (const featureName of featureNames) {
const modelSelectionAtFeature = newModelSelectionOfFeature[featureName]
const selnIdx = modelSelectionAtFeature === null ? -1 : newModelOptions.findIndex(m => modelSelectionsEqual(m.selection, modelSelectionAtFeature))
if (selnIdx !== -1) continue
newModelSelectionOfFeature = {
...newModelSelectionOfFeature,
[featureName]: newModelOptions.length === 0 ? null : newModelOptions[0].selection
}
}
const newState = {
...state,
settingsOfProvider: newSettingsOfProvider,
modelSelectionOfFeature: newModelSelectionOfFeature,
_modelOptions: newModelOptions,
} satisfies VoidSettingsState
return newState
}
const defaultState = () => {
const d: VoidSettingsState = {
settingsOfProvider: deepClone(defaultSettingsOfProvider),
modelSelectionOfFeature: { 'Ctrl+L': null, 'Ctrl+K': null, 'Autocomplete': null, 'FastApply': null },
globalSettings: deepClone(defaultGlobalSettings),
_modelOptions: _computeModelOptions(defaultSettingsOfProvider), // computed
_modelOptions: [], // computed later
}
return d
}
@ -93,8 +143,8 @@ export const IVoidSettingsService = createDecorator<IVoidSettingsService>('VoidS
class VoidSettingsService extends Disposable implements IVoidSettingsService {
_serviceBrand: undefined;
private readonly _onDidChangeState = new Emitter<EventProp>();
readonly onDidChangeState: Event<EventProp> = this._onDidChangeState.event; // this is primarily for use in react, so react can listen + update on state changes
private readonly _onDidChangeState = new Emitter<void>();
readonly onDidChangeState: Event<void> = this._onDidChangeState.event; // this is primarily for use in react, so react can listen + update on state changes
state: VoidSettingsState;
waitForInitState: Promise<void> // await this if you need a valid state initially
@ -118,39 +168,47 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
this._readState().then(readS => {
// the stored data structure might be outdated, so we need to update it here (can do a more general solution later when we need to)
readS = {
...readS,
settingsOfProvider: {
// A HACK BECAUSE WE ADDED DEEPSEEK (did not exist before, comes before readS)
...{ deepseek: defaultSettingsOfProvider.deepseek },
const newSettingsOfProvider = {
// A HACK BECAUSE WE ADDED DEEPSEEK (did not exist before, comes before readS)
...{ deepseek: defaultSettingsOfProvider.deepseek },
// A HACK BECAUSE WE ADDED MISTRAL (did not exist before, comes before readS)
...{ mistral: defaultSettingsOfProvider.mistral },
// A HACK BECAUSE WE ADDED MISTRAL (did not exist before, comes before readS)
...{ mistral: defaultSettingsOfProvider.mistral },
...readS.settingsOfProvider,
...readS.settingsOfProvider,
// A HACK BECAUSE WE ADDED NEW GEMINI MODELS (existed before, comes after readS)
gemini: {
...readS.settingsOfProvider.gemini,
models: [
...readS.settingsOfProvider.gemini.models,
...defaultSettingsOfProvider.gemini.models.filter(m => /* if cant find the model in readS (yes this is O(n^2), very small) */ !readS.settingsOfProvider.gemini.models.find(m2 => m2.modelName === m.modelName))
]
}
},
modelSelectionOfFeature: {
// A HACK BECAUSE WE ADDED FastApply
...{ 'FastApply': null },
...readS.modelSelectionOfFeature,
// A HACK BECAUSE WE ADDED NEW GEMINI MODELS (existed before, comes after readS)
gemini: {
...readS.settingsOfProvider.gemini,
models: [
...readS.settingsOfProvider.gemini.models,
...defaultSettingsOfProvider.gemini.models.filter(m => /* if cant find the model in readS (yes this is O(n^2), very small) */ !readS.settingsOfProvider.gemini.models.find(m2 => m2.modelName === m.modelName))
]
}
}
this.state = readS
const newModelSelectionOfFeature = {
// A HACK BECAUSE WE ADDED FastApply
...{ 'FastApply': null },
...readS.modelSelectionOfFeature,
}
readS = {
...readS,
settingsOfProvider: newSettingsOfProvider,
modelSelectionOfFeature: newModelSelectionOfFeature,
}
this.state = _updatedValidatedState(readS)
resolver()
this._onDidChangeState.fire('all')
this._onDidChangeState.fire()
})
}
private async _readState(): Promise<VoidSettingsState> {
const encryptedState = this._storageService.get(STORAGE_KEY, StorageScope.APPLICATION)
@ -172,7 +230,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
const newModelSelectionOfFeature = this.state.modelSelectionOfFeature
const newSettingsOfProvider = {
const newSettingsOfProvider: SettingsOfProvider = {
...this.state.settingsOfProvider,
[providerName]: {
...this.state.settingsOfProvider[providerName],
@ -182,38 +240,17 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
const newGlobalSettings = this.state.globalSettings
// if changed models or enabled a provider, recompute models list
const modelsListChanged = settingName === 'models' || settingName === '_enabled'
const newModelsList = modelsListChanged ? _computeModelOptions(newSettingsOfProvider) : this.state._modelOptions
const newState: VoidSettingsState = {
const newState = {
modelSelectionOfFeature: newModelSelectionOfFeature,
settingsOfProvider: newSettingsOfProvider,
globalSettings: newGlobalSettings,
_modelOptions: newModelsList,
}
// this must go above this.setanythingelse()
this.state = newState
// if the user-selected model is no longer in the list, update the selection for each feature that needs it to something relevant (the 0th model available, or null)
if (modelsListChanged) {
for (const featureName of featureNames) {
const currentSelection = newModelSelectionOfFeature[featureName]
const selnIdx = currentSelection === null ? -1 : newModelsList.findIndex(m => modelSelectionsEqual(m.selection, currentSelection))
if (selnIdx === -1) {
if (newModelsList.length !== 0)
this.setModelSelectionOfFeature(featureName, newModelsList[0].selection, { doNotApplyEffects: true })
else
this.setModelSelectionOfFeature(featureName, null, { doNotApplyEffects: true })
}
}
}
this.state = _updatedValidatedState(newState)
await this._storeState()
this._onDidChangeState.fire('settingsOfProvider')
this._onDidChangeState.fire()
}
@ -227,7 +264,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
}
this.state = newState
await this._storeState()
this._onDidChangeState.fire(['globalSettings', settingName])
this._onDidChangeState.fire()
}
@ -247,7 +284,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
return
await this._storeState()
this._onDidChangeState.fire('modelSelectionOfFeature')
this._onDidChangeState.fire()
}

View file

@ -4,7 +4,7 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { VoidSettingsState } from './voidSettingsService.js'
export type VoidModelInfo = {
@ -186,28 +186,23 @@ export const customSettingNamesOfProvider = (providerName: ProviderName) => {
return Object.keys(defaultProviderSettings[providerName]) as CustomSettingName[]
}
export const getProvidersWithoutModels = (settingsOfProvider: SettingsOfProvider) => {
return Object.entries(settingsOfProvider)
.filter(([name, provider]) => provider._enabled && provider.models.length === 0)
.map(([name]) => name)
}
type CommonProviderSettings = {
_enabled: boolean | undefined, // undefined initially, computed when user types in all fields
_didFillInProviderSettings: boolean | undefined, // undefined initially, computed when user types in all fields
models: VoidModelInfo[],
}
export type SettingsForProvider<providerName extends ProviderName> = CustomProviderSettings<providerName> & CommonProviderSettings
export type SettingsAtProvider<providerName extends ProviderName> = CustomProviderSettings<providerName> & CommonProviderSettings
// part of state
export type SettingsOfProvider = {
[providerName in ProviderName]: SettingsForProvider<providerName>
[providerName in ProviderName]: SettingsAtProvider<providerName>
}
export type SettingName = keyof SettingsForProvider<ProviderName>
export type SettingName = keyof SettingsAtProvider<ProviderName>
@ -316,7 +311,7 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
undefined,
}
}
else if (settingName === '_enabled') {
else if (settingName === '_didFillInProviderSettings') {
return {
title: '(never)',
placeholder: '(never)',
@ -380,55 +375,55 @@ export const defaultSettingsOfProvider: SettingsOfProvider = {
...defaultCustomSettings,
...defaultProviderSettings.anthropic,
...voidInitModelOptions.anthropic,
_enabled: undefined,
_didFillInProviderSettings: undefined,
},
openAI: {
...defaultCustomSettings,
...defaultProviderSettings.openAI,
...voidInitModelOptions.openAI,
_enabled: undefined,
_didFillInProviderSettings: undefined,
},
deepseek: {
...defaultCustomSettings,
...defaultProviderSettings.deepseek,
...voidInitModelOptions.deepseek,
_enabled: undefined,
_didFillInProviderSettings: undefined,
},
gemini: {
...defaultCustomSettings,
...defaultProviderSettings.gemini,
...voidInitModelOptions.gemini,
_enabled: undefined,
_didFillInProviderSettings: undefined,
},
mistral: {
...defaultCustomSettings,
...defaultProviderSettings.mistral,
...voidInitModelOptions.mistral,
_enabled: undefined,
_didFillInProviderSettings: undefined,
},
groq: { // aggregator
...defaultCustomSettings,
...defaultProviderSettings.groq,
...voidInitModelOptions.groq,
_enabled: undefined,
_didFillInProviderSettings: undefined,
},
openRouter: { // aggregator
...defaultCustomSettings,
...defaultProviderSettings.openRouter,
...voidInitModelOptions.openRouter,
_enabled: undefined,
_didFillInProviderSettings: undefined,
},
openAICompatible: { // aggregator
...defaultCustomSettings,
...defaultProviderSettings.openAICompatible,
...voidInitModelOptions.openAICompatible,
_enabled: undefined,
_didFillInProviderSettings: undefined,
},
ollama: { // aggregator
...defaultCustomSettings,
...defaultProviderSettings.ollama,
...voidInitModelOptions.ollama,
_enabled: undefined,
_didFillInProviderSettings: undefined,
},
}
@ -458,10 +453,6 @@ export const displayInfoOfFeatureName = (featureName: FeatureName) => {
}
// the models of these can be refreshed (in theory all can, but not all should)
export const refreshableProviderNames = localProviderNames
export type RefreshableProviderName = typeof refreshableProviderNames[number]
@ -471,6 +462,45 @@ export type RefreshableProviderName = typeof refreshableProviderNames[number]
// use this in isFeatuerNameDissbled
export const isProviderNameDisabled = (providerName: ProviderName, settingsState: VoidSettingsState) => {
const settingsAtProvider = settingsState.settingsOfProvider[providerName]
const isAutodetected = (refreshableProviderNames as string[]).includes(providerName)
const isDisabled = settingsAtProvider.models.length === 0
if (isDisabled) {
return isAutodetected ? 'providerNotAutoDetected' : (!settingsAtProvider._didFillInProviderSettings ? 'notFilledIn' : 'addModel')
}
return false
}
export const isFeatureNameDisabled = (featureName: FeatureName, settingsState: VoidSettingsState) => {
// if has a selected provider, check if it's enabled
const selectedProvider = settingsState.modelSelectionOfFeature[featureName]
if (selectedProvider) {
const { providerName } = selectedProvider
return isProviderNameDisabled(providerName, settingsState)
}
// if there are any models they can turn on, tell them that
const canTurnOnAModel = !!providerNames.find(providerName => settingsState.settingsOfProvider[providerName].models.filter(m => m.isHidden).length !== 0)
if (canTurnOnAModel) return 'needToEnableModel'
// if there are any providers filled in, then they just need to add a model
const anyFilledIn = !!providerNames.find(providerName => settingsState.settingsOfProvider[providerName]._didFillInProviderSettings)
if (anyFilledIn) return 'addModel'
return 'addProvider'
}
export type GlobalSettings = {

View file

@ -99,6 +99,7 @@ export const sendOpenAIFIM: _InternalSendLLMFIMMessageFnType = ({ messages, onTe
.create(options)
.then(async response => {
// TODO!!!
console.log('RESPONSE', response)
})
}

View file

@ -12,6 +12,7 @@ import { sendOpenAIChat } from './openai.js';
import { sendGeminiChat } from './gemini.js';
import { sendGroqChat } from './groq.js';
import { sendMistralChat } from './mistral.js';
import { displayInfoOfProviderName } from '../../common/voidSettingsTypes.js';
const cleanChatMessages = (messages: LLMChatMessage[]): _InternalLLMChatMessage[] => {
@ -49,7 +50,7 @@ const cleanChatMessages = (messages: LLMChatMessage[]): _InternalLLMChatMessage[
export const sendLLMMessage = ({
type,
messagesType,
aiInstructions,
messages: messages_,
onText: onText_,
@ -66,20 +67,20 @@ export const sendLLMMessage = ({
) => {
// messages.unshift({ role: 'system', content: aiInstructions })
const messagesArr = type === 'sendChatMessage' ? cleanChatMessages(messages_) : []
const messagesArr = messagesType === 'chatMessages' ? cleanChatMessages(messages_) : []
// only captures number of messages and message "shape", no actual code, instructions, prompts, etc
const captureLLMEvent = (eventId: string, extras?: object) => {
metricsService.capture(eventId, {
providerName,
modelName,
...type === 'sendChatMessage' ? {
...messagesType === 'chatMessages' ? {
numMessages: messagesArr?.length,
messagesShape: messagesArr?.map(msg => ({ role: msg.role, length: msg.content.length })),
origNumMessages: messages_?.length,
origMessagesShape: messages_?.map(msg => ({ role: msg.role, length: msg.content.length })),
} : type === 'sendFIMMessage' ? {
} : messagesType === 'FIMMessage' ? {
prefixLength: messages_.prefix.length,
suffixLength: messages_.suffix.length,
} : {},
@ -109,6 +110,11 @@ export const sendLLMMessage = ({
const onError: OnError = ({ message: error, fullError }) => {
if (_didAbort) return
console.error('sendLLMMessage onError:', error)
// handle failed to fetch errors, which give 0 information by design
if (error === 'TypeError: fetch failed')
error = `Failed to fetch from ${displayInfoOfProviderName(providerName).title}. This likely means you specified the wrong endpoint in Void Settings, or your local model provider like Ollama is powered off.`
captureLLMEvent(`${loggingName} - Error`, { error })
onError_({ message: error, fullError })
}
@ -139,8 +145,8 @@ export const sendLLMMessage = ({
break;
case 'ollama':
if ( // TODO @andrew in future we want to use our own templates instead of using ollamaFIM
type === 'sendFIMMessage'
&& settingsOfProvider['ollama']._enabled
messagesType === 'FIMMessage'
&& settingsOfProvider['ollama']._didFillInProviderSettings
&& settingsOfProvider['ollama'].models.some(m => !m.isHidden)
)
sendOllamaFIM({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName })

View file

@ -785,13 +785,13 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
}
console.log('BB')
console.log(predictionType)
console.log('type', predictionType)
// set parameters of `newAutocompletion` appropriately
newAutocompletion.llmPromise = new Promise((resolve, reject) => {
const requestId = this._llmMessageService.sendLLMMessage({
type: 'sendFIMMessage',
messagesType: 'FIMMessage',
messages: {
prefix: llmPrefix,
suffix: llmSuffix,

View file

@ -70,7 +70,7 @@ export type ThreadsState = {
export type ThreadStreamState = {
[threadId: string]: undefined | {
error?: { message: string, fullError: Error | null };
error?: { message: string, fullError: Error | null, };
messageSoFar?: string;
streamingToken?: string;
}
@ -202,12 +202,12 @@ class ChatThreadService extends Disposable implements IChatThreadService {
this._setStreamState(threadId, { error: undefined })
const llmCancelToken = this._llmMessageService.sendLLMMessage({
type: 'sendChatMessage',
messagesType: 'chatMessages',
logging: { loggingName: 'Chat' },
useProviderFor: 'Ctrl+L',
messages: [
{ role: 'system', content: chat_systemMessage },
...this.getCurrentThread().messages.map(m => ({ role: m.role, content: m.content || '(null)' })),
...this.getCurrentThread().messages.map(m => ({ role: m.role, content: m.content || '(empty model output)' })),
],
onText: ({ newText, fullText }) => {
this._setStreamState(threadId, { messageSoFar: fullText })

View file

@ -102,13 +102,13 @@ const getLeadingWhitespacePx = (editor: ICodeEditor, startLine: number): number
// similar to ServiceLLM
export type StartApplyingOpts = {
featureName: 'Ctrl+K';
from: 'QuickEdit';
diffareaid: number; // id of the CtrlK area (contains text selection)
} | {
featureName: 'Ctrl+L';
from: 'Chat';
applyStr: string;
} | {
featureName: 'Autocomplete';
from: 'Autocomplete';
range: IRange;
userMessage: string;
}
@ -1209,13 +1209,13 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
private _initializeStartApplying(opts: StartApplyingOpts): DiffZone | undefined {
const { featureName } = opts
const { from } = opts
let startLine: number
let endLine: number
let uri: URI
if (featureName === 'Ctrl+L') {
if (from === 'Chat') {
const uri_ = this._getActiveEditorURI()
if (!uri_) return
@ -1231,7 +1231,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
endLine = numLines
}
else if (featureName === 'Ctrl+K') {
else if (from === 'QuickEdit') {
const { diffareaid } = opts
const ctrlKZone = this.diffAreaOfId[diffareaid]
if (ctrlKZone.type !== 'CtrlKZone') return
@ -1242,7 +1242,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
endLine = endLine_
}
else {
throw new Error(`Void: diff.type not recognized on: ${featureName}`)
throw new Error(`Void: diff.type not recognized on: ${from}`)
}
const currentFileStr = this._readURI(uri)
@ -1278,7 +1278,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid })
this._onDidAddOrDeleteDiffZones.fire({ uri })
if (featureName === 'Ctrl+K') {
if (from === 'QuickEdit') {
const { diffareaid } = opts
const ctrlKZone = this.diffAreaOfId[diffareaid]
if (ctrlKZone.type !== 'CtrlKZone') return
@ -1289,14 +1289,14 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
// now handle messages
let messages: LLMChatMessage[]
if (featureName === 'Ctrl+L') {
if (from === 'Chat') {
const userContent = fastApply_userMessage({ originalCode, applyStr: opts.applyStr, uri })
messages = [
{ role: 'system', content: fastApply_systemMessage, },
{ role: 'user', content: userContent, }
]
}
else if (featureName === 'Ctrl+K') {
else if (from === 'QuickEdit') {
const { diffareaid } = opts
const ctrlKZone = this.diffAreaOfId[diffareaid]
if (ctrlKZone.type !== 'CtrlKZone') return
@ -1323,14 +1323,14 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
]
// }
}
else { throw new Error(`featureName ${featureName} is invalid`) }
else { throw new Error(`featureName ${from} is invalid`) }
const onDone = (hadError: boolean) => {
diffZone._streamState = { isStreaming: false, }
this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid })
if (featureName === 'Ctrl+K') {
if (from === 'QuickEdit') {
const ctrlKZone = this.diffAreaOfId[opts.diffareaid] as CtrlKZone
ctrlKZone._linkedStreamingDiffZone = null
@ -1350,11 +1350,11 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
const extractText = (fullText: string, recentlyAddedTextLen: number) => {
if (featureName === 'Ctrl+K') {
if (from === 'QuickEdit') {
if (isOllamaFIM) return fullText
return extractCodeFromFIM({ text: fullText, recentlyAddedTextLen, midTag: modelFimTags.midTag })
}
else if (featureName === 'Ctrl+L') {
else if (from === 'Chat') {
return extractCodeFromRegular({ text: fullText, recentlyAddedTextLen })
}
throw 1
@ -1367,9 +1367,9 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
let prevIgnoredSuffix = ''
streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({
type: 'sendChatMessage',
useProviderFor: opts.featureName === 'Ctrl+L' ? 'FastApply' : 'Ctrl+K',
logging: { loggingName: `startApplying - ${featureName}` },
messagesType: 'chatMessages',
useProviderFor: opts.from === 'Chat' ? 'FastApply' : 'Ctrl+K',
logging: { loggingName: `startApplying - ${from}` },
messages,
onText: ({ newText: newText_ }) => {

View file

@ -45,7 +45,7 @@ const CodeButtonsOnHover = ({ text }: { text: string }) => {
const onApply = useCallback(() => {
inlineDiffService.startApplying({
featureName: 'Ctrl+L',
from: 'Chat',
applyStr: text,
})
metricsService.capture('Apply Code', { length: text.length }) // capture the length only

View file

@ -7,11 +7,12 @@ import React, { FormEvent, useCallback, useEffect, useRef, useState } from 'reac
import { useSettingsState, useSidebarState, useChatThreadsState, useQuickEditState, useAccessor } from '../util/services.js';
import { TextAreaFns, VoidInputBox2 } from '../util/inputs.js';
import { QuickEditPropsType } from '../../../quickEditActions.js';
import { ButtonStop, ButtonSubmit, IconX, VoidInputForm } from '../sidebar-tsx/SidebarChat.js';
import { ButtonStop, ButtonSubmit, IconX, VoidChatArea } from '../sidebar-tsx/SidebarChat.js';
import { ModelDropdown } from '../void-settings-tsx/ModelDropdown.js';
import { VOID_CTRL_K_ACTION_ID } from '../../../actionIDs.js';
import { useRefState } from '../util/helpers.js';
import { useScrollbarStyles } from '../util/useScrollbarStyles.js';
import { isFeatureNameDisabled } from '../../../../../../../platform/void/common/voidSettingsTypes.js';
export const QuickEditChat = ({
diffareaid,
@ -42,9 +43,11 @@ export const QuickEditChat = ({
}, [onChangeHeight]);
const settingsState = useSettingsState()
// state of current message
const [instructionsAreEmpty, setInstructionsAreEmpty] = useState(!(initText ?? '')) // the user's instructions
const isDisabled = instructionsAreEmpty
const isDisabled = instructionsAreEmpty || !!isFeatureNameDisabled('Ctrl+K', settingsState)
const [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone] = useRefState<number | null>(initStreamingDiffZoneId)
const isStreaming = currStreamingDiffZoneRef.current !== null
@ -55,7 +58,7 @@ export const QuickEditChat = ({
textAreaFnsRef.current?.disable()
const id = inlineDiffsService.startApplying({
featureName: 'Ctrl+K',
from: 'QuickEdit',
diffareaid: diffareaid,
})
setCurrentlyStreamingDiffZone(id ?? null)
@ -79,7 +82,7 @@ export const QuickEditChat = ({
const keybindingString = accessor.get('IKeybindingService').lookupKeybinding(VOID_CTRL_K_ACTION_ID)?.getLabel()
return <div ref={sizerRef} style={{ maxWidth: 450 }} className={`py-2 w-full`}>
<VoidInputForm
<VoidChatArea
onSubmit={onSubmit}
onAbort={onInterrupt}
onClose={onX}
@ -113,7 +116,7 @@ export const QuickEditChat = ({
}}
multiline={true}
/>
</VoidInputForm>
</VoidChatArea>
</div>

View file

@ -23,8 +23,9 @@ export const ErrorDisplay = ({
const [isExpanded, setIsExpanded] = useState(false);
const details = errorDetails(fullError)
const isExpandable = !!details
const message = message_ === 'TypeError: fetch failed' ? `TypeError: fetch failed. This likely means you specified the wrong endpoint in Void Settings, or a provider like Ollama is powered off.` : message_ + ''
const message = message_ + ''
return (
<div className={`rounded-lg border border-red-200 bg-red-50 p-4 overflow-auto`}>
@ -45,7 +46,7 @@ export const ErrorDisplay = ({
</div>
<div className='flex gap-2'>
{details && (
{isExpandable && (
<button className='text-red-600 hover:text-red-800 p-1 rounded'
onClick={() => setIsExpanded(!isExpanded)}
>

View file

@ -22,7 +22,7 @@ import { VOID_CTRL_L_ACTION_ID } from '../../../actionIDs.js';
import { filenameToVscodeLanguage } from '../../../helpers/detectLanguage.js';
import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js';
import { Pencil } from 'lucide-react';
import { FeatureName } from '../../../../../../../platform/void/common/voidSettingsTypes.js';
import { FeatureName, isFeatureNameDisabled } from '../../../../../../../platform/void/common/voidSettingsTypes.js';
export const IconX = ({ size, className = '', ...props }: { size: number, className?: string } & React.SVGProps<SVGSVGElement>) => {
@ -158,7 +158,7 @@ interface VoidInputFormProps {
onClose?: () => void;
}
export const VoidInputForm: React.FC<VoidInputFormProps> = ({
export const VoidChatArea: React.FC<VoidInputFormProps> = ({
children,
onSubmit,
onAbort,
@ -578,6 +578,7 @@ const ChatBubble = ({ chatMessage, isLoading }: { chatMessage: ChatMessage, isLo
: role === 'user' ? `px-2 self-end w-fit max-w-full whitespace-pre-wrap` // user words should be pre
: role === 'assistant' ? `px-2 self-start w-full max-w-full` : ''
}
${role !== 'assistant' ? 'my-2' : ''}
`}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
@ -621,6 +622,7 @@ export const SidebarChat = () => {
// const modelService = accessor.get('IModelService')
const commandService = accessor.get('ICommandService')
const settingsState = useSettingsState()
// ----- HIGHER STATE -----
// sidebar state
const sidebarStateService = accessor.get('ISidebarStateService')
@ -654,7 +656,8 @@ export const SidebarChat = () => {
// state of current message
const initVal = ''
const [instructionsAreEmpty, setInstructionsAreEmpty] = useState(!initVal)
const isDisabled = instructionsAreEmpty
const isDisabled = instructionsAreEmpty || !!isFeatureNameDisabled('Ctrl+L', settingsState)
const [sidebarRef, sidebarDimensions] = useResizeObserver()
const [formRef, formDimensions] = useResizeObserver()
@ -670,22 +673,13 @@ export const SidebarChat = () => {
if (isDisabled) return
if (isStreaming) return
console.log('chatThreadsService', chatThreadsService ? chatThreadsService : '!undefined')
// send message to LLM
const userMessage = textAreaRef.current?.value ?? ''
console.log('userMessage', userMessage)
console.log('streaming...',)
await chatThreadsService.addUserMessageAndStreamResponse(userMessage)
console.log('done streaming',)
chatThreadsService.setStaging([]) // clear staging
console.log('set staging',)
textAreaFnsRef.current?.setValue('')
console.log('set value',)
textAreaRef.current?.focus() // focus input after submit
console.log('textAreaRef', textAreaRef.current)
console.log('focus',)
}, [chatThreadsService, isDisabled, isStreaming, textAreaRef, textAreaFnsRef])
@ -765,7 +759,7 @@ export const SidebarChat = () => {
}
}, [onSubmit])
const inputForm = <div className={`right-0 left-0 m-2 z-[999] overflow-hidden ${previousMessages.length > 0 ? 'absolute bottom-0' : ''}`}>
<VoidInputForm
<VoidChatArea
formRef={formRef}
onSubmit={onSubmit}
onAbort={onAbort}
@ -785,7 +779,7 @@ export const SidebarChat = () => {
fnsRef={textAreaFnsRef}
multiline={true}
/>
</VoidInputForm>
</VoidChatArea>
</div>
return <div ref={sidebarRef} className={`w-full h-full`}>

View file

@ -298,9 +298,9 @@ export const VoidCheckBox = ({ label, value, onClick, className }: { label: stri
export const VoidCustomSelectBox = <T extends any>({
export const VoidCustomDropdownBox = <T extends any>({
options,
selectedOption: selectedOption_,
selectedOption,
onChangeOption,
getOptionDropdownName,
getOptionDisplayName,
@ -311,7 +311,7 @@ export const VoidCustomSelectBox = <T extends any>({
gap = 0,
}: {
options: T[];
selectedOption?: T;
selectedOption: T | undefined;
onChangeOption: (newValue: T) => void;
getOptionDropdownName: (option: T) => string;
getOptionDisplayName: (option: T) => string;
@ -335,7 +335,7 @@ export const VoidCustomSelectBox = <T extends any>({
} = useFloating({
open: isOpen,
onOpenChange: setIsOpen,
placement:'bottom-start',
placement: 'bottom-start',
middleware: [
offset(gap),
@ -367,17 +367,15 @@ export const VoidCustomSelectBox = <T extends any>({
}),
],
whileElementsMounted: autoUpdate,
strategy:'fixed',
strategy: 'fixed',
});
// if the selected option is null, use the 0th option
// if the selected option is null, set the selection to the 0th option
useEffect(() => {
if (!options[0]) return
if (!selectedOption_) {
onChangeOption(options[0]);
}
}, [selectedOption_, options])
const selectedOption = !selectedOption_ ? options[0] : selectedOption_
if (options.length === 0) return
if (selectedOption) return
onChangeOption(options[0])
}, [selectedOption, onChangeOption, options])
// Handle clicks outside
useEffect(() => {
@ -404,6 +402,9 @@ export const VoidCustomSelectBox = <T extends any>({
return () => document.removeEventListener('mousedown', handleClickOutside);
}, [isOpen, refs.floating, refs.reference]);
if (!selectedOption)
return null
return (
<div className={`inline-block relative ${className}`}>
{/* Hidden measurement div */}

View file

@ -4,9 +4,9 @@
*--------------------------------------------------------------------------------------*/
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { FeatureName, featureNames, getProvidersWithoutModels, ModelSelection, modelSelectionsEqual, ProviderName, providerNames, SettingsOfProvider } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
import { FeatureName, featureNames, isFeatureNameDisabled, ModelSelection, modelSelectionsEqual, ProviderName, providerNames, SettingsOfProvider } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
import { useSettingsState, useRefreshModelState, useAccessor } from '../util/services.js'
import { _VoidSelectBox, VoidCustomSelectBox } from '../util/inputs.js'
import { _VoidSelectBox, VoidCustomDropdownBox } from '../util/inputs.js'
import { SelectBox } from '../../../../../../../base/browser/ui/selectBox/selectBox.js'
import { IconWarning } from '../sidebar-tsx/SidebarChat.js'
import { VOID_OPEN_SETTINGS_ACTION_ID, VOID_TOGGLE_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js'
@ -25,13 +25,13 @@ const ModelSelectBox = ({ options, featureName }: { options: ModelOption[], feat
const voidSettingsService = accessor.get('IVoidSettingsService')
const selection = voidSettingsService.state.modelSelectionOfFeature[featureName]
const selectedOption = selection ? voidSettingsService.state._modelOptions.find(v => modelSelectionsEqual(v.selection, selection)) : options[0]
const selectedOption = selection ? voidSettingsService.state._modelOptions.find(v => modelSelectionsEqual(v.selection, selection))! : options[0]
const onChangeOption = useCallback((newOption: ModelOption) => {
voidSettingsService.setModelSelectionOfFeature(featureName, newOption.selection)
}, [voidSettingsService, featureName])
return <VoidCustomSelectBox
return <VoidCustomDropdownBox
options={options}
selectedOption={selectedOption}
onChangeOption={onChangeOption}
@ -73,22 +73,7 @@ const ModelSelectBox = ({ options, featureName }: { options: ModelOption[], feat
// />
// }
const MemoizedModelSelectBox = ({ featureName }: { featureName: FeatureName }) => {
const settingsState = useSettingsState()
const oldOptionsRef = useRef<ModelOption[]>([])
const [memoizedOptions, setMemoizedOptions] = useState(oldOptionsRef.current)
useEffect(() => {
const oldOptions = oldOptionsRef.current
const newOptions = settingsState._modelOptions
if (!optionsEqual(oldOptions, newOptions)) {
setMemoizedOptions(newOptions)
}
oldOptionsRef.current = newOptions
}, [settingsState._modelOptions])
return <ModelSelectBox featureName={featureName} options={memoizedOptions} />
}
export const WarningBox = ({ text, onClick, className }: { text: string; onClick?: () => void; className?: string }) => {
@ -114,22 +99,40 @@ export const WarningBox = ({ text, onClick, className }: { text: string; onClick
// />
}
const MemoizedModelDropdown = ({ featureName }: { featureName: FeatureName }) => {
const settingsState = useSettingsState()
const oldOptionsRef = useRef<ModelOption[]>([])
const [memoizedOptions, setMemoizedOptions] = useState(oldOptionsRef.current)
useEffect(() => {
const oldOptions = oldOptionsRef.current
const newOptions = settingsState._modelOptions
if (!optionsEqual(oldOptions, newOptions)) {
setMemoizedOptions(newOptions)
}
oldOptionsRef.current = newOptions
}, [settingsState._modelOptions])
return <ModelSelectBox featureName={featureName} options={memoizedOptions} />
}
export const ModelDropdown = ({ featureName }: { featureName: FeatureName }) => {
const settingsState = useSettingsState()
const providersWithMissingModels = getProvidersWithoutModels(settingsState.settingsOfProvider)
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
const openSettings = () => { commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID); };
return <>
{providersWithMissingModels.length !== 0 ?
<WarningBox onClick={openSettings} text={`Model required for ${providersWithMissingModels[0]}`} />
: settingsState._modelOptions.length === 0 ?
<WarningBox onClick={openSettings} text='Provider required' />
: <MemoizedModelSelectBox featureName={featureName} />
}
</>
const isDisabled = isFeatureNameDisabled(featureName, settingsState)
if (isDisabled)
return <WarningBox onClick={openSettings} text={
isDisabled === 'needToEnableModel' ? 'Enable model'
: isDisabled === 'addModel' ? 'Add model'
: (isDisabled === 'addProvider' || isDisabled === 'notFilledIn' || isDisabled === 'providerNotAutoDetected') ? 'Provider required'
: 'Provider required'
} />
return <MemoizedModelDropdown featureName={featureName} />
}

View file

@ -5,9 +5,9 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js'
import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidModelInfo, globalSettingNames, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, defaultProviderSettings, nonlocalProviderNames, localProviderNames, GlobalSettingName, featureNames, displayInfoOfFeatureName } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidModelInfo, globalSettingNames, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, defaultProviderSettings, nonlocalProviderNames, localProviderNames, GlobalSettingName, featureNames, displayInfoOfFeatureName, isProviderNameDisabled } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
import { VoidButton, VoidCheckBox, VoidCustomSelectBox, VoidInputBox, VoidInputBox2, VoidSwitch } from '../util/inputs.js'
import { VoidButton, VoidCheckBox, VoidCustomDropdownBox, VoidInputBox, VoidInputBox2, VoidSwitch } from '../util/inputs.js'
import { useAccessor, useIsDark, useRefreshModelListener, useRefreshModelState, useSettingsState } from '../util/services.js'
import { X, RefreshCw, Loader2, Check, MoveRight } from 'lucide-react'
import { useScrollbarStyles } from '../util/useScrollbarStyles.js'
@ -79,7 +79,7 @@ const RefreshableModels = () => {
const buttons = refreshableProviderNames.map(providerName => {
if (!settingsState.settingsOfProvider[providerName]._enabled) return null
if (!settingsState.settingsOfProvider[providerName]._didFillInProviderSettings) return null
return <div key={providerName} className='pb-4'>
<RefreshModelButton providerName={providerName} />
</div>
@ -112,7 +112,7 @@ const AddModelMenu = ({ onSubmit }: { onSubmit: () => void }) => {
<div className='flex items-center gap-4'>
{/* provider */}
<VoidCustomSelectBox
<VoidCustomDropdownBox
options={providerNames}
selectedOption={providerName}
onChangeOption={(pn) => setProviderName(pn)}
@ -199,7 +199,7 @@ export const ModelDump = () => {
for (let providerName of providerNames) {
const providerSettings = settingsState.settingsOfProvider[providerName]
// if (!providerSettings.enabled) continue
modelDump.push(...providerSettings.models.map(model => ({ ...model, providerName, providerEnabled: !!providerSettings._enabled })))
modelDump.push(...providerSettings.models.map(model => ({ ...model, providerName, providerEnabled: !!providerSettings._didFillInProviderSettings })))
}
// sort by hidden
@ -223,7 +223,6 @@ export const ModelDump = () => {
<div className={`flex-grow flex items-center gap-4`}>
<span className='w-full max-w-32'>{isNewProviderName ? displayInfoOfProviderName(providerName).title : ''}</span>
<span className='w-fit truncate'>{modelName}</span>
{/* <span>{`${modelName} (${providerName})`}</span> */}
</div>
{/* right part is anything that fits */}
<div className='flex items-center gap-4'>
@ -260,7 +259,6 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider
const accessor = useAccessor()
const voidSettingsService = accessor.get('IVoidSettingsService')
const voidMetricsService = accessor.get('IMetricsService')
let weChangedTextRef = false
@ -284,25 +282,8 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider
weChangedTextRef = true
instance.value = stateVal as string
weChangedTextRef = false
const isEverySettingPresent = Object.keys(defaultProviderSettings[providerName]).every(key => {
return !!settingsAtProvider[key as keyof typeof settingsAtProvider]
})
const shouldEnable = isEverySettingPresent && !settingsAtProvider._enabled // enable if all settings are present and not already enabled
const shouldDisable = !isEverySettingPresent && settingsAtProvider._enabled
if (shouldEnable) {
voidSettingsService.setSettingOfProvider(providerName, '_enabled', true)
voidMetricsService.capture('Enable Provider', { providerName })
}
if (shouldDisable) {
voidSettingsService.setSettingOfProvider(providerName, '_enabled', false)
voidMetricsService.capture('Disable Provider', { providerName })
}
}
syncInstance()
const disposable = voidSettingsService.onDidChangeState(syncInstance)
return [disposable]
@ -318,7 +299,10 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider
}
const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) => {
// const voidSettingsState = useSettingsState()
const voidSettingsState = useSettingsState()
const needsModel = isProviderNameDisabled(providerName, voidSettingsState) === 'addModel'
// const accessor = useAccessor()
// const voidSettingsService = accessor.get('IVoidSettingsService')
@ -349,6 +333,12 @@ const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) =
{settingNames.map((settingName, i) => {
return <ProviderSetting key={settingName} providerName={providerName} settingName={settingName} />
})}
{needsModel ?
providerName === 'ollama' ?
<WarningBox text={`Please install an Ollama model. We'll auto-detect it.`} />
: <WarningBox text={`Please add a model for ${providerTitle} below (Models).`} />
: null}
</div>
</div >
}