From 2692211ef91bba64f321e41d1a841c36b5bb3dba Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 25 Jun 2026 14:11:25 +0700 Subject: [PATCH] fix(providers): prevent stale models during refresh Provider settings sheets can remain mounted while hidden, so the old display cache could survive into a different provider. Scope the cache to the active provider identity and remount the sheet when that identity changes. --- .../provider-diagnostic-models.test.ts | 69 +++++++++++++++++++ .../components/provider-diagnostic-models.ts | 43 ++++++++++++ .../components/provider-diagnostic-sheet.tsx | 27 ++++---- .../src/components/provider-settings-host.tsx | 1 + 4 files changed, 126 insertions(+), 14 deletions(-) create mode 100644 packages/app/src/components/provider-diagnostic-models.test.ts create mode 100644 packages/app/src/components/provider-diagnostic-models.ts diff --git a/packages/app/src/components/provider-diagnostic-models.test.ts b/packages/app/src/components/provider-diagnostic-models.test.ts new file mode 100644 index 000000000..37159c75a --- /dev/null +++ b/packages/app/src/components/provider-diagnostic-models.test.ts @@ -0,0 +1,69 @@ +import { describe, expect, it } from "vitest"; +import type { AgentModelDefinition } from "@getpaseo/protocol/agent-types"; +import { + resolveProviderDiscoveredModels, + type ProviderDiscoveredModelsCache, +} from "./provider-diagnostic-models"; + +const piModel: AgentModelDefinition = { + provider: "pi", + id: "pi/model", + label: "Pi Model", +}; + +const grokModel: AgentModelDefinition = { + provider: "grok", + id: "grok-build", + label: "Grok Build", +}; + +function resolveModels(input: { + serverId?: string; + provider: string; + currentModels?: AgentModelDefinition[]; + loading?: boolean; + cache?: ProviderDiscoveredModelsCache | null; +}) { + return resolveProviderDiscoveredModels({ + serverId: input.serverId ?? "local", + provider: input.provider, + currentModels: input.currentModels, + providerSnapshotRefreshing: input.loading === true, + previousCache: input.cache ?? null, + }); +} + +describe("resolveProviderDiscoveredModels", () => { + it("keeps a provider's cached discovered models visible while that provider refreshes", () => { + const ready = resolveModels({ provider: "grok", currentModels: [grokModel] }); + + const refreshing = resolveModels({ provider: "grok", loading: true, cache: ready.cache }); + + expect(refreshing.models).toEqual([grokModel]); + }); + + it("does not show one provider's cached models while another provider loads", () => { + const ready = resolveModels({ provider: "pi", currentModels: [piModel] }); + + const refreshing = resolveModels({ provider: "grok", loading: true, cache: ready.cache }); + + expect(refreshing.models).toEqual([]); + }); + + it("does not show another server's cached models while the same provider loads", () => { + const ready = resolveModels({ + serverId: "server-a", + provider: "grok", + currentModels: [grokModel], + }); + + const refreshing = resolveModels({ + serverId: "server-b", + provider: "grok", + loading: true, + cache: ready.cache, + }); + + expect(refreshing.models).toEqual([]); + }); +}); diff --git a/packages/app/src/components/provider-diagnostic-models.ts b/packages/app/src/components/provider-diagnostic-models.ts new file mode 100644 index 000000000..d0368a43f --- /dev/null +++ b/packages/app/src/components/provider-diagnostic-models.ts @@ -0,0 +1,43 @@ +import type { AgentModelDefinition } from "@getpaseo/protocol/agent-types"; + +export interface ProviderDiscoveredModelsCache { + serverId: string; + provider: string; + models: AgentModelDefinition[]; +} + +export interface ResolveProviderDiscoveredModelsInput { + serverId: string; + provider: string; + currentModels: AgentModelDefinition[] | undefined; + providerSnapshotRefreshing: boolean; + previousCache: ProviderDiscoveredModelsCache | null; +} + +export interface ResolveProviderDiscoveredModelsResult { + models: AgentModelDefinition[]; + cache: ProviderDiscoveredModelsCache | null; +} + +export function resolveProviderDiscoveredModels({ + serverId, + provider, + currentModels, + providerSnapshotRefreshing, + previousCache, +}: ResolveProviderDiscoveredModelsInput): ResolveProviderDiscoveredModelsResult { + if (currentModels && currentModels.length > 0) { + const cache = { serverId, provider, models: currentModels }; + return { models: currentModels, cache }; + } + + if ( + providerSnapshotRefreshing && + previousCache?.serverId === serverId && + previousCache.provider === provider + ) { + return { models: previousCache.models, cache: previousCache }; + } + + return { models: [], cache: previousCache }; +} diff --git a/packages/app/src/components/provider-diagnostic-sheet.tsx b/packages/app/src/components/provider-diagnostic-sheet.tsx index 61bc4b04f..5d9c19978 100644 --- a/packages/app/src/components/provider-diagnostic-sheet.tsx +++ b/packages/app/src/components/provider-diagnostic-sheet.tsx @@ -32,6 +32,10 @@ import { formatTimeAgo } from "@/utils/time"; import { compareMatchScores, scoreTextFields } from "@/utils/score-match"; import type { AgentModelDefinition, AgentProvider } from "@getpaseo/protocol/agent-types"; import type { ProviderProfileModel } from "@getpaseo/protocol/provider-config"; +import { + resolveProviderDiscoveredModels, + type ProviderDiscoveredModelsCache, +} from "./provider-diagnostic-models"; interface ProviderDiagnosticSheetProps { provider: string; @@ -599,21 +603,16 @@ export function ProviderDiagnosticSheet({ : null; const modelsRefreshing = isRefreshing || providerSnapshotRefreshing; - const stableDiscoveredRef = useRef([]); + const stableDiscoveredRef = useRef(null); const currentModels = providerEntry?.models; - if (currentModels && currentModels.length > 0) { - stableDiscoveredRef.current = currentModels; - } - - const discoveredModels = useMemo(() => { - if (currentModels && currentModels.length > 0) { - return currentModels; - } - if (providerSnapshotRefreshing) { - return stableDiscoveredRef.current; - } - return []; - }, [currentModels, providerSnapshotRefreshing]); + const { models: discoveredModels, cache: nextDiscoveredCache } = resolveProviderDiscoveredModels({ + serverId, + provider, + currentModels, + providerSnapshotRefreshing, + previousCache: stableDiscoveredRef.current, + }); + stableDiscoveredRef.current = nextDiscoveredCache; const [clockTick, setClockTick] = useState(0); useEffect(() => { diff --git a/packages/app/src/components/provider-settings-host.tsx b/packages/app/src/components/provider-settings-host.tsx index ce8937a93..7518fc28f 100644 --- a/packages/app/src/components/provider-settings-host.tsx +++ b/packages/app/src/components/provider-settings-host.tsx @@ -18,6 +18,7 @@ export function ProviderSettingsHost() { return (