| 1 | import { useMemo } from "react"; |
| 2 | |
| 3 | import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell"; |
| 4 | import { |
| 5 | normalizeProviderAccentColor, |
| 6 | resolveProviderInstanceDisplayName, |
| 7 | shouldShowInstanceBadge, |
| 8 | } from "@t3tools/client-runtime/state/provider-instance-display"; |
| 9 | import type { EnvironmentId, ProviderDriverKind } from "@t3tools/contracts"; |
| 10 | |
| 11 | import type { ThreadListProvider } from "../../state/thread-list-environments"; |
| 12 | /** What a thread row needs to draw the provider glyph and its account badge. */ |
| 13 | export interface ThreadRowProviderInstance { |
| 14 | readonly driverKind: ProviderDriverKind; |
| 15 | readonly displayName: string; |
| 16 | readonly accentColor?: string | undefined; |
| 17 | readonly showBadge: boolean; |
| 18 | } |
| 19 | |
| 20 | /** |
| 21 | * Resolve the provider instance a thread runs on, scoped to the thread's own |
| 22 | * environment: default instance ids are the driver slug, so the same id |
| 23 | * names a different account on every server. |
| 24 | */ |
| 25 | export function resolveThreadProviderInstance( |
| 26 | providers: ReadonlyArray<ThreadListProvider> | undefined, |
| 27 | thread: EnvironmentThreadShell, |
| 28 | ): ThreadRowProviderInstance | null { |
| 29 | if (providers === undefined) return null; |
| 30 | const instanceId = thread.runtime?.providerInstanceId ?? thread.modelSelection.instanceId; |
| 31 | const snapshot = providers.find((provider) => provider.instanceId === instanceId); |
| 32 | if (!snapshot) return null; |
| 33 | const entry = { |
| 34 | driverKind: snapshot.driver, |
| 35 | displayName: resolveProviderInstanceDisplayName(snapshot), |
| 36 | accentColor: normalizeProviderAccentColor(snapshot.accentColor), |
| 37 | }; |
| 38 | return { |
| 39 | ...entry, |
| 40 | showBadge: shouldShowInstanceBadge( |
| 41 | entry, |
| 42 | providers.map((provider) => ({ driverKind: provider.driver })), |
| 43 | ), |
| 44 | }; |
| 45 | } |
| 46 | |
| 47 | /** |
| 48 | * Builds a resolver handing out reference-stable `ThreadRowProviderInstance` |
| 49 | * objects. `resolveThreadProviderInstance` builds a fresh object per call, |
| 50 | * which breaks the memoized row's props comparison on every parent render — |
| 51 | * the result only depends on (environment, instance id), so one cache per |
| 52 | * provider-list generation keeps each row's `providerInstance` prop stable |
| 53 | * until the instance behind the row actually changes. |
| 54 | */ |
| 55 | export function createThreadRowProviderInstanceResolver( |
| 56 | providersByEnvironmentId: ReadonlyMap<EnvironmentId, ReadonlyArray<ThreadListProvider>>, |
| 57 | ): (thread: EnvironmentThreadShell) => ThreadRowProviderInstance | null { |
| 58 | const cache = new Map<string, ThreadRowProviderInstance | null>(); |
| 59 | return (thread) => { |
| 60 | const instanceId = thread.runtime?.providerInstanceId ?? thread.modelSelection.instanceId; |
| 61 | const cacheKey = `${thread.environmentId}|${instanceId ?? ""}`; |
| 62 | const cached = cache.get(cacheKey); |
| 63 | if (cached !== undefined) return cached; |
| 64 | const resolved = resolveThreadProviderInstance( |
| 65 | providersByEnvironmentId.get(thread.environmentId), |
| 66 | thread, |
| 67 | ); |
| 68 | cache.set(cacheKey, resolved); |
| 69 | return resolved; |
| 70 | }; |
| 71 | } |
| 72 | |
| 73 | /** List-scoped wrapper: one cache per provider-list generation. */ |
| 74 | export function useThreadRowProviderInstanceResolver( |
| 75 | providersByEnvironmentId: ReadonlyMap<EnvironmentId, ReadonlyArray<ThreadListProvider>>, |
| 76 | ): (thread: EnvironmentThreadShell) => ThreadRowProviderInstance | null { |
| 77 | return useMemo( |
| 78 | () => createThreadRowProviderInstanceResolver(providersByEnvironmentId), |
| 79 | [providersByEnvironmentId], |
| 80 | ); |
| 81 | } |