ui : sort provider models by recent use

Assisted-by: pi:llama.cpp/DeepSeek-V4.1-Flash
This commit is contained in:
Aleksander Grygier
2026-09-22 01:28:13 +02:00
parent c0a92fbab1
commit 4635cf8236
5 changed files with 62 additions and 4 deletions
@@ -137,9 +137,18 @@ export function groupProviderOptions(
loading: boolean;
name: string;
}[],
limit = Infinity
limit = Infinity,
recentIds: readonly string[] = []
): ProviderGroup[] {
const byBackend = new SvelteMap<string, ModelItem[]>();
const rank = new SvelteMap<string, number>();
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 };
});
@@ -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`;
@@ -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;
@@ -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(() => {
+43 -1
View File
@@ -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<ModelOption[]>([]);
error = $state<string | null>(null);
favoriteModelIds = $state<Set<string>>(this.loadFavoritesFromStorage());
loading = $state(false);
recentModelIds = $state<string[]>(loadRecentModels());
routerModels = $state<ApiModelDataEntry[]>([]);
selectedModelId = $state<string | null>(storedSelection?.id ?? null);
selectedModelName = $state<string | null>(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<void> {
this.loading = true;
this.error = null;