Commit b12b405c1 for llama.cpp

commit b12b405c171bc2ec98ce7a860a69c073dcac41b3
Author: Aleksander Grygier <aleksander.grygier@gmail.com>
Date:   Mon Nov 2 12:33:28 2026 +0100

    ui: don't reload a loading model on select, prefer it in pre-selection (#31326)

    * ui: don't reload a loading model on select, prefer it in pre-selection

    Assisted-by: Claude (Opus 5.5 Medium)

    * ui: skip hidden models in automatic model selection

    Assisted-by: Claude (Opus 5.5 Medium)

    * ui: don't count a model being unloaded as loading

    Assisted-by: Claude (Opus 5.5 Medium)

diff --git a/tools/ui/src/lib/hooks/use-chat-form-model.svelte.ts b/tools/ui/src/lib/hooks/use-chat-form-model.svelte.ts
index a32acba6a..c86f492a8 100644
--- a/tools/ui/src/lib/hooks/use-chat-form-model.svelte.ts
+++ b/tools/ui/src/lib/hooks/use-chat-form-model.svelte.ts
@@ -2,10 +2,10 @@
  * Model state for the chat form's action bar.
  *
  * Syncs the conversation's model into the global selection (a conversation
- * opened from history names the model that produced it), falls back to the
- * first loaded model on a router, and reactively tracks which modalities
- * (vision / audio / video) the active model supports - fetching model props
- * from the server on demand if they are not cached yet.
+ * opened from history names the model that produced it), falls back to a
+ * loading, then the first loaded, model on a router, and reactively tracks
+ * which modalities (vision / audio / video) the active model supports -
+ * fetching model props from the server on demand if they are not cached yet.
  */

 import { conversationsStore, modelsStore, serverStore } from '$lib/stores';
@@ -36,12 +36,16 @@ export function useChatFormModel() {
 		} else if (
 			isRouter &&
 			!modelsStore.selectedModelId &&
-			modelsStore.loadedModelIds.length > 0 &&
 			conversationsStore.activeMessages.length > 0 &&
 			!conversationModel
 		) {
 			lastSyncedConversationModel = null;
-			const first = modelsStore.models.find((m) => modelsStore.loadedModelIds.includes(m.model));
+			// a model that is loading wins over one that is already up; a model the
+			// selector doesn't list is never picked
+			const visible = modelsStore.getVisibleModels();
+			const first =
+				visible.find((m) => modelsStore.isModelLoading(m.model)) ??
+				visible.find((m) => modelsStore.loadedModelIds.includes(m.model));

 			if (first) modelsStore.selectModelById(first.id);
 		}
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 844a7d4fb..6fe35e444 100644
--- a/tools/ui/src/lib/hooks/use-models-selector.svelte.ts
+++ b/tools/ui/src/lib/hooks/use-models-selector.svelte.ts
@@ -167,8 +167,14 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele
 			});
 		}

