diff --git a/tools/ui/src/lib/constants/storage.constants.ts b/tools/ui/src/lib/constants/storage.constants.ts index 5e6610d859..4241a0d995 100644 --- a/tools/ui/src/lib/constants/storage.constants.ts +++ b/tools/ui/src/lib/constants/storage.constants.ts @@ -16,6 +16,9 @@ export const DB_APP_NAME_DEPRECATED = 'LlamacppWebui'; export const ALWAYS_ALLOWED_TOOLS_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.alwaysAllowedTools`; +/** Id of the backend the selector and new requests target, restored on page load. */ +export const ACTIVE_BACKEND_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.activeBackend`; + /** Paused model download ids (`:`), restored on the next page load. */ export const PAUSED_MODEL_DOWNLOADS_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.pausedModelDownloads`; export const CONFIG_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.config`; diff --git a/tools/ui/src/lib/stores/backends.svelte.ts b/tools/ui/src/lib/stores/backends.svelte.ts index b071948eeb..cd9c20e8be 100644 --- a/tools/ui/src/lib/stores/backends.svelte.ts +++ b/tools/ui/src/lib/stores/backends.svelte.ts @@ -8,14 +8,34 @@ */ import { browser } from '$app/environment'; -import { LOCAL_BACKEND_ID, SETTINGS_KEYS } from '$lib/constants'; +import { ACTIVE_BACKEND_LOCALSTORAGE_KEY, LOCAL_BACKEND_ID, SETTINGS_KEYS } from '$lib/constants'; import { settingsStore } from '$lib/stores/settings/index.svelte'; import type { Backend } from '$lib/types'; import { setBackendsResolver } from '$lib/utils/api-base'; import { createLocalBackend, parseBackendsSettings } from '$lib/utils/backend'; +function loadActiveBackendId(): string { + if (!browser) return LOCAL_BACKEND_ID; + + try { + return localStorage.getItem(ACTIVE_BACKEND_LOCALSTORAGE_KEY) ?? LOCAL_BACKEND_ID; + } catch { + return LOCAL_BACKEND_ID; + } +} + +function persistActiveBackendId(backendId: string): void { + if (!browser) return; + + try { + localStorage.setItem(ACTIVE_BACKEND_LOCALSTORAGE_KEY, backendId); + } catch { + /* ignore */ + } +} + class BackendsStore { - activeId = $state(LOCAL_BACKEND_ID); + activeId = $state(loadActiveBackendId()); get active(): Backend { const active = this.enabled.find((backend) => backend.id === this.activeId); @@ -56,14 +76,15 @@ class BackendsStore { this.saveExternal(this.external.filter((backend) => backend.id !== backendId)); if (this.activeId === backendId) { - this.activeId = LOCAL_BACKEND_ID; + this.setActive(LOCAL_BACKEND_ID); } } setActive(backendId: string): void { - this.activeId = this.list.some((backend) => backend.id === backendId) - ? backendId - : LOCAL_BACKEND_ID; + const id = this.list.some((backend) => backend.id === backendId) ? backendId : LOCAL_BACKEND_ID; + + this.activeId = id; + persistActiveBackendId(id); } setLocalEnabled(enabled: boolean): void { diff --git a/tools/ui/src/lib/stores/server.svelte.ts b/tools/ui/src/lib/stores/server.svelte.ts index 0fe4499781..731f0c976b 100644 --- a/tools/ui/src/lib/stores/server.svelte.ts +++ b/tools/ui/src/lib/stores/server.svelte.ts @@ -22,6 +22,7 @@ class ServerStore { props = $state(null); role = $state(null); status = $state(null); + private fetchBackendId: string | undefined; private fetchPromise: Promise | null = null; private retryTimer: ReturnType | null = null; @@ -62,6 +63,7 @@ class ServerStore { this.loading = false; this.role = null; this.fetchPromise = null; + this.fetchBackendId = undefined; } /** @@ -70,7 +72,12 @@ class ServerStore { * splash and the chat screen every retry tick. */ async fetch({ background = false }: { background?: boolean } = {}): Promise { - if (this.fetchPromise) return this.fetchPromise; + // props and role describe one server. a fetch started for another backend + // must not be reused, and its response must not commit once the active + // backend has changed while it was in flight + const backendId = getBackend()?.id; + + if (this.fetchPromise && this.fetchBackendId === backendId) return this.fetchPromise; this.clearRetryTimer(); @@ -93,15 +100,20 @@ class ServerStore { this.error = null; } - const fetchPromise = (async () => { + const promise = (async () => { try { const props = await PropsService.fetch(); + // the active backend changed while this request was in flight + if (getBackend()?.id !== backendId) return; + this.props = props; this.error = null; this.status = null; this.detectRole(props); } catch (error: unknown) { + if (getBackend()?.id !== backendId) return; + this.error = error instanceof Error ? error.message : String(error); this.status = error instanceof ApiError ? error.status : null; console.error('Error fetching server properties:', error); @@ -113,13 +125,24 @@ class ServerStore { if (!background) { this.loading = false; } - - this.fetchPromise = null; } })(); - this.fetchPromise = fetchPromise; - await fetchPromise; + this.fetchPromise = promise; + this.fetchBackendId = backendId; + + // a backend switch clears the in-flight handle; only the fetch that is + // still the current one may release it + void promise + .catch(() => {}) + .finally(() => { + if (this.fetchPromise === promise) { + this.fetchPromise = null; + this.fetchBackendId = undefined; + } + }); + + await promise; } private clearRetryTimer(): void {