1import { useMemo } from "react";
2
3import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell";
4import {
5 normalizeProviderAccentColor,
6 resolveProviderInstanceDisplayName,
7 shouldShowInstanceBadge,
8} from "@t3tools/client-runtime/state/provider-instance-display";
9import type { EnvironmentId, ProviderDriverKind } from "@t3tools/contracts";
10
11import type { ThreadListProvider } from "../../state/thread-list-environments";
12/** What a thread row needs to draw the provider glyph and its account badge. */
13export 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 */
25export 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 */
55export 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. */
74export function useThreadRowProviderInstanceResolver(
75 providersByEnvironmentId: ReadonlyMap<EnvironmentId, ReadonlyArray<ThreadListProvider>>,
76): (thread: EnvironmentThreadShell) => ThreadRowProviderInstance | null {
77 return useMemo(
78 () => createThreadRowProviderInstanceResolver(providersByEnvironmentId),
79 [providersByEnvironmentId],
80 );
81}