diff --git a/tools/server/server-context.cpp b/tools/server/server-context.cpp
index 0f3fb9efa..6b16c6b49 100644
--- a/tools/server/server-context.cpp
+++ b/tools/server/server-context.cpp
@@ -3885,6 +3885,7 @@ void server_routes::init_routes() {
{ "eos_token", meta->eos_token_str },
{ "build_info", meta->build_info },
{ "is_sleeping", queue_tasks.is_sleeping() },
+ { "cors_proxy_enabled", params.ui_mcp_proxy || params.webui_mcp_proxy },
};
if (params.use_jinja) {
if (!tmpl_tools.empty()) {
diff --git a/tools/server/server-models.cpp b/tools/server/server-models.cpp
index 6c6fed52d..ccf42320f 100644
--- a/tools/server/server-models.cpp
+++ b/tools/server/server-models.cpp
@@ -1165,6 +1165,7 @@ void server_models_routes::init_routes() {
// Deprecated: use ui_settings instead (kept for backward compat)
{"webui_settings", webui_settings},
{"build_info", std::string(llama_build_info())},
+ {"cors_proxy_enabled", params.ui_mcp_proxy || params.webui_mcp_proxy},
});
return res;
}
diff --git a/tools/ui/.env.example b/tools/ui/.env.example
new file mode 100644
index 000000000..9a995b746
--- /dev/null
+++ b/tools/ui/.env.example
@@ -0,0 +1,2 @@
+VITE_PUBLIC_APP_NAME='llama-ui'
+# VITE_DEBUG='true'
diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionModels.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionModels.svelte
index 2f9471e0d..297020605 100644
--- a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionModels.svelte
+++ b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormActions/ChatFormActionModels.svelte
@@ -7,7 +7,6 @@
import { activeMessages } from '$lib/stores/conversations.svelte';
interface Props {
- currentModel?: string;
disabled?: boolean;
forceForegroundText?: boolean;
hasAudioModality?: boolean;
@@ -20,7 +19,6 @@
}
let {
- currentModel,
disabled = false,
forceForegroundText = false,
hasAudioModality = $bindable(false),
@@ -41,14 +39,28 @@
let lastSyncedConversationModel: string | null = null;
+ let selectorModel = $derived(conversationModel ?? modelsStore.selectedModelName ?? null);
+
$effect(() => {
if (conversationModel && conversationModel !== lastSyncedConversationModel) {
- lastSyncedConversationModel = conversationModel;
+ if (modelOptions().some((m) => m.model === conversationModel)) {
+ modelsStore.selectedModelName = conversationModel;
+ modelsStore.selectModelByName(conversationModel);
+ } else {
+ modelsStore.selectedModelName = null;
+ modelsStore.clearSelection();
+ }
- modelsStore.selectModelByName(conversationModel);
- } else if (isRouter && !modelsStore.selectedModelId && modelsStore.loadedModelIds.length > 0) {
+ lastSyncedConversationModel = conversationModel;
+ } else if (
+ isRouter &&
+ !modelsStore.selectedModelId &&
+ modelsStore.loadedModelIds.length > 0 &&
+ activeMessages().length > 0 &&
+ !conversationModel
+ ) {
lastSyncedConversationModel = null;
- // auto-select the first loaded model only when nothing is selected yet
+
const first = modelOptions().find((m) => modelsStore.loadedModelIds.includes(m.model));
if (first) modelsStore.selectModelById(first.id);
@@ -151,7 +163,7 @@
@@ -159,7 +171,7 @@
diff --git a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormPickerMcpPrompts/ChatFormPickerMcpPrompts.svelte b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormPickerMcpPrompts/ChatFormPickerMcpPrompts.svelte
index 567fdac47..ff734ac88 100644
--- a/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormPickerMcpPrompts/ChatFormPickerMcpPrompts.svelte
+++ b/tools/ui/src/lib/components/app/chat/ChatForm/ChatFormPickers/ChatFormPickerMcpPrompts/ChatFormPickerMcpPrompts.svelte
@@ -162,7 +162,7 @@
return;
}
- if (import.meta.env.DEV) {
+ if (import.meta.env.DEV && import.meta.env.VITE_DEBUG) {
console.log('[ChatFormPickerMcpPrompts] Fetching completions for:', {
serverName: selectedPrompt.serverName,
promptName: selectedPrompt.name,
@@ -181,7 +181,7 @@
value
);
- if (import.meta.env.DEV) {
+ if (import.meta.env.DEV && import.meta.env.VITE_DEBUG) {
console.log('[ChatFormPickerMcpPrompts] Autocomplete result:', {
argName,
value,
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 537a2af18..098cb2c27 100644
--- a/tools/ui/src/lib/hooks/use-models-selector.svelte.ts
+++ b/tools/ui/src/lib/hooks/use-models-selector.svelte.ts
@@ -66,7 +66,6 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele
const serverModel = $derived(singleModelName());
const currentModel = $derived(opts.currentModel());
- const useGlobalSelection = $derived(opts.useGlobalSelection?.() ?? false);
const onModelChange = $derived(opts.onModelChange?.());
const isHighlightedCurrentModelActive = $derived.by(() => {
@@ -128,6 +127,7 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele
if (onModelChange) {
const result = await onModelChange(option.id, option.model);
+
if (result === false) {
shouldCloseMenu = false;
}
@@ -142,12 +142,14 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele
const textarea = document.querySelector(
'[data-slot="chat-form"] textarea'
);
+
textarea?.focus();
});
}
if (!onModelChange && isRouter && !modelsStore.isModelLoaded(option.model)) {
isLoadingModel = true;
+
modelsStore
.loadModel(option.model)
.catch((error) => console.error('Failed to load model:', error))
@@ -158,6 +160,7 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele
function getDisplayOption(): ModelOption | undefined {
if (!isRouter) {
const displayModel = serverModel || currentModel;
+
if (displayModel) {
return {
id: serverModel ? 'current' : 'offline-current',
@@ -166,12 +169,8 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele
capabilities: []
};
}
- return undefined;
- }
- if (useGlobalSelection && activeId) {
- const selected = options.find((option) => option.id === activeId);
- if (selected) return selected;
+ return undefined;
}
if (currentModel) {
@@ -183,6 +182,7 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele
capabilities: []
};
}
+
return options.find((option) => option.model === currentModel);
}
@@ -197,57 +197,77 @@ export function useModelsSelector(opts: UseModelsSelectorOptions): UseModelsSele
get options() {
return options;
},
+
get loading() {
return loading;
},
+
get updating() {
return updating;
},
+
get activeId() {
return activeId;
},
+
get isRouter() {
return isRouter;
},
+
get serverModel() {
return serverModel;
},
+
get isHighlightedCurrentModelActive() {
return isHighlightedCurrentModelActive;
},
+
get isCurrentModelInCache() {
return isCurrentModelInCache;
},
+
get filteredOptions() {
return filteredOptions;
},
+
get groupedFilteredOptions() {
return groupedFilteredOptions;
},
+
get isLoadingModel() {
return isLoadingModel;
},
+
get searchTerm() {
return searchTerm;
},
+
get showModelDialog() {
return showModelDialog;
},
+
get infoModelId() {
return infoModelId;
},
+
setSearchTerm(value: string) {
searchTerm = value;
},
+
setShowModelDialog(value: boolean) {
showModelDialog = value;
},
+
handleInfoClick,
+
handleSelect,
+
handleOpenChange,
+
isFavorite(model: string) {
return modelsStore.favoriteModelIds.has(model);
},
+
getDisplayOption
};
}
diff --git a/tools/ui/src/lib/services/mcp.service.ts b/tools/ui/src/lib/services/mcp.service.ts
index 44cbd4a8a..d596381aa 100644
--- a/tools/ui/src/lib/services/mcp.service.ts
+++ b/tools/ui/src/lib/services/mcp.service.ts
@@ -392,7 +392,7 @@ export class MCPService {
const url = new URL(config.url);
- if (import.meta.env.DEV) {
+ if (import.meta.env.DEV && import.meta.env.VITE_DEBUG) {
console.log(`[MCPService] Creating WebSocket transport for ${url.href}`);
}
@@ -413,12 +413,12 @@ export class MCPService {
onLog
);
- if (useProxy && import.meta.env.DEV) {
+ if (useProxy && import.meta.env.DEV && import.meta.env.VITE_DEBUG) {
console.log(`[MCPService] Using CORS proxy for ${config.url} -> ${url.href}`);
}
try {
- if (import.meta.env.DEV) {
+ if (import.meta.env.DEV && import.meta.env.VITE_DEBUG) {
console.log(`[MCPService] Creating StreamableHTTP transport for ${url.href}`);
}
@@ -520,7 +520,7 @@ export class MCPService {
)
);
- if (import.meta.env.DEV) {
+ if (import.meta.env.DEV && import.meta.env.VITE_DEBUG) {
console.log(`[MCPService][${serverName}] Creating transport...`);
}
@@ -560,6 +560,22 @@ export class MCPService {
);
const runtimeErrorHandler = (error: Error) => {
+ // Ignore errors that are expected when the SDK's transport is closed,
+ // or when connecting to servers that don't support SSE (stateless-only
+ // endpoints returning 405). The SDK wraps the original AbortError in
+ // a new Error with the message "SSE stream disconnected: AbortError",
+ // and also produces "Cannot cancel a stream locked by a reader".
+ // DOMException is thrown by the browser when aborting fetch requests.
+ const msg = error.message || String(error);
+ if (
+ error.name === 'AbortError' ||
+ error instanceof DOMException ||
+ msg.includes('SSE stream disconnected') ||
+ msg.includes('stream locked by a reader') ||
+ msg.includes('The operation was aborted')
+ ) {
+ return;
+ }
console.error(`[MCPService][${serverName}] Protocol error after initialize:`, error);
};
@@ -658,7 +674,10 @@ export class MCPService {
this.createLog(MCPConnectionPhase.LISTING_TOOLS, 'Listing available tools...')
);
- console.log(`[MCPService][${serverName}] Connected, listing tools...`);
+ if (import.meta.env.DEV && import.meta.env.VITE_DEBUG) {
+ console.log(`[MCPService][${serverName}] Connected, listing tools...`);
+ }
+
const tools = await this.listTools({
client,
transport,
@@ -680,10 +699,11 @@ export class MCPService {
`Connection established with ${tools.length} tools (${connectionTimeMs}ms)`
)
);
-
- console.log(
- `[MCPService][${serverName}] Initialization complete with ${tools.length} tools in ${connectionTimeMs}ms`
- );
+ if (import.meta.env.DEV && import.meta.env.VITE_DEBUG) {
+ console.log(
+ `[MCPService][${serverName}] Initialization complete with ${tools.length} tools in ${connectionTimeMs}ms`
+ );
+ }
return {
client,
@@ -709,9 +729,22 @@ export class MCPService {
* @param connection - The active MCP connection to close
*/
static async disconnect(connection: MCPConnection): Promise {
- console.log(`[MCPService][${connection.serverName}] Disconnecting...`);
+ if (import.meta.env.DEV && import.meta.env.VITE_DEBUG) {
+ console.log(`[MCPService][${connection.serverName}] Disconnecting...`);
+ }
+
try {
- // Prevent reconnection on voluntary disconnect
+ // Terminate the session first for streamable-http transports to cleanly
+ // close streams, matching the inspector's disconnect flow.
+ if (connection.transport instanceof StreamableHTTPClientTransport) {
+ await connection.transport.terminateSession();
+ }
+
+ // Clear error handlers before closing to prevent noise from expected
+ // abort errors during shutdown. The inspector avoids this entirely
+ // by not setting onerror, but since we use it for protocol logging,
+ // we must clear it before disconnect.
+ connection.client.onerror = undefined;
if (connection.transport.onclose) {
connection.transport.onclose = undefined;
}
@@ -1078,7 +1111,9 @@ export class MCPService {
try {
await connection.client.unsubscribeResource({ uri });
- console.log(`[MCPService][${connection.serverName}] Unsubscribed from resource: ${uri}`);
+ if (import.meta.env.DEV && import.meta.env.VITE_DEBUG) {
+ console.log(`[MCPService][${connection.serverName}] Unsubscribed from resource: ${uri}`);
+ }
} catch (error) {
console.error(
`[MCPService][${connection.serverName}] Failed to unsubscribe from resource:`,
diff --git a/tools/ui/src/lib/services/migration.service.ts b/tools/ui/src/lib/services/migration.service.ts
index 5ed24c00d..35d47070a 100644
--- a/tools/ui/src/lib/services/migration.service.ts
+++ b/tools/ui/src/lib/services/migration.service.ts
@@ -119,7 +119,8 @@ const localStorageMigration: Migration = {
// Only migrate if new key doesn't already exist
const newValue = localStorage.getItem(newKey);
if (newValue !== null) {
- console.log(`[Migration] localStorage: ${newKey} already exists, skipping`);
+ if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
+ console.log(`[Migration] localStorage: ${newKey} already exists, skipping`);
continue;
}
@@ -127,9 +128,11 @@ const localStorageMigration: Migration = {
if (oldValue !== null) {
localStorage.setItem(newKey, oldValue);
// Keep old key for downgrade compatibility - DO NOT DELETE
- console.log(
- `[Migration] localStorage: copied ${deprecatedKey} → ${newKey} (preserved old)`
- );
+ if (import.meta.env.DEV && import.meta.env.VITE_DEBUG) {
+ console.log(
+ `[Migration] localStorage: copied ${deprecatedKey} → ${newKey} (preserved old)`
+ );
+ }
}
}
}
@@ -146,7 +149,8 @@ const idxdbMigration: Migration = {
async run(): Promise {
const oldDbNames = await Dexie.getDatabaseNames();
if (!oldDbNames.includes(DB_APP_NAME_DEPRECATED)) {
- console.log('[Migration] IndexedDB: no old database found, skipping');
+ if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
+ console.log('[Migration] IndexedDB: no old database found, skipping');
return;
}
@@ -155,11 +159,13 @@ const idxdbMigration: Migration = {
newDb.version(1).stores(IDXDB_STORES);
const existingConvs = await newDb.table(IDXDB_TABLES.conversations).count();
if (existingConvs > 0) {
- console.log('[Migration] IndexedDB: new database already has data, skipping');
+ if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
+ console.log('[Migration] IndexedDB: new database already has data, skipping');
return;
}
- console.log('[Migration] IndexedDB: copying from', DB_APP_NAME_DEPRECATED);
+ if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
+ console.log('[Migration] IndexedDB: copying from', DB_APP_NAME_DEPRECATED);
const oldDb = new Dexie(DB_APP_NAME_DEPRECATED);
oldDb.version(1).stores(IDXDB_STORES);
@@ -169,15 +175,18 @@ const idxdbMigration: Migration = {
if (conversations.length > 0) {
await newDb.table(IDXDB_TABLES.conversations).bulkAdd(conversations);
- console.log(`[Migration] IndexedDB: copied ${conversations.length} conversations`);
+ if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
+ console.log(`[Migration] IndexedDB: copied ${conversations.length} conversations`);
}
if (messages.length > 0) {
await newDb.table(IDXDB_TABLES.messages).bulkAdd(messages);
- console.log(`[Migration] IndexedDB: copied ${messages.length} messages`);
+ if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
+ console.log(`[Migration] IndexedDB: copied ${messages.length} messages`);
}
// Non-destructive: DO NOT delete old database - keep for downgrade compatibility
- console.log('[Migration] IndexedDB: preserved old database for downgrade compatibility');
+ if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
+ console.log('[Migration] IndexedDB: preserved old database for downgrade compatibility');
}
};
@@ -419,7 +428,8 @@ const legacyMessageMigration: Migration = {
}
}
- console.log(`[Migration] Legacy messages: migrated ${migratedCount} messages`);
+ if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
+ console.log(`[Migration] Legacy messages: migrated ${migratedCount} messages`);
}
};
@@ -434,7 +444,8 @@ const themeMigration: Migration = {
async run(): Promise {
const legacyTheme = localStorage.getItem('theme');
if (legacyTheme === null) {
- console.log('[Migration] Theme: no legacy theme key found, skipping');
+ if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
+ console.log('[Migration] Theme: no legacy theme key found, skipping');
return;
}
@@ -443,7 +454,8 @@ const themeMigration: Migration = {
const config = configRaw ? JSON.parse(configRaw) : {};
if (SETTINGS_KEYS.THEME in config) {
- console.log('[Migration] Theme: config already has theme, skipping');
+ if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
+ console.log('[Migration] Theme: config already has theme, skipping');
return;
}
@@ -451,7 +463,8 @@ const themeMigration: Migration = {
localStorage.setItem(CONFIG_LOCALSTORAGE_KEY, JSON.stringify(config));
// Non-destructive: DO NOT delete legacy theme key - keep for downgrade compatibility
- console.log(`[Migration] Theme: copied standalone theme to config (preserved old key)`);
+ if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
+ console.log(`[Migration] Theme: copied standalone theme to config (preserved old key)`);
}
};
@@ -491,7 +504,8 @@ export const MigrationService = {
*/
resetState(): void {
localStorage.removeItem(MIGRATION_STATE_KEY);
- console.log('[Migration] State reset - all migrations will run again');
+ if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
+ console.log('[Migration] State reset - all migrations will run again');
},
/**
@@ -500,25 +514,30 @@ export const MigrationService = {
*/
async runAllMigrations(): Promise {
const state = getMigrationState();
- console.log('[Migration] Starting migration run, state:', state);
+ if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
+ console.log('[Migration] Starting migration run, state:', state);
for (const migration of migrations) {
if (isMigrationCompleted(migration.id)) {
- console.log(`[Migration] ${migration.id}: already completed, skipping`);
+ if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
+ console.log(`[Migration] ${migration.id}: already completed, skipping`);
continue;
}
try {
- console.log(`[Migration] ${migration.id}: running...`);
+ if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
+ console.log(`[Migration] ${migration.id}: running...`);
await migration.run();
markMigrationCompleted(migration.id);
- console.log(`[Migration] ${migration.id}: completed successfully`);
+ if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
+ console.log(`[Migration] ${migration.id}: completed successfully`);
} catch (error) {
console.error(`[Migration] ${migration.id}: failed`, error);
markMigrationFailed(migration.id);
}
}
- console.log('[Migration] All migrations complete');
+ if (import.meta.env.DEV && import.meta.env.VITE_DEBUG)
+ console.log('[Migration] All migrations complete');
}
};
diff --git a/tools/ui/src/lib/stores/mcp.svelte.ts b/tools/ui/src/lib/stores/mcp.svelte.ts
index 8fb306da8..effb78e33 100644
--- a/tools/ui/src/lib/stores/mcp.svelte.ts
+++ b/tools/ui/src/lib/stores/mcp.svelte.ts
@@ -20,11 +20,11 @@
*/
import { browser } from '$app/environment';
-import { base } from '$app/paths';
import { SETTINGS_KEYS } from '$lib/constants';
import { MCPService } from '$lib/services/mcp.service';
import { config, settingsStore } from '$lib/stores/settings.svelte';
import { mcpResourceStore } from '$lib/stores/mcp-resources.svelte';
+import { serverStore } from '$lib/stores/server.svelte';
import { mode } from 'mode-watcher';
import {
parseMcpServerSettings,
@@ -43,7 +43,6 @@ import {
ToolCallType
} from '$lib/enums';
import {
- CORS_PROXY_ENDPOINT,
DEFAULT_CACHE_TTL_MS,
DEFAULT_MCP_CONFIG,
EXPECTED_THEMED_ICON_PAIR_COUNT,
@@ -86,7 +85,6 @@ class MCPStore {
private _toolCount = $state(0);
private _connectedServers = $state([]);
private _healthChecks = $state>({});
- private _proxyAvailable = $state(false);
private connections = new Map();
private toolsIndex = new Map();
@@ -96,27 +94,8 @@ class MCPStore {
private initPromise: Promise | null = null;
private activeFlowCount = 0;
- constructor() {
- if (browser) {
- this.probeProxy();
- }
- }
-
- /**
- * Probes the CORS proxy endpoint to determine availability.
- * The endpoint is only registered when llama-server runs with --ui-mcp-proxy.
- */
- async probeProxy(): Promise {
- try {
- const response = await fetch(`${base}${CORS_PROXY_ENDPOINT}`, { method: 'HEAD' });
- this._proxyAvailable = response.status !== 404;
- } catch {
- this._proxyAvailable = false;
- }
- }
-
get isProxyAvailable(): boolean {
- return this._proxyAvailable;
+ return serverStore.props?.cors_proxy_enabled ?? false;
}
/**
diff --git a/tools/ui/src/lib/stores/models.svelte.ts b/tools/ui/src/lib/stores/models.svelte.ts
index 45981b38f..bc99d7412 100644
--- a/tools/ui/src/lib/stores/models.svelte.ts
+++ b/tools/ui/src/lib/stores/models.svelte.ts
@@ -3,7 +3,7 @@ import { toast } from 'svelte-sonner';
import { ServerModelStatus, ModelModality } from '$lib/enums';
import { ModelsService } from '$lib/services/models.service';
import { PropsService } from '$lib/services/props.service';
-import { serverStore } from '$lib/stores/server.svelte';
+import { serverStore, isRouterMode } from '$lib/stores/server.svelte';
import { TTLCache } from '$lib/utils';
import {
MODEL_PROPS_CACHE_TTL_MS,
@@ -14,14 +14,7 @@ import {
import { conversationsStore } from '$lib/stores/conversations.svelte';
/**
- * modelsStore - Reactive store for model management in both MODEL and ROUTER modes
- *
- * This store manages:
- * - Available models list
- * - Selected model for new conversations
- * - Loaded models tracking (ROUTER mode)
- * - Model usage tracking per conversation
- * - Automatic unloading of unused models
+ * modelsStore - Reactive store for model management in both MODEL and ROUTER modes.
*
* **Architecture & Relationships:**
* - **ModelsService**: Stateless service for model API communication
@@ -31,14 +24,8 @@ import { conversationsStore } from '$lib/stores/conversations.svelte';
*
* **API Inconsistency Workaround:**
* In MODEL mode, `/props` returns modalities for the single model.
- * In ROUTER mode, `/props` has no modalities - must use `/props?model=` per model.
+ * In ROUTER mode, `/props` has no modalities — must use `/props?model=` per model.
* This store normalizes this behavior so consumers don't need to know the server mode.
- *
- * **Key Features:**
- * - **MODEL mode**: Single model, always loaded
- * - **ROUTER mode**: Multi-model with load/unload capability
- * - **Auto-unload**: Automatically unloads models not used by any conversation
- * - **Lazy loading**: ensureModelLoaded() loads models on demand
*/
class ModelsStore {
/**
@@ -57,8 +44,8 @@ class ModelsStore {
selectedModelId = $state(null);
selectedModelName = $state(null);
- // dedup concurrent fetch() callers, all awaiters share the same inflight promise
- // without this, ?model= URL handler raced an in-progress fetch and saw an empty list
+ // Dedup concurrent fetch() callers — all awaiters share the same inflight promise.
+ // Without this, ?model= URL handler races an in-progress fetch and sees an empty list.
private inflightFetch: Promise | null = null;
private modelUsage = $state