Skip to content

Commit 441ebb4

Browse files
committed
fix(providers): auto-poll and refresh button so local provider models appear without a full reload
When Hecate starts before Ollama / LM Studio the first discovery attempt fails (connection refused), caches that failure for 30 s, and the UI has no way to pick up the result once the local server comes up — the dashboard only loads once on page open. Two fixes: - ProvidersView mounts a 30 s setInterval that calls a new lightweight refreshProviders action (/admin/providers + /v1/models only, not the full loadDashboard). Models appear automatically within one poll cycle after the local server starts, matching the backend's capabilitiesLocalFailureTTL. - A "Refresh" button in the providers header lets the operator trigger an immediate re-discovery without navigating away. The button shows "Refreshing…" and is disabled while the fetch is in flight.
1 parent 89fb04d commit 441ebb4

3 files changed

Lines changed: 49 additions & 2 deletions

File tree

ui/src/app/useRuntimeConsole.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,24 @@ export function useRuntimeConsole() {
385385
}
386386
}
387387

388+
// refreshProviders re-fetches only /admin/providers and /v1/models without
389+
// the full loadDashboard cost. Used by the ProvidersView auto-poll so local
390+
// provider model lists converge within ~30 s of starting Ollama / LM Studio,
391+
// regardless of when Hecate itself was started.
392+
async function refreshProviders() {
393+
if (!session.isAdmin || !authToken) return;
394+
try {
395+
const [pResult, mResult] = await Promise.allSettled([
396+
getProviders(authToken),
397+
getModels(authToken),
398+
]);
399+
if (pResult.status === "fulfilled") setProviders(pResult.value.data ?? []);
400+
if (mResult.status === "fulfilled") setModels(mResult.value.data ?? []);
401+
} catch {
402+
// Best-effort background refresh — ignore errors.
403+
}
404+
}
405+
388406
function buildChatPayload(messages: ChatMessage[], sessionID?: string) {
389407
return {
390408
model,
@@ -1172,6 +1190,7 @@ export function useRuntimeConsole() {
11721190
setModelFilter,
11731191
setProviderFilter: selectProviderRoute,
11741192
setProviderEnabled,
1193+
refreshProviders,
11751194
setRetentionSubsystems,
11761195
setRotateAPIKeyID,
11771196
setRotateAPIKeySecret,

ui/src/features/providers/ProvidersView.tsx

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState } from "react";
1+
import { useEffect, useState } from "react";
22
import type { RuntimeConsoleViewModel } from "../../app/useRuntimeConsole";
33
import { buildConflictMap, providerDotColor, resolvedBaseURL } from "../../lib/provider-utils";
44
import { describeCredentialState, describeHealthErrorClass, describeRoutingBlockedReason } from "../../lib/runtime-utils";
@@ -35,10 +35,25 @@ function iconColorByID(id: string): string {
3535
return PRESET_COLORS[id.toLowerCase()] ?? "var(--teal)";
3636
}
3737

38+
// Interval for background provider discovery refresh (ms). Matches the backend's
39+
// local-failure TTL (30 s) so a freshly-started Ollama / LM Studio server will
40+
// surface its models within one poll cycle.
41+
const PROVIDER_POLL_INTERVAL_MS = 30_000;
42+
3843
export function ProvidersView({ state, actions }: Props) {
3944
const [selectedID, setSelectedID] = useState<string | null>(null);
4045
const [pendingKey, setPendingKey] = useState("");
4146
const [pendingToggles, setPendingToggles] = useState<Map<string, boolean>>(new Map());
47+
const [refreshing, setRefreshing] = useState(false);
48+
49+
// Auto-poll while the providers tab is visible so local inference servers
50+
// that start after Hecate appear automatically without a full page reload.
51+
useEffect(() => {
52+
const id = setInterval(() => {
53+
void actions.refreshProviders();
54+
}, PROVIDER_POLL_INTERVAL_MS);
55+
return () => clearInterval(id);
56+
}, [actions.refreshProviders]);
4257

4358
const configuredProviders = state.adminConfig?.providers ?? [];
4459
const healthyNames = new Set(state.providers.filter(p => p.healthy).map(p => p.name));
@@ -242,11 +257,23 @@ export function ProvidersView({ state, actions }: Props) {
242257
<div style={{ display: "flex", height: "100%", overflow: "hidden" }}>
243258
{/* Provider list */}
244259
<div style={{ flex: 1, overflowY: "auto", padding: 16 }}>
245-
<div style={{ display: "flex", flexWrap: "wrap", gap: 8, marginBottom: 16 }}>
260+
<div style={{ display: "flex", flexWrap: "wrap", gap: 8, marginBottom: 16, alignItems: "center" }}>
246261
<Badge status="ok" label={`${readyCount} route ready`} />
247262
<Badge status={blockedCount > 0 ? "warn" : "ok"} label={`${blockedCount} blocked`} />
248263
<Badge status={unhealthyCount > 0 ? "error" : "ok"} label={`${unhealthyCount} unhealthy`} />
249264
<Badge status={misconfiguredCount > 0 ? "warn" : "ok"} label={`${misconfiguredCount} missing credentials`} />
265+
<button
266+
className="btn btn-ghost btn-sm"
267+
style={{ marginLeft: "auto", display: "flex", alignItems: "center", gap: 4, opacity: refreshing ? 0.5 : 1 }}
268+
disabled={refreshing}
269+
title="Re-discover models from all local providers"
270+
onClick={() => {
271+
setRefreshing(true);
272+
void actions.refreshProviders().finally(() => setRefreshing(false));
273+
}}>
274+
<Icon d={Icons.refresh} size={12} />
275+
{refreshing ? "Refreshing…" : "Refresh"}
276+
</button>
250277
</div>
251278
{renderSection(
252279
"Cloud providers",

ui/src/test/runtime-console-fixture.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ export function createRuntimeConsoleActions(): RuntimeConsoleViewModel["actions"
116116
setModelFilter: () => undefined,
117117
setProviderFilter: () => undefined,
118118
setProviderEnabled: async () => undefined,
119+
refreshProviders: async () => undefined,
119120
setRetentionSubsystems: () => undefined,
120121
setRotateAPIKeyID: () => undefined,
121122
setRotateAPIKeySecret: () => undefined,

0 commit comments

Comments
 (0)