@stigmer/react 0.4.5 → 0.4.7

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 (52) hide show
  1. package/composer/ComposerToolbar.d.ts.map +1 -1
  2. package/composer/ComposerToolbar.js +1 -1
  3. package/composer/ComposerToolbar.js.map +1 -1
  4. package/index.d.ts +2 -2
  5. package/index.d.ts.map +1 -1
  6. package/index.js +1 -1
  7. package/index.js.map +1 -1
  8. package/models/ModelRegistryContext.d.ts +21 -0
  9. package/models/ModelRegistryContext.d.ts.map +1 -0
  10. package/models/ModelRegistryContext.js +22 -0
  11. package/models/ModelRegistryContext.js.map +1 -0
  12. package/models/ModelSelector.d.ts +9 -1
  13. package/models/ModelSelector.d.ts.map +1 -1
  14. package/models/ModelSelector.js +10 -5
  15. package/models/ModelSelector.js.map +1 -1
  16. package/models/__tests__/useModelRegistry.test.js +127 -32
  17. package/models/__tests__/useModelRegistry.test.js.map +1 -1
  18. package/models/index.d.ts +3 -1
  19. package/models/index.d.ts.map +1 -1
  20. package/models/index.js +2 -1
  21. package/models/index.js.map +1 -1
  22. package/models/registry.d.ts +20 -12
  23. package/models/registry.d.ts.map +1 -1
  24. package/models/registry.js +51 -27
  25. package/models/registry.js.map +1 -1
  26. package/models/useModelRegistry.d.ts +11 -3
  27. package/models/useModelRegistry.d.ts.map +1 -1
  28. package/models/useModelRegistry.js +13 -5
  29. package/models/useModelRegistry.js.map +1 -1
  30. package/package.json +4 -4
  31. package/provider.d.ts.map +1 -1
  32. package/provider.js +42 -1
  33. package/provider.js.map +1 -1
  34. package/session/__tests__/useNewSessionFlow.test.js +89 -18
  35. package/session/__tests__/useNewSessionFlow.test.js.map +1 -1
  36. package/session/__tests__/usePersistedModel.test.js +26 -12
  37. package/session/__tests__/usePersistedModel.test.js.map +1 -1
  38. package/session/useNewSessionFlow.d.ts.map +1 -1
  39. package/session/useNewSessionFlow.js +20 -6
  40. package/session/useNewSessionFlow.js.map +1 -1
  41. package/src/composer/ComposerToolbar.tsx +1 -0
  42. package/src/index.ts +4 -1
  43. package/src/models/ModelRegistryContext.ts +32 -0
  44. package/src/models/ModelSelector.tsx +22 -5
  45. package/src/models/__tests__/useModelRegistry.test.tsx +150 -41
  46. package/src/models/index.ts +3 -1
  47. package/src/models/registry.ts +51 -30
  48. package/src/models/useModelRegistry.ts +18 -7
  49. package/src/provider.tsx +58 -8
  50. package/src/session/__tests__/useNewSessionFlow.test.tsx +120 -18
  51. package/src/session/__tests__/usePersistedModel.test.tsx +33 -12
  52. package/src/session/useNewSessionFlow.ts +17 -6
@@ -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" });
@@ -10,6 +10,7 @@ import { DEFAULT_HARNESS, type HarnessOption } from "../models/harness";
10
10
  import { useWorkspaceEntries, type UseWorkspaceEntriesReturn } from "../workspace";
11
11
  import { useSessionVariables, type UseSessionVariablesReturn } from "../execution/useSessionVariables";
12
12
  import type { SessionComposerSubmitContext } from "../composer";
13
+ import { useRunnerList } from "../runner/useRunnerList";
13
14
  import { useCreateSession } from "./useCreateSession";
14
15
  import { useCreateAgentExecution } from "../execution/useCreateAgentExecution";
15
16
 
@@ -155,7 +156,8 @@ export function useNewSessionFlow(
155
156
  return stored === "cursor" ? "cursor" : DEFAULT_HARNESS;
156
157
  });
157
158
 
158
- const { getModel } = useModelRegistry({ harness });
159
+ const { getModel, isLoading: isModelsLoading } = useModelRegistry({ harness });
160
+ const { runners, isLoading: isRunnersLoading } = useRunnerList(org);
159
161
  const { create: createSession } = useCreateSession();
160
162
  const { create: createExecution } = useCreateAgentExecution();
161
163
  const {
@@ -192,8 +194,10 @@ export function useNewSessionFlow(
192
194
  [],
193
195
  );
194
196
 
195
- // Restore persisted model on mount (using current harness key)
197
+ // Restore persisted model only after the registry has loaded so
198
+ // getModel can actually validate the stored ID against live data.
196
199
  useEffect(() => {
200
+ if (isModelsLoading) return;
197
201
  const stored = localStorage.getItem(modelStorageKey(harness));
198
202
  if (stored) {
199
203
  const plain = parseModelKey(stored)?.modelId ?? stored;
@@ -201,7 +205,7 @@ export function useNewSessionFlow(
201
205
  setModelId(plain);
202
206
  }
203
207
  }
204
- }, [getModel, harness]);
208
+ }, [getModel, harness, isModelsLoading]);
205
209
 
206
210
  // Persist model on change (using current harness key).
207
211
  // Strip compound keys (e.g. "cursor/default") to plain modelId before storing.
@@ -212,13 +216,20 @@ export function useNewSessionFlow(
212
216
  }
213
217
  }, [modelId, harness]);
214
218
 
215
- // Restore persisted runner on mount
219
+ // Restore persisted runner validate against the live runner list.
220
+ // If the stored runner no longer exists, discard it and clean up localStorage.
216
221
  useEffect(() => {
222
+ if (isRunnersLoading) return;
217
223
  const stored = localStorage.getItem(STORAGE_KEY_RUNNER);
218
- if (stored) {
224
+ if (!stored) return;
225
+
226
+ const exists = runners.some((r) => r.metadata?.id === stored);
227
+ if (exists) {
219
228
  setRunnerId(stored);
229
+ } else {
230
+ localStorage.removeItem(STORAGE_KEY_RUNNER);
220
231
  }
221
- }, []);
232
+ }, [runners, isRunnersLoading]);
222
233
 
223
234
  // Persist runner on change
224
235
  useEffect(() => {