From 4635cf8236501e2147dfeace76fa703a515cb561 Mon Sep 17 00:00:00 2001 From: Aleksander Grygier Date: Tue, 22 Sep 2026 01:28:13 +0200 Subject: [PATCH] ui : sort provider models by recent use Assisted-by: pi:llama.cpp/DeepSeek-V4.1-Flash --- .../lib/components/app/navigation/utils.ts | 13 +++++- .../ui/src/lib/constants/storage.constants.ts | 3 ++ tools/ui/src/lib/constants/ui.constants.ts | 3 ++ .../lib/hooks/use-models-selector.svelte.ts | 3 +- .../ui/src/lib/stores/models/index.svelte.ts | 44 ++++++++++++++++++- 5 files changed, 62 insertions(+), 4 deletions(-) diff --git a/tools/ui/src/lib/components/app/navigation/utils.ts b/tools/ui/src/lib/components/app/navigation/utils.ts index c7e37313c6..39afbc63db 100644 --- a/tools/ui/src/lib/components/app/navigation/utils.ts +++ b/tools/ui/src/lib/components/app/navigation/utils.ts @@ -137,9 +137,18 @@ export function groupProviderOptions( loading: boolean; name: string; }[], - limit = Infinity + limit = Infinity, + recentIds: readonly string[] = [] ): ProviderGroup[] { const byBackend = new SvelteMap(); + const rank = new SvelteMap(); + + recentIds.forEach((id, index) => rank.set(id, index)); + + const rankOf = (id: string) => rank.get(id) ?? Number.MAX_SAFE_INTEGER; + // recently used models lead their section, the rest keep the backend's order + const byRecency = (items: ModelItem[]) => + rank.size === 0 ? items : [...items].sort((a, b) => rankOf(a.option.id) - rankOf(b.option.id)); for (let i = 0; i < options.length; i++) { const option = options[i]; @@ -151,7 +160,7 @@ export function groupProviderOptions( } return providers.map((provider) => { - const items = byBackend.get(provider.backendId) ?? []; + const items = byRecency(byBackend.get(provider.backendId) ?? []); return { ...provider, items: items.slice(0, limit), matched: items.length }; }); diff --git a/tools/ui/src/lib/constants/storage.constants.ts b/tools/ui/src/lib/constants/storage.constants.ts index a33f4bd205..94df814b4d 100644 --- a/tools/ui/src/lib/constants/storage.constants.ts +++ b/tools/ui/src/lib/constants/storage.constants.ts @@ -33,6 +33,9 @@ export const FAVORITE_MODELS_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.favoriteMod /** Model the user picked last, kept across reloads. Stores `{ id, model }`. */ export const SELECTED_MODEL_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.selectedModel`; + +/** Recently used model ids, most recent first, backend-qualified. */ +export const RECENT_MODELS_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.recentModels`; export const REASONING_EFFORT_DEFAULT_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.reasoningEffortDefault`; export const CONVERSATION_TABS_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.conversationTabs`; export const USER_OVERRIDES_LOCALSTORAGE_KEY = `${STORAGE_APP_NAME}.userOverrides`; diff --git a/tools/ui/src/lib/constants/ui.constants.ts b/tools/ui/src/lib/constants/ui.constants.ts index 30d170c5bd..d140521f1f 100644 --- a/tools/ui/src/lib/constants/ui.constants.ts +++ b/tools/ui/src/lib/constants/ui.constants.ts @@ -53,6 +53,9 @@ export const MODEL_SELECTOR_ICON = Package; /** Models listed per remote provider before the "+ X more" line; search covers the rest. */ export const REMOTE_PROVIDER_MODEL_LIMIT = 12; +/** Recently used models kept per browser, most recent first. */ +export const RECENT_MODEL_LIMIT = 20; + /** Model selector views: the favorites of every backend, the local server, the remote backends. */ export const ICON_STRIP_TRANSITION_DURATION = 150; diff --git a/tools/ui/src/lib/hooks/use-models-selector.svelte.ts b/tools/ui/src/lib/hooks/use-models-selector.svelte.ts index 526717dacd..c18084aeff 100644 --- a/tools/ui/src/lib/hooks/use-models-selector.svelte.ts +++ b/tools/ui/src/lib/hooks/use-models-selector.svelte.ts @@ -152,7 +152,8 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele sectionOptions, remoteProviders, // a drill-in or a search reaches every model, the sections stay short - providerViewId || searchTerm ? Infinity : REMOTE_PROVIDER_MODEL_LIMIT + providerViewId || searchTerm ? Infinity : REMOTE_PROVIDER_MODEL_LIMIT, + modelsStore.recentModelIds ) ); const groupedFilteredOptions = $derived.by(() => { diff --git a/tools/ui/src/lib/stores/models/index.svelte.ts b/tools/ui/src/lib/stores/models/index.svelte.ts index 2fbe2ea780..2ba9643315 100644 --- a/tools/ui/src/lib/stores/models/index.svelte.ts +++ b/tools/ui/src/lib/stores/models/index.svelte.ts @@ -8,7 +8,12 @@ */ import { browser } from '$app/environment'; -import { FAVORITE_MODELS_LOCALSTORAGE_KEY, SELECTED_MODEL_LOCALSTORAGE_KEY } from '$lib/constants'; +import { + FAVORITE_MODELS_LOCALSTORAGE_KEY, + RECENT_MODEL_LIMIT, + RECENT_MODELS_LOCALSTORAGE_KEY, + SELECTED_MODEL_LOCALSTORAGE_KEY +} from '$lib/constants'; import { ServerModelStatus } from '$lib/enums'; import { ModelsService } from '$lib/services/models.service'; // direct imports between stores, not via the barrel, to avoid circular deps @@ -45,11 +50,31 @@ function loadStoredSelection(): { id: string; model: string | null } | null { const storedSelection = loadStoredSelection(); +/** Recently used backend-qualified ids, most recent first. */ +function loadRecentModels(): string[] { + if (!browser) return []; + + try { + const raw = localStorage.getItem(RECENT_MODELS_LOCALSTORAGE_KEY); + + if (!raw) return []; + + const parsed = JSON.parse(raw) as unknown; + + return Array.isArray(parsed) + ? parsed.filter((id): id is string => typeof id === 'string').slice(0, RECENT_MODEL_LIMIT) + : []; + } catch { + return []; + } +} + class ModelsStore implements ModelPropsHost, ModelStatusHost { activeModels = $state([]); error = $state(null); favoriteModelIds = $state>(this.loadFavoritesFromStorage()); loading = $state(false); + recentModelIds = $state(loadRecentModels()); routerModels = $state([]); selectedModelId = $state(storedSelection?.id ?? null); selectedModelName = $state(storedSelection?.model ?? null); @@ -399,6 +424,7 @@ class ModelsStore implements ModelPropsHost, ModelStatusHost { this.selectedModelName = option.model; this.selectionFromStorage = false; this.persistSelection(); + this.recordRecentModel(qualifiedId); } finally { this.updating = false; } @@ -606,6 +632,22 @@ class ModelsStore implements ModelPropsHost, ModelStatusHost { } } + /** Move a model to the front of the recently used list. */ + private recordRecentModel(qualifiedId: string): void { + this.recentModelIds = [ + qualifiedId, + ...this.recentModelIds.filter((id) => id !== qualifiedId) + ].slice(0, RECENT_MODEL_LIMIT); + + if (!browser) return; + + try { + localStorage.setItem(RECENT_MODELS_LOCALSTORAGE_KEY, JSON.stringify(this.recentModelIds)); + } catch { + console.warn('[ModelsStore] Failed to persist the recently used models'); + } + } + private async runFetch(): Promise { this.loading = true; this.error = null;