-		// only the built-in server loads on request, and only in router mode
-		if (!onModelChange && isRouter && !modelsStore.isModelLoaded(option.model)) {
+		// only the built-in server loads on request, and only in router mode; a model
+		// that is already loading is only selected, not loaded a second time
+		if (
+			!onModelChange &&
+			isRouter &&
+			!modelsStore.isModelLoaded(option.model) &&
+			!modelsStore.isModelLoading(option.model)
+		) {
 			isLoadingModel = true;

 			modelsStore.status
diff --git a/tools/ui/src/lib/stores/models/index.svelte.ts b/tools/ui/src/lib/stores/models/index.svelte.ts
index 79bd5b1ad..0a683c24b 100644
--- a/tools/ui/src/lib/stores/models/index.svelte.ts
+++ b/tools/ui/src/lib/stores/models/index.svelte.ts
@@ -205,12 +205,22 @@ class ModelsStore implements ModelPropsHost, ModelStatusHost {
 	 * Prioritizes:
 	 * 1. Model from active conversation's last assistant response (if loaded)
 	 * 2. Model from active conversation's last assistant response (if not loaded)
-	 * 3. First loaded model (not from active conversation)
-	 * 4. A favorite model
-	 * 5. First available model
+	 * 3. A model that is loading
+	 * 4. First loaded model (not from active conversation)
+	 * 5. A favorite model
+	 * 6. First available model
+	 *
+	 * A chat without a model of its own also trades an idle selection for a
+	 * model that is loading.
 	 */
 	async ensureFirstModelSelected(): Promise<void> {
-		if (this.selectedModelName) return;
+		if (this.selectedModelName) {
+			const loadingModel = this.getLoadingModelToPreselect();
+
+			if (loadingModel) await this.selectModelById(loadingModel.id);
+
+			return;
+		}

 		const availableModels = this.getVisibleModels();

@@ -233,6 +243,15 @@ class ModelsStore implements ModelPropsHost, ModelStatusHost {
 			}
 		}

+		// a model that is loading is the one the user is waiting for
+		const loadingModel = availableModels.find((m) => this.isModelLoading(m.model));
+
+		if (loadingModel) {
+			await this.selectModelById(loadingModel.id);
+
+			return;
+		}
+
 		// Try a loaded model first
 		const loadedModel = availableModels.find((m) => this.isModelLoaded(m.model));

@@ -321,6 +340,20 @@ class ModelsStore implements ModelPropsHost, ModelStatusHost {
 		);
 	}

+	/**
+	 * The loading model a chat without a model of its own should switch to: none
+	 * while the current selection is loaded or loading itself.
+	 */
+	getLoadingModelToPreselect(): ModelOption | null {
+		if (this.getModelFromLastAssistantResponse()) return null;
+
+		const selected = this.selectedModelName;
+
+		if (selected && (this.isModelLoaded(selected) || this.isModelLoading(selected))) return null;
+
+		return this.getVisibleModels().find((m) => this.isModelLoading(m.model)) ?? null;
+	}
+
 	/**
 	 * Gets the model name from the last assistant message in the active conversation.
 	 * Used by both the chat page and settings page to maintain model consistency.
@@ -345,6 +378,16 @@ class ModelsStore implements ModelPropsHost, ModelStatusHost {
 		return (model?.status?.value as ServerModelStatus) ?? null;
 	}

+	/**
+	 * Models the selector lists: not flagged out of the UI (ui === false) and not
+	 * hidden by the user, so an automatic pick never lands on one it doesn't show.
+	 */
+	getVisibleModels(): ModelOption[] {
+		return this.models.filter(
+			(option) => this.props.getModelProps(option.model)?.ui !== false && !this.isHidden(option.id)
+		);
+	}
+
 	hasModel(modelName: string): boolean {
 		return this.models.some((model) => model.model === modelName);
 	}
@@ -367,6 +410,15 @@ class ModelsStore implements ModelPropsHost, ModelStatusHost {
 		return status === ServerModelStatus.LOADED || status === ServerModelStatus.SLEEPING;
 	}

+	/** Loading on the server, or a load this client started that has not finished yet. */
+	isModelLoading(modelId: string): boolean {
+		// an unload raises the same in-progress flag, but the model stays loaded until it ends
+		return (
+			this.getModelStatus(modelId) === ServerModelStatus.LOADING ||
+			(this._status.isOperationInProgress(modelId) && !this.isModelLoaded(modelId))
+		);
+	}
+
 	/** Loaded, or sleeping, and not mid-operation: what a row shows as running. */
 	isModelRunning(modelId: string): boolean {
 		return this.isModelLoaded(modelId) && !this._status.isOperationInProgress(modelId);
@@ -422,13 +474,24 @@ class ModelsStore implements ModelPropsHost, ModelStatusHost {
 	}

 	/**
-	 * Auto-selects the model from the last assistant response if available and loaded.
+	 * Auto-selects the model from the last assistant response if available and loaded,
+	 * or a loading model when the conversation has no model yet.
 	 * Returns true if a model was selected, false otherwise.
 	 */
 	async selectModelFromLastAssistantResponse(): Promise<boolean> {
 		const lastModel = this.getModelFromLastAssistantResponse();

-		if (!lastModel || this.selectedModelName === lastModel) return false;
+		if (!lastModel) {
+			const loadingModel = this.getLoadingModelToPreselect();
+
+			if (!loadingModel) return false;
+
+			await this.selectModelById(loadingModel.id);
+
+			return true;
+		}
+
+		if (this.selectedModelName === lastModel) return false;

 		const matchingModel = this.models.find((option) => option.model === lastModel);

@@ -612,19 +675,12 @@ class ModelsStore implements ModelPropsHost, ModelStatusHost {
 				})
 		);
 	}
-
 	/** Fetch models in MODEL mode (single model, standard OpenAI-compatible). */
 	private async fetchModelModeInternal(): Promise<ModelOption[]> {
 		const response = await ModelsService.list();

 		return this.buildModelOptions(response);
 	}
-	/**
-	 * Filter to models visible in the UI (ui !== false).
-	 */
-	private getVisibleModels(): ModelOption[] {
-		return this.models.filter((option) => this.props.getModelProps(option.model)?.ui !== false);
-	}

 	private loadFavoritesFromStorage(): Set<string> {
 		try {