diff --git a/src/vs/platform/void/common/refreshModelService.ts b/src/vs/platform/void/common/refreshModelService.ts index 425393ef..d5e31eb7 100644 --- a/src/vs/platform/void/common/refreshModelService.ts +++ b/src/vs/platform/void/common/refreshModelService.ts @@ -25,11 +25,20 @@ type RefreshableState = ({ state: 'finished', timeoutId: null, } | { - state: 'finished_invisible', + state: 'error', timeoutId: null, }) +/* + +user click -> error -> fire(error) + \> success -> fire(success) + finally: keep polling + +poll -> do not fire + +*/ export type RefreshModelStateOfProvider = Record @@ -41,6 +50,8 @@ const refreshBasedOn: { [k in RefreshableProviderName]: (keyof SettingsOfProvide const REFRESH_INTERVAL = 5_000 // const COOLDOWN_TIMEOUT = 300 +const autoOptions = { enableProviderOnSuccess: true, doNotFire: true } + // element-wise equals function eq(a: T[], b: T[]): boolean { if (a.length !== b.length) return false @@ -51,7 +62,7 @@ function eq(a: T[], b: T[]): boolean { } export interface IRefreshModelService { readonly _serviceBrand: undefined; - refreshModels: (providerName: RefreshableProviderName) => Promise; + startRefreshingModels: (providerName: RefreshableProviderName, options: { enableProviderOnSuccess: boolean, doNotFire: boolean }) => void; onDidChangeState: Event; state: RefreshModelStateOfProvider; } @@ -75,7 +86,7 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ const disposables: Set = new Set() - const initializePollingAndOnChange = () => { + const initializeAutoPollingAndOnChange = () => { this._clearAllTimeouts() disposables.forEach(d => d.dispose()) disposables.clear() @@ -84,14 +95,14 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ for (const providerName of refreshableProviderNames) { - const { _enabled: enabled } = this.voidSettingsService.state.settingsOfProvider[providerName] - this.refreshModels(providerName, !enabled, { isPolling: true, isInvisible: true }) + // const { _enabled: enabled } = this.voidSettingsService.state.settingsOfProvider[providerName] + this.startRefreshingModels(providerName, autoOptions) // every time providerName.enabled changes, refresh models too, like a useEffect - let relevantVals = () => refreshBasedOn[providerName].map(settingName => this.voidSettingsService.state.settingsOfProvider[providerName][settingName]) + let relevantVals = () => refreshBasedOn[providerName].map(settingName => voidSettingsService.state.settingsOfProvider[providerName][settingName]) let prevVals = relevantVals() // each iteration of a for loop has its own context and vars, so this is ok disposables.add( - this.voidSettingsService.onDidChangeState(() => { // we might want to debounce this + voidSettingsService.onDidChangeState(() => { // we might want to debounce this const newVals = relevantVals() if (!eq(prevVals, newVals)) { @@ -101,7 +112,7 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ // if it was just enabled, or there was a change and it wasn't to the enabled state, refresh if ((enabled && !prevEnabled) || (!enabled && !prevEnabled)) { // if user just clicked enable, refresh - this.refreshModels(providerName, !enabled, { isPolling: false, isInvisible: true }) + this.startRefreshingModels(providerName, autoOptions) } else { // else if user just clicked disable, don't refresh @@ -119,9 +130,9 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ // on mount (when get init settings state), and if a relevant feature flag changes, start refreshing models voidSettingsService.waitForInitState.then(() => { - initializePollingAndOnChange() + initializeAutoPollingAndOnChange() this._register( - voidSettingsService.onDidChangeState((type) => { if (typeof type === 'object' && type[1] === 'autoRefreshModels') initializePollingAndOnChange() }) + voidSettingsService.onDidChangeState((type) => { if (typeof type === 'object' && type[1] === 'autoRefreshModels') initializeAutoPollingAndOnChange() }) ) }) @@ -129,22 +140,23 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ state: RefreshModelStateOfProvider = { ollama: { state: 'init', timeoutId: null }, - // openAICompatible: { state: 'init', timeoutId: null }, } // start listening for models (and don't stop until success) - async refreshModels(providerName: RefreshableProviderName, enableProviderOnSuccess?: boolean, options?: { isPolling?: boolean, isInvisible?: boolean }) { - - const { isPolling, isInvisible } = options ?? {} - - // console.log(`refreshModels, isInvisible ${isInvisible} isPolling ${isPolling}`) + startRefreshingModels: IRefreshModelService['startRefreshingModels'] = (providerName, options) => { this._clearProviderTimeout(providerName) - // start loading models - if (!isInvisible) this._setRefreshState(providerName, 'refreshing') + this._setRefreshState(providerName, 'refreshing', options) + const autoPoll = () => { + if (this.voidSettingsService.state.globalSettings.autoRefreshModels) { + // resume auto-polling + const timeoutId = setTimeout(() => this.startRefreshingModels(providerName, autoOptions), REFRESH_INTERVAL) + this._setTimeoutId(providerName, timeoutId) + } + } const listFn = providerName === 'ollama' ? this.llmMessageService.ollamaList : providerName === 'openAICompatible' ? this.llmMessageService.openAICompatibleList : () => { } @@ -160,34 +172,21 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ else if (providerName === 'openAICompatible') return (model as OpenaiCompatibleModelResponse).id; else throw new Error('refreshMode fn: unknown provider', providerName); }), - { enableProviderOnSuccess, isPolling, isInvisible } + { enableProviderOnSuccess: options.enableProviderOnSuccess, hideRefresh: options.doNotFire } ) - // update state - if (enableProviderOnSuccess) { - this.voidSettingsService.setSettingOfProvider(providerName, '_enabled', true) - } - - if (!isInvisible) { - this._setRefreshState(providerName, 'finished') - } else if (isInvisible) { - this._setRefreshState(providerName, 'finished_invisible') - } - + if (options.enableProviderOnSuccess) this.voidSettingsService.setSettingOfProvider(providerName, '_enabled', true) + this._setRefreshState(providerName, 'finished', options) + autoPoll() }, onError: ({ error }) => { - console.log('retrying list models:', providerName, error) + this._setRefreshState(providerName, 'error', options) + autoPoll() } }) - // check if we should poll - // if it was originally called as `isPolling` and if the `autoRefreshModels` flag is enabled - if (isPolling && this.voidSettingsService.state.globalSettings['autoRefreshModels']) { - const timeoutId = setTimeout(() => this.refreshModels(providerName, enableProviderOnSuccess, options), REFRESH_INTERVAL) - this._setTimeoutId(providerName, timeoutId) - } } _clearAllTimeouts() { @@ -208,7 +207,8 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ this.state[providerName].timeoutId = timeoutId } - private _setRefreshState(providerName: RefreshableProviderName, state: RefreshableState['state']) { + private _setRefreshState(providerName: RefreshableProviderName, state: RefreshableState['state'], options?: { doNotFire: boolean }) { + if (options?.doNotFire) return this.state[providerName].state = state this._onDidChangeState.fire(providerName) } diff --git a/src/vs/platform/void/common/voidSettingsService.ts b/src/vs/platform/void/common/voidSettingsService.ts index a85c31ca..ffaa5e72 100644 --- a/src/vs/platform/void/common/voidSettingsService.ts +++ b/src/vs/platform/void/common/voidSettingsService.ts @@ -57,7 +57,7 @@ export interface IVoidSettingsService { setModelSelectionOfFeature: SetModelSelectionOfFeatureFn; setGlobalSetting: SetGlobalSettingFn; - setAutodetectedModels(providerName: ProviderName, modelNames: string[], logging: { enableProviderOnSuccess?: boolean, isPolling?: boolean, isInvisible?: boolean }): void; + setAutodetectedModels(providerName: ProviderName, modelNames: string[], logging: object): void; toggleModelHidden(providerName: ProviderName, modelName: string): void; addModel(providerName: ProviderName, modelName: string): void; deleteModel(providerName: ProviderName, modelName: string): boolean; @@ -223,7 +223,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { - setAutodetectedModels(providerName: ProviderName, newDefaultModelNames: string[], logging: { enableProviderOnSuccess?: boolean, isPolling?: boolean, isInvisible?: boolean }) { + setAutodetectedModels(providerName: ProviderName, newDefaultModelNames: string[], logging: object) { const { models } = this.state.settingsOfProvider[providerName] diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorDisplay.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorDisplay.tsx index a87e466e..84fe410a 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorDisplay.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorDisplay.tsx @@ -9,7 +9,7 @@ import { errorDetails } from '../../../../../../../platform/void/common/llmMessa export const ErrorDisplay = ({ - message, + message:message_, fullError, onDismiss, showDismiss, @@ -23,6 +23,8 @@ export const ErrorDisplay = ({ const details = errorDetails(fullError) + const message = message_ === 'TypeError: fetch failed' ? 'TypeError: fetch failed. This likely means you specified the wrong endpoint in Void Settings.' : message_ + return (
diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx index 0b11c209..93f95ea9 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx @@ -716,7 +716,7 @@ export const SidebarChat = () => { {/* text input */} { setInstructionsAreEmpty(!newStr) }, [setInstructionsAreEmpty])} onKeyDown={(e) => { diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx index 99cd222f..689419df 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx @@ -38,32 +38,39 @@ const RefreshModelButton = ({ providerName }: { providerName: RefreshableProvide const refreshModelService = accessor.get('IRefreshModelService') const metricsService = accessor.get('IMetricsService') - const [justFinished, setJustFinished] = useState(false) + const [justFinished, setJustFinished] = useState(null) useRefreshModelListener( useCallback((providerName2, refreshModelState) => { if (providerName2 !== providerName) return const { state } = refreshModelState[providerName] - if (state !== 'finished') return + if (!(state === 'finished' || state === 'error')) return // now we know we just entered 'finished' state for this providerName - setJustFinished(true) - const tid = setTimeout(() => { setJustFinished(false) }, 2000) + setJustFinished(state) + const tid = setTimeout(() => { setJustFinished(null) }, 2000) return () => clearTimeout(tid) }, [providerName]) ) const { state } = refreshModelState[providerName] - const isRefreshing = state === 'refreshing' const { title: providerTitle } = displayInfoOfProviderName(providerName) return { - refreshModelService.refreshModels(providerName) + refreshModelService.startRefreshingModels(providerName, { enableProviderOnSuccess: false, doNotFire: false }) metricsService.capture('Click', { providerName, action: 'Refresh Models' }) }} - text={justFinished ? `${providerTitle} Models are up-to-date!` : `Manually refresh models list for ${providerTitle}.`} - icon={isRefreshing ? : (justFinished ? : )} - disabled={isRefreshing || justFinished} + text={justFinished === 'finished' ? `${providerTitle} Models are up-to-date!` + : justFinished === 'error' ? `${providerTitle} not found!` + : `Manually refresh ${providerTitle} models.` + } + icon={justFinished === 'finished' ? + : justFinished === 'error' ? + : state === 'refreshing' ? + : + } + + disabled={state === 'refreshing' || justFinished !== null} /> }