@stigmer/react 0.4.4 → 0.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/billing/AutoRechargeCard.js +2 -2
  2. package/billing/AutoRechargeCard.js.map +1 -1
  3. package/billing/CreditLedgerTable.js +1 -1
  4. package/billing/CreditLedgerTable.js.map +1 -1
  5. package/billing/CreditPackGrid.js +1 -1
  6. package/billing/CreditPackGrid.js.map +1 -1
  7. package/billing/LowBalanceBanner.js +1 -1
  8. package/billing/LowBalanceBanner.js.map +1 -1
  9. package/index.d.ts +2 -2
  10. package/index.d.ts.map +1 -1
  11. package/index.js +1 -1
  12. package/index.js.map +1 -1
  13. package/internal/__tests__/stream-controller.test.js +34 -1
  14. package/internal/__tests__/stream-controller.test.js.map +1 -1
  15. package/internal/stream-controller.d.ts.map +1 -1
  16. package/internal/stream-controller.js +3 -3
  17. package/internal/stream-controller.js.map +1 -1
  18. package/models/ModelRegistryContext.d.ts +21 -0
  19. package/models/ModelRegistryContext.d.ts.map +1 -0
  20. package/models/ModelRegistryContext.js +22 -0
  21. package/models/ModelRegistryContext.js.map +1 -0
  22. package/models/ModelSelector.js +3 -3
  23. package/models/ModelSelector.js.map +1 -1
  24. package/models/__tests__/useModelRegistry.test.js +127 -32
  25. package/models/__tests__/useModelRegistry.test.js.map +1 -1
  26. package/models/index.d.ts +3 -1
  27. package/models/index.d.ts.map +1 -1
  28. package/models/index.js +2 -1
  29. package/models/index.js.map +1 -1
  30. package/models/registry.d.ts +20 -12
  31. package/models/registry.d.ts.map +1 -1
  32. package/models/registry.js +51 -27
  33. package/models/registry.js.map +1 -1
  34. package/models/useModelRegistry.d.ts +11 -3
  35. package/models/useModelRegistry.d.ts.map +1 -1
  36. package/models/useModelRegistry.js +13 -5
  37. package/models/useModelRegistry.js.map +1 -1
  38. package/package.json +4 -4
  39. package/provider.d.ts.map +1 -1
  40. package/provider.js +42 -1
  41. package/provider.js.map +1 -1
  42. package/session/__tests__/useNewSessionFlow.test.js +32 -18
  43. package/session/__tests__/useNewSessionFlow.test.js.map +1 -1
  44. package/session/__tests__/usePersistedModel.test.js +26 -12
  45. package/session/__tests__/usePersistedModel.test.js.map +1 -1
  46. package/src/billing/AutoRechargeCard.tsx +2 -2
  47. package/src/billing/CreditLedgerTable.tsx +1 -1
  48. package/src/billing/CreditPackGrid.tsx +1 -1
  49. package/src/billing/LowBalanceBanner.tsx +1 -1
  50. package/src/index.ts +4 -1
  51. package/src/internal/__tests__/stream-controller.test.ts +47 -1
  52. package/src/internal/stream-controller.ts +4 -4
  53. package/src/models/ModelRegistryContext.ts +32 -0
  54. package/src/models/ModelSelector.tsx +4 -4
  55. package/src/models/__tests__/useModelRegistry.test.tsx +150 -41
  56. package/src/models/index.ts +3 -1
  57. package/src/models/registry.ts +51 -30
  58. package/src/models/useModelRegistry.ts +18 -7
  59. package/src/provider.tsx +58 -8
  60. package/src/session/__tests__/useNewSessionFlow.test.tsx +39 -18
  61. package/src/session/__tests__/usePersistedModel.test.tsx +33 -12
  62. package/src/usage/OrgUsagePanel.tsx +3 -3
  63. package/styles.css +1 -1
  64. package/usage/OrgUsagePanel.js +1 -1
  65. package/usage/OrgUsagePanel.js.map +1 -1
@@ -2,7 +2,6 @@
2
2
 
3
3
  import { useMemo } from "react";
