ui : keep the model selector mounted across backend switches

Assisted-by: pi:llama.cpp/DeepSeek-V4.1-Flash
This commit is contained in:
Aleksander Grygier
2026-09-16 19:33:34 +02:00
parent ce00f0bcd1
commit 5c9cd156d5
3 changed files with 31 additions and 16 deletions
@@ -276,13 +276,6 @@
class="w-full md:min-w-80 md:max-w-[26rem] max-w-[calc(100vw-2rem)] p-0!" class="w-full md:min-w-80 md:max-w-[26rem] max-w-[calc(100vw-2rem)] p-0!"
onOpenAutoFocus={(event) => event.preventDefault()} onOpenAutoFocus={(event) => event.preventDefault()}
> >
<ModelsSelectorBackendSwitcher
activeId={ms.activeBackendId}
backends={ms.backends}
onAdd={handleAddBackend}
onSelect={(backendId) => void ms.handleBackendChange(backendId)}
/>
<DropdownMenuSearchable <DropdownMenuSearchable
emptyMessage="No models found." emptyMessage="No models found."
isEmpty={ms.filteredOptions.length === 0 && ms.isCurrentModelInCache} isEmpty={ms.filteredOptions.length === 0 && ms.isCurrentModelInCache}
@@ -292,6 +285,14 @@
searchClass="bg-transparent" searchClass="bg-transparent"
searchValue={ms.searchTerm} searchValue={ms.searchTerm}
> >
<!-- Provider tabs sit under the search input, above the option list. -->
<ModelsSelectorBackendSwitcher
activeId={ms.activeBackendId}
backends={ms.backends}
onAdd={handleAddBackend}
onSelect={(backendId) => void ms.handleBackendChange(backendId)}
/>
<!-- Option list; the search header sticks to the top and the actions <!-- Option list; the search header sticks to the top and the actions
footer to the bottom of the content scrollport. --> footer to the bottom of the content scrollport. -->
<div class="models-list px-1.5"> <div class="models-list px-1.5">
@@ -91,7 +91,7 @@
? Math.round(modelLoadFraction(modelsStore.status.getLoadProgress(triggerModel)) * 100) ? Math.round(modelLoadFraction(modelsStore.status.getLoadProgress(triggerModel)) * 100)
: 0} : 0}
{#if ms.isMultiModel} {#if ms.isMultiModel || ms.switchingBackends}
<button <button
class={[ class={[
`relative inline-flex cursor-pointer items-center gap-1.5 rounded-sm bg-background px-1.5 py-1 text-xs shadow-sm transition hover:bg-muted-foreground/20 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-60 max-sm:px-3 max-sm:py-2 max-sm:text-sm dark:bg-muted-foreground/15 dark:text-secondary-foreground`, `relative inline-flex cursor-pointer items-center gap-1.5 rounded-sm bg-background px-1.5 py-1 text-xs shadow-sm transition hover:bg-muted-foreground/20 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-60 max-sm:px-3 max-sm:py-2 max-sm:text-sm dark:bg-muted-foreground/15 dark:text-secondary-foreground`,
@@ -127,7 +127,7 @@
<Lightbulb class="h-3.5 w-3.5 shrink-0 text-amber-400" /> <Lightbulb class="h-3.5 w-3.5 shrink-0 text-amber-400" />
{/if} {/if}
{#if ms.updating || ms.isLoadingModel} {#if ms.updating || ms.isLoadingModel || ms.switchingBackends}
<Loader2 class="h-3 w-3.5 shrink-0 animate-spin" /> <Loader2 class="h-3 w-3.5 shrink-0 animate-spin" />
{:else} {:else}
<ChevronDown class="h-3 w-3.5 shrink-0" /> <ChevronDown class="h-3 w-3.5 shrink-0" />
@@ -149,13 +149,6 @@
</Sheet.Header> </Sheet.Header>
<div class="flex flex-col gap-1 pb-4"> <div class="flex flex-col gap-1 pb-4">
<ModelsSelectorBackendSwitcher
activeId={ms.activeBackendId}
backends={ms.backends}
onAdd={handleAddBackend}
onSelect={(backendId) => void ms.handleBackendChange(backendId)}
/>
<div class="mb-3 px-4"> <div class="mb-3 px-4">
<SearchInput <SearchInput
onInput={(v) => ms.setSearchTerm(v)} onInput={(v) => ms.setSearchTerm(v)}
@@ -164,6 +157,14 @@
/> />
</div> </div>
<!-- Provider tabs sit under the search input, above the option list. -->
<ModelsSelectorBackendSwitcher
activeId={ms.activeBackendId}
backends={ms.backends}
onAdd={handleAddBackend}
onSelect={(backendId) => void ms.handleBackendChange(backendId)}
/>
<div class="max-h-[60vh] overflow-y-auto px-2"> <div class="max-h-[60vh] overflow-y-auto px-2">
{#if !ms.isCurrentModelInCache && currentModel} {#if !ms.isCurrentModelInCache && currentModel}
<button <button
@@ -30,6 +30,7 @@ export interface UseModelsSelectorReturn {
readonly filteredOptions: ModelOption[]; readonly filteredOptions: ModelOption[];
readonly groupedFilteredOptions: ReturnType<typeof groupModelOptions>; readonly groupedFilteredOptions: ReturnType<typeof groupModelOptions>;
readonly isLoadingModel: boolean; readonly isLoadingModel: boolean;
readonly switchingBackends: boolean;
readonly searchTerm: string; readonly searchTerm: string;
readonly showModelDialog: boolean; readonly showModelDialog: boolean;
readonly infoModelId: string | null; readonly infoModelId: string | null;
@@ -88,6 +89,7 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele
}); });
let isLoadingModel = $state(false); let isLoadingModel = $state(false);
let switchingBackends = $state(false);
let searchTerm = $state(''); let searchTerm = $state('');
let showModelDialog = $state(false); let showModelDialog = $state(false);
let infoModelId = $state<string | null>(null); let infoModelId = $state<string | null>(null);
@@ -141,11 +143,18 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele
backendsStore.setActive(backendId); backendsStore.setActive(backendId);
searchTerm = ''; searchTerm = '';
// keep the multi-model selector mounted while the new backend's props and
// models load; role flips (external MODEL mode -> local ROUTER mode) would
// otherwise unmount and remount the open dropdown mid switch
switchingBackends = true;
try { try {
await backendsModelsStore.ensureLoaded(backendId); await backendsModelsStore.ensureLoaded(backendId);
await modelsStore.switchBackend(); await modelsStore.switchBackend();
} catch (error) { } catch (error) {
console.error('Failed to switch backend:', error); console.error('Failed to switch backend:', error);
} finally {
switchingBackends = false;
} }
} }
@@ -321,6 +330,10 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele
return showModelDialog; return showModelDialog;
}, },
get switchingBackends() {
return switchingBackends;
},
get updating() { get updating() {
return updating; return updating;
} }