4
4
  import {
5
- MODEL_REGISTRY,
6
5
  DEFAULT_MODEL_ID,
7
6
  DEFAULT_CURSOR_MODEL_ID,
8
7
  DISABLED_PROVIDERS,
@@ -12,6 +11,7 @@ import {
12
11
  type Provider,
13
12
  } from "./registry";
14
13
  import type { HarnessOption } from "./harness";
14
+ import { useModelRegistryContext } from "./ModelRegistryContext";
15
15
 
16
16
  /** Options for {@link useModelRegistry}. */
17
17
  export interface UseModelRegistryOptions {
@@ -31,8 +31,8 @@ export interface UseModelRegistryReturn {
31
31
  readonly models: readonly ModelInfo[];
32
32
  /** Models grouped by provider for sectioned rendering. */
33
33
  readonly byProvider: ReadonlyMap<Provider, readonly ModelInfo[]>;
34
- /** The platform default model for the current mode. */
35
- readonly defaultModel: ModelInfo;
34
+ /** The platform default model for the current mode. `undefined` while the registry is loading. */
35
+ readonly defaultModel: ModelInfo | undefined;
36
36
  /**
37
37
  * Look up a single model by its `modelId`.
38
38
  *
@@ -51,6 +51,10 @@ export interface UseModelRegistryReturn {
51
51
  * Always unambiguous, even in unified mode.
52
52
  */
53
53
  readonly getByKey: (key: string) => ModelInfo | undefined;
54
+ /** `true` while the model registry is being fetched from the API. */
55
+ readonly isLoading: boolean;
56
+ /** Non-null if the API fetch failed. Models will be empty in this case. */
57
+ readonly error: Error | null;
54
58
  }
55
59
 
56
60
  /**
@@ -61,6 +65,10 @@ export interface UseModelRegistryReturn {
61
65
  * who want full control over rendering import this hook and build
62
66
  * their own UI.
63
67
  *
68
+ * The model data is fetched from the public model registry API by
69
+ * {@link StigmerProvider} and cached in context. During loading,
70
+ * `isLoading` is `true` and `models` is empty.
71
+ *
64
72
  * **Modes:**
65
73
  * - `options.harness === "cursor"` — Cursor-harness models only
66
74
  * - `options.harness === "native"` — native models, excluding disabled providers
@@ -69,7 +77,7 @@ export interface UseModelRegistryReturn {
69
77
  * @example
70
78
  * ```tsx
71
79
  * // Unified mode — flat picker with all models
72
- * const { featured, models, getByKey } = useModelRegistry();
80
+ * const { featured, models, getByKey, isLoading } = useModelRegistry();
73
81
  *
74
82
  * // Legacy single-harness mode
75
83
  * const { models, defaultModel } = useModelRegistry({ harness: "native" });
@@ -77,11 +85,12 @@ export interface UseModelRegistryReturn {
77
85
  */
78
86
  export function useModelRegistry(options?: UseModelRegistryOptions): UseModelRegistryReturn {
79
87
  const harness = options?.harness;
88
+ const { models: allModels, isLoading, error } = useModelRegistryContext();
80
89
 
81
90
  return useMemo(() => {
82
91
  const isUnified = harness === undefined;
83
92
  const { modelId: defaultId } = harness
84
- ? resolveDefaultModelId(harness)
93
+ ? resolveDefaultModelId(harness, allModels)
85
94
  : { modelId: DEFAULT_MODEL_ID };
86
95
 
87
96
  const byProvider = new Map<Provider, ModelInfo[]>();
@@ -91,7 +100,7 @@ export function useModelRegistry(options?: UseModelRegistryOptions): UseModelReg
91
100
  const featuredModels: ModelInfo[] = [];
92
101
  let defaultModel: ModelInfo | undefined;
93
102
 
94
- for (const model of MODEL_REGISTRY) {
103
+ for (const model of allModels) {
95
104
  if (isUnified) {
96
105
  if (DISABLED_PROVIDERS.has(model.provider)) continue;
97
106
  } else {
@@ -132,6 +141,8 @@ export function useModelRegistry(options?: UseModelRegistryOptions): UseModelReg
132
141
  providers,
133
142
  featured: featuredModels,
134
143
  getByKey: (key: string) => byCompoundKey.get(key),
144
+ isLoading,
145
+ error,
135
146
  };
136
- }, [harness]);
147
+ }, [harness, allModels, isLoading, error]);
137
148
  }
package/src/provider.tsx CHANGED
@@ -9,6 +9,9 @@ import { DeploymentModeContext } from "./deployment-mode";
9
9
  import type { ColorMode, ResolvedColorMode } from "./color-mode";
10
10
  import { ColorModeContext, useSystemColorMode } from "./color-mode";
11
11
  import { PortalContainerContext } from "./portal-container";
12
+ import { ModelRegistryContext } from "./models/ModelRegistryContext";
13
+ import type { ModelRegistryState } from "./models/ModelRegistryContext";
14
+ import { fetchModelRegistry } from "./models/registry";
12
15
 
13
16
  /** Props for {@link StigmerProvider}. */
14
17
  export interface StigmerProviderProps {
@@ -126,25 +129,72 @@ export function StigmerProvider({
126
129
  const presetClass = preset ? resolvePresetClass(preset) : "";
127
130
 
128
131
  const portalContainer = usePortalContainer(resolvedMode, presetClass);
132
+ const registryState = useModelRegistryFetch(client);
129
133
 
130
134
  return (
131
135
  <StigmerContext.Provider value={client}>
132
136
  <DeploymentModeContext.Provider value={deploymentMode}>
133
137
  <ColorModeContext.Provider value={resolvedMode}>
134
- <PortalContainerContext.Provider value={portalContainer}>
135
- <div
136
- className={cn("stgm", presetClass, className)}
137
- data-stgm-color-mode={resolvedMode}
138
- >
139
- {children}
140
- </div>
141
- </PortalContainerContext.Provider>
138
+ <ModelRegistryContext.Provider value={registryState}>
139
+ <PortalContainerContext.Provider value={portalContainer}>
140
+ <div
141
+ className={cn("stgm", presetClass, className)}
142
+ data-stgm-color-mode={resolvedMode}
143
+ >
144
+ {children}
145
+ </div>
146
+ </PortalContainerContext.Provider>
147
+ </ModelRegistryContext.Provider>
142
148
  </ColorModeContext.Provider>
143
149
  </DeploymentModeContext.Provider>
144
150
  </StigmerContext.Provider>
145
151
  );
146
152
  }
147
153
 
154
+ /**
155
+ * Fetches the model registry from the authenticated API on mount and
156
+ * caches the result for the lifetime of the provider.
157
+ *
158
+ * Uses the client's `baseUrl` and `getAuthCredential()` so the fetch
159
+ * is authenticated with the same token the SDK uses for all other calls.
160
+ */
161
+ function useModelRegistryFetch(client: Stigmer): ModelRegistryState {
162
+ const [state, setState] = useState<ModelRegistryState>({
163
+ models: [],
164
+ isLoading: true,
165
+ error: null,
166
+ });
167
+
168
+ const clientRef = useRef(client);
169
+ clientRef.current = client;
170
+
171
+ useEffect(() => {
172
+ let cancelled = false;
173
+
174
+ const c = clientRef.current;
175
+ c.getAuthCredential()
176
+ .then((token) => fetchModelRegistry(c.baseUrl, token))
177
+ .then((models) => {
178
+ if (!cancelled) {
179
+ setState({ models, isLoading: false, error: null });
180
+ }
181
+ })
182
+ .catch((err: unknown) => {
183
+ if (!cancelled) {
184
+ setState({
185
+ models: [],
186
+ isLoading: false,
187
+ error: err instanceof Error ? err : new Error(String(err)),
188
+ });
189
+ }
190
+ });
191
+
192
+ return () => { cancelled = true; };
193
+ }, []);
194
+
195
+ return state;
196
+ }
197
+
148
198
  /**
149
199
  * Creates and manages a portal container `<div>` appended to
150
200
  * `document.body` that mirrors the scoping attributes of the main
@@ -1,6 +1,9 @@
1
1
  import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
2
  import { renderHook, act } from "@testing-library/react";
3
- import { DEFAULT_MODEL_ID, DEFAULT_CURSOR_MODEL_ID } from "../../models/registry";
3
+ import type { ReactNode } from "react";
4
+ import { DEFAULT_MODEL_ID, DEFAULT_CURSOR_MODEL_ID, parseRegistryJson } from "../../models/registry";
5
+ import { ModelRegistryContext } from "../../models/ModelRegistryContext";
6
+ import type { ModelRegistryState } from "../../models/ModelRegistryContext";
4
7
  import type { UseCreateSessionReturn } from "../useCreateSession";
5
8
 
6
9
  const mockCreateSession = vi.fn<UseCreateSessionReturn["create"]>();
@@ -60,6 +63,24 @@ vi.mock("../../execution/useSessionVariables", () => ({
60
63
 
61
64
  import { useNewSessionFlow } from "../useNewSessionFlow";
62
65
 
66
+ const TEST_MODELS = parseRegistryJson({
67
+ models: [
68
+ { id: "claude-sonnet-4.6", displayName: "Claude Sonnet 4.6", shortDescription: "", speedTier: "fast", provider: "anthropic", harness: "native", costTier: "standard", featured: true, pricing: { inputPricePerMillion: 3, outputPricePerMillion: 15, cacheWritePricePerMillion: 3.75, cacheReadPricePerMillion: 0.3 } },
69
+ { id: "default", displayName: "Cursor Auto", shortDescription: "", speedTier: "fast", provider: "cursor", harness: "cursor", costTier: "standard", featured: true, pricing: { inputPricePerMillion: 1.25, outputPricePerMillion: 6, cacheWritePricePerMillion: 1.25, cacheReadPricePerMillion: 0.25 } },
70
+ ],
71
+ });
72
+
73
+ function createWrapper() {
74
+ const state: ModelRegistryState = { models: TEST_MODELS, isLoading: false, error: null };
75
+ return function Wrapper({ children }: { children: ReactNode }) {
76
+ return (
77
+ <ModelRegistryContext.Provider value={state}>
78
+ {children}
79
+ </ModelRegistryContext.Provider>
80
+ );
81
+ };
82
+ }
83
+
63
84
  const STORAGE_KEY_HARNESS = "stigmer:session:harness";
64
85
  const STORAGE_KEY_MODEL_NATIVE = "stigmer:session:model";
65
86
  const STORAGE_KEY_MODEL_CURSOR = "stigmer:session:model:cursor";
@@ -91,24 +112,24 @@ describe("useNewSessionFlow", () => {
91
112
 
92
113
  describe("harness state", () => {
93
114
  it("defaults to native when localStorage is empty", () => {
94
- const { result } = renderHook(() => useNewSessionFlow(defaultOptions()));
115
+ const { result } = renderHook(() => useNewSessionFlow(defaultOptions()), { wrapper: createWrapper() });
95
116
  expect(result.current.harness).toBe("native");
96
117
  });
97
118
 
98
119
  it("restores cursor harness from localStorage", () => {
99
120
  localStorage.setItem(STORAGE_KEY_HARNESS, "cursor");
100
- const { result } = renderHook(() => useNewSessionFlow(defaultOptions()));
121
+ const { result } = renderHook(() => useNewSessionFlow(defaultOptions()), { wrapper: createWrapper() });
101
122
  expect(result.current.harness).toBe("cursor");
102
123
  });
103
124
 
104
125
  it("falls back to native for unknown localStorage values", () => {
105
126
  localStorage.setItem(STORAGE_KEY_HARNESS, "unknown-value");
106
- const { result } = renderHook(() => useNewSessionFlow(defaultOptions()));
127
+ const { result } = renderHook(() => useNewSessionFlow(defaultOptions()), { wrapper: createWrapper() });
107
128
  expect(result.current.harness).toBe("native");
108
129
  });
109
130
 
110
131
  it("persists harness to localStorage on change", () => {
111
- const { result } = renderHook(() => useNewSessionFlow(defaultOptions()));
132
+ const { result } = renderHook(() => useNewSessionFlow(defaultOptions()), { wrapper: createWrapper() });
112
133
 
113
134
  act(() => result.current.setHarness("cursor"));
114
135
 
@@ -118,7 +139,7 @@ describe("useNewSessionFlow", () => {
118
139
 
119
140
  it("persists native harness to localStorage", () => {
120
141
  localStorage.setItem(STORAGE_KEY_HARNESS, "cursor");
121
- const { result } = renderHook(() => useNewSessionFlow(defaultOptions()));
142
+ const { result } = renderHook(() => useNewSessionFlow(defaultOptions()), { wrapper: createWrapper() });
122
143
 
123
144
  act(() => result.current.setHarness("native"));
124
145
 
@@ -128,7 +149,7 @@ describe("useNewSessionFlow", () => {
128
149
 
129
150
  describe("per-harness model persistence", () => {
130
151
  it("uses separate storage keys for native and cursor models", () => {
131
- const { result } = renderHook(() => useNewSessionFlow(defaultOptions()));
152
+ const { result } = renderHook(() => useNewSessionFlow(defaultOptions()), { wrapper: createWrapper() });
132
153
 
133
154
  act(() => result.current.setModelId(DEFAULT_MODEL_ID));
134
155
 
@@ -140,7 +161,7 @@ describe("useNewSessionFlow", () => {
140
161
 
141
162
  it("persists cursor model to cursor-specific key", () => {
142
163
  localStorage.setItem(STORAGE_KEY_HARNESS, "cursor");
143
- const { result } = renderHook(() => useNewSessionFlow(defaultOptions()));
164
+ const { result } = renderHook(() => useNewSessionFlow(defaultOptions()), { wrapper: createWrapper() });
144
165
 
145
166
  act(() => result.current.setModelId(DEFAULT_CURSOR_MODEL_ID));
146
167
 
@@ -152,7 +173,7 @@ describe("useNewSessionFlow", () => {
152
173
  it("restores per-harness model when switching harness", () => {
153
174
  localStorage.setItem(STORAGE_KEY_MODEL_CURSOR, DEFAULT_CURSOR_MODEL_ID);
154
175
 
155
- const { result } = renderHook(() => useNewSessionFlow(defaultOptions()));
176
+ const { result } = renderHook(() => useNewSessionFlow(defaultOptions()), { wrapper: createWrapper() });
156
177
 
157
178
  act(() => result.current.setHarness("cursor"));
158
179
 
@@ -160,7 +181,7 @@ describe("useNewSessionFlow", () => {
160
181
  });
161
182
 
162
183
  it("clears modelId when switching to a harness with no stored model", () => {
163
- const { result } = renderHook(() => useNewSessionFlow(defaultOptions()));
184
+ const { result } = renderHook(() => useNewSessionFlow(defaultOptions()), { wrapper: createWrapper() });
164
185
 
165
186
  act(() => result.current.setModelId(DEFAULT_MODEL_ID));
166
187
  expect(result.current.modelId).toBe(DEFAULT_MODEL_ID);
@@ -178,7 +199,7 @@ describe("useNewSessionFlow", () => {
178
199
  });
179
200
 
180
201
  it("invalidates modelId when it is not in the active harness registry", () => {
181
- const { result } = renderHook(() => useNewSessionFlow(defaultOptions()));
202
+ const { result } = renderHook(() => useNewSessionFlow(defaultOptions()), { wrapper: createWrapper() });
182
203
 
183
204
  // Set a native-only model
184
205
  act(() => result.current.setModelId(DEFAULT_MODEL_ID));
@@ -193,7 +214,7 @@ describe("useNewSessionFlow", () => {
193
214
 
194
215
  it("strips compound keys before persisting to localStorage", () => {
195
216
  localStorage.setItem(STORAGE_KEY_HARNESS, "cursor");
196
- const { result } = renderHook(() => useNewSessionFlow(defaultOptions()));
217
+ const { result } = renderHook(() => useNewSessionFlow(defaultOptions()), { wrapper: createWrapper() });
197
218
 
198
219
  // Simulate compound key from unified mode ModelSelector
199
220
  act(() => result.current.setModelId("cursor/default"));
@@ -207,7 +228,7 @@ describe("useNewSessionFlow", () => {
207
228
  // Legacy: compound key was stored before fix
208
229
  localStorage.setItem(STORAGE_KEY_MODEL_CURSOR, "cursor/default");
209
230
 
210
- const { result } = renderHook(() => useNewSessionFlow(defaultOptions()));
231
+ const { result } = renderHook(() => useNewSessionFlow(defaultOptions()), { wrapper: createWrapper() });
211
232
 
212
233
  // Should extract plain modelId and validate against registry
213
234
  expect(result.current.modelId).toBe(DEFAULT_CURSOR_MODEL_ID);
@@ -217,7 +238,7 @@ describe("useNewSessionFlow", () => {
217
238
  describe("submit with harness", () => {
218
239
  it("passes harness field to createSession", async () => {
219
240
  const opts = defaultOptions();
220
- const { result } = renderHook(() => useNewSessionFlow(opts));
241
+ const { result } = renderHook(() => useNewSessionFlow(opts), { wrapper: createWrapper() });
221
242
 
222
243
  await act(async () => {
223
244
  await result.current.submit("Hello");
@@ -230,7 +251,7 @@ describe("useNewSessionFlow", () => {
230
251
 
231
252
  it("passes cursor harness to createSession after switching", async () => {
232
253
  const opts = defaultOptions();
233
- const { result } = renderHook(() => useNewSessionFlow(opts));
254
+ const { result } = renderHook(() => useNewSessionFlow(opts), { wrapper: createWrapper() });
234
255
 
235
256
  act(() => result.current.setHarness("cursor"));
236
257
 
@@ -244,7 +265,7 @@ describe("useNewSessionFlow", () => {
244
265
 
245
266
  it("calls onSessionCreated on success", async () => {
246
267
  const opts = defaultOptions();
247
- const { result } = renderHook(() => useNewSessionFlow(opts));
268
+ const { result } = renderHook(() => useNewSessionFlow(opts), { wrapper: createWrapper() });
248
269
 
249
270
  await act(async () => {
250
271
  await result.current.submit("Hello");
@@ -256,7 +277,7 @@ describe("useNewSessionFlow", () => {
256
277
  it("sets submitError and calls onError on failure", async () => {
257
278
  mockCreateSession.mockRejectedValueOnce(new Error("RPC fail"));
258
279
  const opts = defaultOptions();
259
- const { result } = renderHook(() => useNewSessionFlow(opts));
280
+ const { result } = renderHook(() => useNewSessionFlow(opts), { wrapper: createWrapper() });
260
281
 
261
282
  await act(async () => {
262
283
  await result.current.submit("Hello");
@@ -268,7 +289,7 @@ describe("useNewSessionFlow", () => {
268
289
 
269
290
  it("resets isSubmitting after completion", async () => {
270
291
  const opts = defaultOptions();
271
- const { result } = renderHook(() => useNewSessionFlow(opts));
292
+ const { result } = renderHook(() => useNewSessionFlow(opts), { wrapper: createWrapper() });
272
293
 
273
294
  await act(async () => {
274
295
  await result.current.submit("Hello");
@@ -1,7 +1,28 @@
1
1
  import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
2
  import { renderHook, act } from "@testing-library/react";
3
+ import type { ReactNode } from "react";
3
4
  import { usePersistedModel } from "../usePersistedModel";
4
- import { DEFAULT_MODEL_ID, DEFAULT_CURSOR_MODEL_ID } from "../../models/registry";
5
+ import { DEFAULT_MODEL_ID, DEFAULT_CURSOR_MODEL_ID, parseRegistryJson } from "../../models/registry";
6
+ import { ModelRegistryContext } from "../../models/ModelRegistryContext";
7
+ import type { ModelRegistryState } from "../../models/ModelRegistryContext";
8
+
9
+ const TEST_MODELS = parseRegistryJson({
10
+ models: [
11
+ { id: "claude-sonnet-4.6", displayName: "Claude Sonnet 4.6", shortDescription: "", speedTier: "fast", provider: "anthropic", harness: "native", costTier: "standard", featured: true, pricing: { inputPricePerMillion: 3, outputPricePerMillion: 15, cacheWritePricePerMillion: 3.75, cacheReadPricePerMillion: 0.3 } },
12
+ { id: "default", displayName: "Cursor Auto", shortDescription: "", speedTier: "fast", provider: "cursor", harness: "cursor", costTier: "standard", featured: true, pricing: { inputPricePerMillion: 1.25, outputPricePerMillion: 6, cacheWritePricePerMillion: 1.25, cacheReadPricePerMillion: 0.25 } },
13
+ ],
14
+ });
15
+
16
+ function createWrapper() {
17
+ const state: ModelRegistryState = { models: TEST_MODELS, isLoading: false, error: null };
18
+ return function Wrapper({ children }: { children: ReactNode }) {
19
+ return (
20
+ <ModelRegistryContext.Provider value={state}>
21
+ {children}
22
+ </ModelRegistryContext.Provider>
23
+ );
24
+ };
25
+ }
5
26
 
6
27
  const STORAGE_KEY_NATIVE = "stigmer:session:model";
7
28
  const STORAGE_KEY_CURSOR = "stigmer:session:model:cursor";
@@ -17,24 +38,24 @@ describe("usePersistedModel", () => {
17
38
 
18
39
  describe("basic persistence", () => {
19
40
  it("returns undefined when localStorage is empty", () => {
20
- const { result } = renderHook(() => usePersistedModel({ harness: "native" }));
41
+ const { result } = renderHook(() => usePersistedModel({ harness: "native" }), { wrapper: createWrapper() });
21
42
  expect(result.current[0]).toBeUndefined();
22
43
  });
23
44
 
24
45
  it("restores a valid model from localStorage", () => {
25
46
  localStorage.setItem(STORAGE_KEY_NATIVE, DEFAULT_MODEL_ID);
26
- const { result } = renderHook(() => usePersistedModel({ harness: "native" }));
47
+ const { result } = renderHook(() => usePersistedModel({ harness: "native" }), { wrapper: createWrapper() });
27
48
  expect(result.current[0]).toBe(DEFAULT_MODEL_ID);
28
49
  });
29
50
 
30
51
  it("returns undefined for an invalid model in localStorage", () => {
31
52
  localStorage.setItem(STORAGE_KEY_NATIVE, "nonexistent-model-xyz");
32
- const { result } = renderHook(() => usePersistedModel({ harness: "native" }));
53
+ const { result } = renderHook(() => usePersistedModel({ harness: "native" }), { wrapper: createWrapper() });
33
54
  expect(result.current[0]).toBeUndefined();
34
55
  });
35
56
 
36
57
  it("persists model on change", () => {
37
- const { result } = renderHook(() => usePersistedModel({ harness: "native" }));
58
+ const { result } = renderHook(() => usePersistedModel({ harness: "native" }), { wrapper: createWrapper() });
38
59
 
39
60
  act(() => result.current[1](DEFAULT_MODEL_ID));
40
61
 
@@ -44,7 +65,7 @@ describe("usePersistedModel", () => {
44
65
 
45
66
  it("uses cursor-specific key for cursor harness", () => {
46
67
  localStorage.setItem(STORAGE_KEY_CURSOR, DEFAULT_CURSOR_MODEL_ID);
47
- const { result } = renderHook(() => usePersistedModel({ harness: "cursor" }));
68
+ const { result } = renderHook(() => usePersistedModel({ harness: "cursor" }), { wrapper: createWrapper() });
48
69
  expect(result.current[0]).toBe(DEFAULT_CURSOR_MODEL_ID);
49
70
  });
50
71
  });
@@ -52,19 +73,19 @@ describe("usePersistedModel", () => {
52
73
  describe("compound key handling", () => {
53
74
  it("extracts plain modelId from compound key in localStorage", () => {
54
75
  localStorage.setItem(STORAGE_KEY_CURSOR, "cursor/default");
55
- const { result } = renderHook(() => usePersistedModel({ harness: "cursor" }));
76
+ const { result } = renderHook(() => usePersistedModel({ harness: "cursor" }), { wrapper: createWrapper() });
56
77
  expect(result.current[0]).toBe(DEFAULT_CURSOR_MODEL_ID);
57
78
  });
58
79
 
59
80
  it("extracts plain modelId from native compound key", () => {
60
81
  localStorage.setItem(STORAGE_KEY_NATIVE, `native/${DEFAULT_MODEL_ID}`);
61
- const { result } = renderHook(() => usePersistedModel({ harness: "native" }));
82
+ const { result } = renderHook(() => usePersistedModel({ harness: "native" }), { wrapper: createWrapper() });
62
83
  expect(result.current[0]).toBe(DEFAULT_MODEL_ID);
63
84
  });
64
85
 
65
86
  it("handles non-compound values unchanged", () => {
66
87
  localStorage.setItem(STORAGE_KEY_CURSOR, DEFAULT_CURSOR_MODEL_ID);
67
- const { result } = renderHook(() => usePersistedModel({ harness: "cursor" }));
88
+ const { result } = renderHook(() => usePersistedModel({ harness: "cursor" }), { wrapper: createWrapper() });
68
89
  expect(result.current[0]).toBe(DEFAULT_CURSOR_MODEL_ID);
69
90
  });
70
91
  });
@@ -76,7 +97,7 @@ describe("usePersistedModel", () => {
76
97
 
77
98
  const { result, rerender } = renderHook(
78
99
  ({ harness }: { harness: "native" | "cursor" }) => usePersistedModel({ harness }),
79
- { initialProps: { harness: "native" } },
100
+ { initialProps: { harness: "native" }, wrapper: createWrapper() },
80
101
  );
81
102
 
82
103
  expect(result.current[0]).toBe(DEFAULT_MODEL_ID);
@@ -91,7 +112,7 @@ describe("usePersistedModel", () => {
91
112
 
92
113
  const { result, rerender } = renderHook(
93
114
  ({ harness }: { harness: "native" | "cursor" }) => usePersistedModel({ harness }),
94
- { initialProps: { harness: "native" } },
115
+ { initialProps: { harness: "native" }, wrapper: createWrapper() },
95
116
  );
96
117
 
97
118
  expect(result.current[0]).toBe(DEFAULT_MODEL_ID);
@@ -106,7 +127,7 @@ describe("usePersistedModel", () => {
106
127
 
107
128
  const { result, rerender } = renderHook(
108
129
  ({ harness }: { harness: "native" | "cursor" }) => usePersistedModel({ harness }),
109
- { initialProps: { harness: "native" } },
130
+ { initialProps: { harness: "native" }, wrapper: createWrapper() },
110
131
  );
111
132
 
112
133
  rerender({ harness: "cursor" });
@@ -239,19 +239,19 @@ function SummaryCards({
239
239
 
240
240
  {/* Secondary row */}
241
241
  <div className="grid grid-cols-3 gap-3">
242
- <div className="rounded-lg border border-border-muted bg-card/50 px-3.5 py-2.5">
242
+ <div className="rounded-lg border border-border-muted bg-muted-subtle px-3.5 py-2.5">
243
243
  <div className="text-sm font-semibold tabular-nums text-foreground">
244
244
  {formatCompactNumber(report.totalExecutions)}
245
245
  </div>
246
246
  <div className="text-[0.65rem] text-muted-foreground">Executions</div>
247
247
  </div>
248
- <div className="rounded-lg border border-border-muted bg-card/50 px-3.5 py-2.5">
248
+ <div className="rounded-lg border border-border-muted bg-muted-subtle px-3.5 py-2.5">
249
249
  <div className="text-sm font-semibold tabular-nums text-foreground">
250
250
  {formatCompactNumber(report.totalAgents)}
251
251
  </div>
252
252
  <div className="text-[0.65rem] text-muted-foreground">Agents</div>
253
253
  </div>
254
- <div className="rounded-lg border border-border-muted bg-card/50 px-3.5 py-2.5">
254
+ <div className="rounded-lg border border-border-muted bg-muted-subtle px-3.5 py-2.5">
255
255
  <div className="text-sm font-semibold tabular-nums text-foreground">
256
256
  {formatCompactNumber(report.totalSessions)}
257
257
  </div>