@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
@@ -7,6 +7,7 @@ import { parseModelKey } from "../models/registry";
7
7
  import { DEFAULT_HARNESS } from "../models/harness";
8
8
  import { useWorkspaceEntries } from "../workspace";
9
9
  import { useSessionVariables } from "../execution/useSessionVariables";
10
+ import { useRunnerList } from "../runner/useRunnerList";
10
11
  import { useCreateSession } from "./useCreateSession";
11
12
  import { useCreateAgentExecution } from "../execution/useCreateAgentExecution";
12
13
  const STORAGE_KEY_HARNESS = "stigmer:session:harness";
@@ -65,7 +66,8 @@ export function useNewSessionFlow(options) {
65
66
  const stored = localStorage.getItem(STORAGE_KEY_HARNESS);
66
67
  return stored === "cursor" ? "cursor" : DEFAULT_HARNESS;
67
68
  });
68
- const { getModel } = useModelRegistry({ harness });
69
+ const { getModel, isLoading: isModelsLoading } = useModelRegistry({ harness });
70
+ const { runners, isLoading: isRunnersLoading } = useRunnerList(org);
69
71
  const { create: createSession } = useCreateSession();
70
72
  const { create: createExecution } = useCreateAgentExecution();
71
73
  const { agent: defaultAgent, isLoading: isDefaultAgentLoading, error: defaultAgentError, } = useDefaultAgent(org);
@@ -90,8 +92,11 @@ export function useNewSessionFlow(options) {
90
92
  const plain = storedModel ? (parseModelKey(storedModel)?.modelId ?? storedModel) : undefined;
91
93
  setModelId(plain);
92
94
  }, []);
93
- // Restore persisted model on mount (using current harness key)
95
+ // Restore persisted model only after the registry has loaded so
96
+ // getModel can actually validate the stored ID against live data.
94
97
  useEffect(() => {
98
+ if (isModelsLoading)
99
+ return;
95
100
  const stored = localStorage.getItem(modelStorageKey(harness));
96
101
  if (stored) {
97
102
  const plain = parseModelKey(stored)?.modelId ?? stored;
@@ -99,7 +104,7 @@ export function useNewSessionFlow(options) {
99
104
  setModelId(plain);
100
105
  }
101
106
  }
102
- }, [getModel, harness]);
107
+ }, [getModel, harness, isModelsLoading]);
103
108
  // Persist model on change (using current harness key).
104
109
  // Strip compound keys (e.g. "cursor/default") to plain modelId before storing.
105
110
  useEffect(() => {
@@ -108,13 +113,22 @@ export function useNewSessionFlow(options) {
108
113
  localStorage.setItem(modelStorageKey(harness), plain);
109
114
  }
110
115
  }, [modelId, harness]);
111
- // Restore persisted runner on mount
116
+ // Restore persisted runner validate against the live runner list.
117
+ // If the stored runner no longer exists, discard it and clean up localStorage.
112
118
  useEffect(() => {
119
+ if (isRunnersLoading)
120
+ return;
113
121
  const stored = localStorage.getItem(STORAGE_KEY_RUNNER);
114
- if (stored) {
122
+ if (!stored)
123
+ return;
124
+ const exists = runners.some((r) => r.metadata?.id === stored);
125
+ if (exists) {
115
126
  setRunnerId(stored);
116
127
  }
117
- }, []);
128
+ else {
129
+ localStorage.removeItem(STORAGE_KEY_RUNNER);
130
+ }
131
+ }, [runners, isRunnersLoading]);
118
132
  // Persist runner on change
119
133
  useEffect(() => {
120
134
  if (runnerId) {
@@ -1 +1 @@
1
- {"version":3,"file":"useNewSessionFlow.js","sourceRoot":"","sources":["../../src/session/useNewSessionFlow.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AACzD,OAAO,EAAE,cAAc,EAA8C,MAAM,cAAc,CAAC;AAE1F,OAAO,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,eAAe,EAAsB,MAAM,mBAAmB,CAAC;AACxE,OAAO,EAAE,mBAAmB,EAAkC,MAAM,cAAc,CAAC;AACnF,OAAO,EAAE,mBAAmB,EAAkC,MAAM,kCAAkC,CAAC;AAEvG,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAAE,uBAAuB,EAAE,MAAM,sCAAsC,CAAC;AAE/E,MAAM,mBAAmB,GAAG,yBAAyB,CAAC;AACtD,MAAM,kBAAkB,GAAG,wBAAwB,CAAC;AAEpD,SAAS,eAAe,CAAC,OAAsB;IAC7C,OAAO,OAAO,KAAK,QAAQ;QACzB,CAAC,CAAC,8BAA8B;QAChC,CAAC,CAAC,uBAAuB,CAAC;AAC9B,CAAC;AAmFD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AACH,MAAM,UAAU,iBAAiB,CAC/B,OAAiC;IAEjC,MAAM,EAAE,GAAG,EAAE,gBAAgB,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;IAEnD,MAAM,CAAC,OAAO,EAAE,aAAa,CAAC,GAAG,QAAQ,CAAgB,GAAG,EAAE;QAC5D,IAAI,OAAO,MAAM,KAAK,WAAW;YAAE,OAAO,eAAe,CAAC;QAC1D,MAAM,MAAM,GAAG,YAAY,CAAC,OAAO,CAAC,mBAAmB,CAAC,CAAC;QACzD,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,eAAe,CAAC;IAC1D,CAAC,CAAC,CAAC;IAEH,MAAM,EAAE,QAAQ,EAAE,GAAG,gBAAgB,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC;IACnD,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,GAAG,gBAAgB,EAAE,CAAC;IACrD,MAAM,EAAE,MAAM,EAAE,eAAe,EAAE,GAAG,uBAAuB,EAAE,CAAC;IAC9D,MAAM,EACJ,KAAK,EAAE,YAAY,EACnB,SAAS,EAAE,qBAAqB,EAChC,KAAK,EAAE,iBAAiB,GACzB,GAAG,eAAe,CAAC,GAAG,CAAC,CAAC;IACzB,MAAM,SAAS,GAAG,mBAAmB,EAAE,CAAC;IACxC,MAAM,gBAAgB,GAAG,mBAAmB,EAAE,CAAC;IAE/C,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,QAAQ,CAAqB,SAAS,CAAC,CAAC;IACtE,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,QAAQ,CAAqB,IAAI,CAAC,CAAC;IACnE,MAAM,CAAC,UAAU,EAAE,aAAa,CAAC,GAAG,QAAQ,CAAyB,IAAI,CAAC,CAAC;IAC3E,MAAM,CAAC,eAAe,EAAE,kBAAkB,CAAC,GAAG,QAAQ,CAAwB,EAAE,CAAC,CAAC;IAClF,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,QAAQ,CAAgB,EAAE,CAAC,CAAC;IAC9D,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,QAAQ,CAAgB,IAAI,CAAC,CAAC;IAC9D,MAAM,CAAC,YAAY,EAAE,eAAe,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IACxD,MAAM,CAAC,WAAW,EAAE,cAAc,CAAC,GAAG,QAAQ,CAAgB,IAAI,CAAC,CAAC;IAEpE,MAAM,YAAY,GAAG,OAAO,IAAI,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC;IAExE,4BAA4B;IAC5B,SAAS,CAAC,GAAG,EAAE;QACb,YAAY,CAAC,OAAO,CAAC,mBAAmB,EAAE,OAAO,CAAC,CAAC;IACrD,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;IAEd,MAAM,UAAU,GAAG,WAAW,CAC5B,CAAC,CAAgB,EAAE,EAAE;QACnB,aAAa,CAAC,CAAC,CAAC,CAAC;QACjB,MAAM,WAAW,GAAG,YAAY,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC;QAC7D,MAAM,KAAK,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,WAAW,CAAC,EAAE,OAAO,IAAI,WAAW,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QAC7F,UAAU,CAAC,KAAK,CAAC,CAAC;IACpB,CAAC,EACD,EAAE,CACH,CAAC;IAEF,+DAA+D;IAC/D,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,MAAM,GAAG,YAAY,CAAC,OAAO,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC,CAAC;QAC9D,IAAI,MAAM,EAAE,CAAC;YACX,MAAM,KAAK,GAAG,aAAa,CAAC,MAAM,CAAC,EAAE,OAAO,IAAI,MAAM,CAAC;YACvD,IAAI,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;gBACpB,UAAU,CAAC,KAAK,CAAC,CAAC;YACpB,CAAC;QACH,CAAC;IACH,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC;IAExB,uDAAuD;IACvD,+EAA+E;IAC/E,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,OAAO,EAAE,CAAC;YACZ,MAAM,KAAK,GAAG,aAAa,CAAC,OAAO,CAAC,EAAE,OAAO,IAAI,OAAO,CAAC;YACzD,YAAY,CAAC,OAAO,CAAC,eAAe,CAAC,OAAO,CAAC,EAAE,KAAK,CAAC,CAAC;QACxD,CAAC;IACH,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;IAEvB,oCAAoC;IACpC,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,MAAM,GAAG,YAAY,CAAC,OAAO,CAAC,kBAAkB,CAAC,CAAC;QACxD,IAAI,MAAM,EAAE,CAAC;YACX,WAAW,CAAC,MAAM,CAAC,CAAC;QACtB,CAAC;IACH,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,2BAA2B;IAC3B,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,QAAQ,EAAE,CAAC;YACb,YAAY,CAAC,OAAO,CAAC,kBAAkB,EAAE,QAAQ,CAAC,CAAC;QACrD,CAAC;aAAM,CAAC;YACN,YAAY,CAAC,UAAU,CAAC,kBAAkB,CAAC,CAAC;QAC9C,CAAC;IACH,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC;IAEf,MAAM,MAAM,GAAG,WAAW,CACxB,KAAK,EACH,OAAe,EACf,aAAsB,EACtB,OAAsC,EACtC,EAAE;QACF,IAAI,YAAY;YAAE,OAAO;QACzB,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,MAAM,GAAG,GAAG,mDAAmD,CAAC;YAChE,cAAc,CAAC,GAAG,CAAC,CAAC;YACpB,OAAO,EAAE,CAAC,GAAG,CAAC,CAAC;YACf,OAAO;QACT,CAAC;QAED,eAAe,CAAC,IAAI,CAAC,CAAC;QACtB,cAAc,CAAC,IAAI,CAAC,CAAC;QAErB,IAAI,CAAC;YACH,MAAM,aAAa,GAAG;gBACpB,GAAG;gBACH,gBAAgB,EAAE,SAAS,CAAC,UAAU;oBACpC,CAAC,CAAC,SAAS,CAAC,OAAO,EAAE;oBACrB,CAAC,CAAC,SAAS;gBACb,eAAe,EAAE,eAAe,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,SAAS;gBACzE,SAAS,EAAE,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS;gBACvD,QAAQ,EAAE,QAAQ,IAAI,SAAS;gBAC/B,OAAO;aACR,CAAC;YAEF,MAAM,eAAe,GAAG;gBACtB,GAAG;gBACH,OAAO;gBACP,SAAS,EAAE,aAAa,IAAI,YAAY;gBACxC,UAAU,EAAE,OAAO,EAAE,UAAU;gBAC/B,WAAW,EAAE,OAAO,EAAE,WAAW;aAClC,CAAC;YAEF,IAAI,SAAiB,CAAC;YAEtB,IAAI,QAAQ,IAAI,UAAU,EAAE,CAAC;gBAC3B,IAAI,UAAU,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;oBAChC,CAAC,EAAE,SAAS,EAAE,GAAG,MAAM,aAAa,CAAC;wBACnC,GAAG,aAAa;wBAChB,eAAe,EAAE,UAAU,CAAC,UAAU;qBACvC,CAAC,CAAC,CAAC;gBACN,CAAC;qBAAM,CAAC;oBACN,CAAC,EAAE,SAAS,EAAE,GAAG,MAAM,aAAa,CAAC;wBACnC,GAAG,aAAa;wBAChB,QAAQ;qBACT,CAAC,CAAC,CAAC;gBACN,CAAC;YACH,CAAC;iBAAM,CAAC;gBACN,MAAM,iBAAiB,GAAG,YAAY,EAAE,MAAM,EAAE,iBAAiB,CAAC;gBAClE,IAAI,CAAC,iBAAiB,EAAE,CAAC;oBACvB,IAAI,qBAAqB,EAAE,CAAC;wBAC1B,MAAM,IAAI,KAAK,CACb,sDAAsD,CACvD,CAAC;oBACJ,CAAC;oBACD,IAAI,iBAAiB,EAAE,CAAC;wBACtB,MAAM,IAAI,KAAK,CACb,iDAAiD,CAClD,CAAC;oBACJ,CAAC;oBACD,MAAM,IAAI,KAAK,CACb,iEAAiE,CAClE,CAAC;gBACJ,CAAC;gBACD,CAAC,EAAE,SAAS,EAAE,GAAG,MAAM,aAAa,CAAC;oBACnC,GAAG,aAAa;oBAChB,eAAe,EAAE,iBAAiB;iBACnC,CAAC,CAAC,CAAC;YACN,CAAC;YAED,MAAM,eAAe,CAAC,EAAE,GAAG,eAAe,EAAE,SAAS,EAAE,CAAC,CAAC;YACzD,gBAAgB,CAAC,KAAK,EAAE,CAAC;YACzB,gBAAgB,CAAC,SAAS,CAAC,CAAC;QAC9B,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,MAAM,GAAG,cAAc,CAAC,GAAG,EAAE,yBAAyB,CAAC,CAAC;YAC9D,cAAc,CAAC,MAAM,CAAC,CAAC;YACvB,OAAO,EAAE,CAAC,MAAM,CAAC,CAAC;QACpB,CAAC;gBAAS,CAAC;YACT,eAAe,CAAC,KAAK,CAAC,CAAC;QACzB,CAAC;IACH,CAAC,EACD;QACE,YAAY;QACZ,GAAG;QACH,OAAO;QACP,YAAY;QACZ,SAAS;QACT,eAAe;QACf,SAAS;QACT,QAAQ;QACR,QAAQ;QACR,UAAU;QACV,YAAY;QACZ,qBAAqB;QACrB,iBAAiB;QACjB,aAAa;QACb,eAAe;QACf,gBAAgB;QAChB,gBAAgB;QAChB,OAAO;KACR,CACF,CAAC;IAEF,OAAO;QACL,OAAO;QACP,UAAU;QACV,OAAO,EAAE,YAAY;QACrB,UAAU;QACV,QAAQ;QACR,WAAW;QACX,UAAU;QACV,aAAa;QACb,eAAe;QACf,kBAAkB;QAClB,SAAS;QACT,YAAY;QACZ,QAAQ;QACR,WAAW;QACX,SAAS;QACT,gBAAgB;QAChB,YAAY;QACZ,WAAW;QACX,MAAM;KACP,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"useNewSessionFlow.js","sourceRoot":"","sources":["../../src/session/useNewSessionFlow.ts"],"names":[],"mappings":"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"}
@@ -174,6 +174,7 @@ export function ComposerToolbar({
174
174
  value={modelId}
175
175
  onValueChange={onModelChange}
176
176
  harness={showHarnessSelector ? undefined : harness}
177
+ initialHarness={showHarnessSelector ? harness : undefined}
177
178
  onHarnessChange={showHarnessSelector ? onHarnessChange : undefined}
178
179
  disabled={disabled}
179
180
  />
package/src/index.ts CHANGED
@@ -27,13 +27,15 @@ export { CloudFeatureNotice, type CloudFeatureNoticeProps } from "./internal/Clo
27
27
 
28
28
  // Models — data hook, styled components, and registry data
29
29
  export {
30
- MODEL_REGISTRY,
31
30
  DEFAULT_MODEL_ID,
32
31
  DEFAULT_CURSOR_MODEL_ID,
33
32
  DISABLED_PROVIDERS,
34
33
  modelKey,
35
34
  parseModelKey,
35
+ fetchModelRegistry,
36
+ parseRegistryJson,
36
37
  useModelRegistry,
38
+ ModelRegistryContext,
37
39
  ModelSelector,
38
40
  HarnessSelector,
39
41
  DEFAULT_HARNESS,
@@ -43,6 +45,7 @@ export {
43
45
  } from "./models";
44
46
  export type {
45
47
  ModelInfo,
48
+ ModelRegistryState,
46
49
  ParsedModelKey,
47
50
  Provider,
48
51
  CostTier,
@@ -0,0 +1,32 @@
1
+ "use client";
2
+
3
+ import { createContext, useContext } from "react";
4
+ import type { ModelInfo } from "./registry";
5
+
6
+ /** Internal state held by the model registry context provider. */
7
+ export interface ModelRegistryState {
8
+ readonly models: readonly ModelInfo[];
9
+ readonly isLoading: boolean;
10
+ readonly error: Error | null;
11
+ }
12
+
13
+ /**
14
+ * React context that holds the model registry fetched from the public API.
15
+ *
16
+ * Populated by {@link StigmerProvider} on mount. Consumer hooks read from
17
+ * this context instead of a static JSON import, enabling always-fresh
18
+ * model data without npm package updates.
19
+ */
20
+ export const ModelRegistryContext = createContext<ModelRegistryState>({
21
+ models: [],
22
+ isLoading: true,
23
+ error: null,
24
+ });
25
+
26
+ /**
27
+ * Internal hook to read the model registry from context.
28
+ * Throws if called outside a StigmerProvider.
29
+ */
30
+ export function useModelRegistryContext(): ModelRegistryState {
31
+ return useContext(ModelRegistryContext);
32
+ }
@@ -32,6 +32,14 @@ export interface ModelSelectorProps {
32
32
  * to that harness (dropdown hidden). When omitted, shows the harness dropdown.
33
33
  */
34
34
  readonly harness?: HarnessOption;
35
+ /**
36
+ * Initial harness value for the internal state when `harness` prop is
37
+ * undefined (unlocked mode). Prevents desync when the parent knows the
38
+ * active harness but delegates the dropdown to this component.
39
+ *
40
+ * When `harness` is provided (locked mode), this prop is ignored.
41
+ */
42
+ readonly initialHarness?: HarnessOption;
35
43
  /** Called when user changes harness in the dropdown. */
36
44
  readonly onHarnessChange?: (harness: HarnessOption) => void;
37
45
  /**
@@ -85,6 +93,7 @@ export function ModelSelector({
85
93
  value,
86
94
  onValueChange,
87
95
  harness,
96
+ initialHarness,
88
97
  onHarnessChange,
89
98
  onHarnessResolved,
90
99
  availableHarnesses,
@@ -99,9 +108,17 @@ export function ModelSelector({
99
108
  const portalContainer = useStigmerPortalContainer();
100
109
 
101
110
  const isHarnessLocked = harness !== undefined;
102
- const [internalHarness, setInternalHarness] = useState<HarnessOption>(harness ?? "native");
111
+ const [internalHarness, setInternalHarness] = useState<HarnessOption>(
112
+ harness ?? initialHarness ?? "native",
113
+ );
103
114
  const activeHarness = harness ?? internalHarness;
104
115
 
116
+ useEffect(() => {
117
+ if (!isHarnessLocked && initialHarness !== undefined) {
118
+ setInternalHarness(initialHarness);
119
+ }
120
+ }, [initialHarness, isHarnessLocked]);
121
+
105
122
  const { models, featured, defaultModel, getModel, byProvider } = useModelRegistry(
106
123
  { harness: activeHarness },
107
124
  );
@@ -122,7 +139,7 @@ export function ModelSelector({
122
139
  return HARNESS_OPTIONS.filter((h) => h === "native" || h === "cursor");
123
140
  }, [availableHarnesses]);
124
141
 
125
- const selectedModel = (value ? getModel(value) : undefined) ?? defaultModel;
142
+ const selectedModel = (value ? getModel(value) : undefined) ?? defaultModel ?? undefined;
126
143
 
127
144
  const isSearching = searchQuery.length > 0;
128
145
  const lowerQuery = searchQuery.toLowerCase();
@@ -248,7 +265,7 @@ export function ModelSelector({
248
265
 
249
266
  const showShowAllButton = !isSearching && !showAll && featuredModels.length > 0 && featuredModels.length < models.length;
250
267
 
251
- const triggerLabel = selectedModel.displayName;
268
+ const triggerLabel = selectedModel?.displayName ?? "Select model";
252
269
  const triggerHarness = !isHarnessLocked ? HARNESS_META[activeHarness].label : undefined;
253
270
 
254
271
  return (
@@ -393,7 +410,7 @@ export function ModelSelector({
393
410
  <ModelRow
394
411
  key={model.modelId}
395
412
  model={model}
396
- isSelected={model.modelId === selectedModel.modelId}
413
+ isSelected={model.modelId === selectedModel?.modelId}
397
414
  showDescription={false}
398
415
  showSpeedBadge={showSpeedBadge}
399
416
  onClick={() => selectModel(model)}
@@ -406,7 +423,7 @@ export function ModelSelector({
406
423
  <ModelRow
407
424
  key={model.modelId}
408
425
  model={model}
409
- isSelected={model.modelId === selectedModel.modelId}
426
+ isSelected={model.modelId === selectedModel?.modelId}
410
427
  isHighlighted={idx === highlightIdx}
411
428
  showDescription={showDescriptions && !compact && !isSearching && !showAll}
412
429
  showSpeedBadge={showSpeedBadge}
@@ -1,45 +1,137 @@
1
- import { describe, it, expect } from "vitest";
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
2
  import { renderHook } from "@testing-library/react";
3
+ import { type ReactNode } from "react";
3
4
  import { useModelRegistry } from "../useModelRegistry";
4
5
  import {
5
- MODEL_REGISTRY,
6
6
  DEFAULT_MODEL_ID,
7
7
  DEFAULT_CURSOR_MODEL_ID,
8
8
  DISABLED_PROVIDERS,
9
9
  modelKey,
10
+ parseRegistryJson,
10
11
  } from "../registry";
12
+ import { ModelRegistryContext } from "../ModelRegistryContext";
13
+ import type { ModelRegistryState } from "../ModelRegistryContext";
14
+ import type { ModelInfo } from "../registry";
15
+
16
+ /**
17
+ * Minimal inline registry data for tests. Mirrors the shape of the
18
+ * API response without depending on a static JSON file.
19
+ */
20
+ const TEST_REGISTRY_JSON = {
21
+ models: [
22
+ {
23
+ id: "claude-sonnet-4.6",
24
+ displayName: "Claude Sonnet 4.6",
25
+ shortDescription: "Balanced capability",
26
+ speedTier: "fast",
27
+ provider: "anthropic",
28
+ harness: "native",
29
+ costTier: "standard",
30
+ featured: true,
31
+ pricing: { inputPricePerMillion: 3, outputPricePerMillion: 15, cacheWritePricePerMillion: 3.75, cacheReadPricePerMillion: 0.3 },
32
+ },
33
+ {
34
+ id: "claude-opus-4.6",
35
+ displayName: "Claude Opus 4.6",
36
+ shortDescription: "Complex reasoning",
37
+ speedTier: "slow",
38
+ provider: "anthropic",
39
+ harness: "native",
40
+ costTier: "premium",
41
+ featured: true,
42
+ pricing: { inputPricePerMillion: 5, outputPricePerMillion: 25, cacheWritePricePerMillion: 6.25, cacheReadPricePerMillion: 0.5 },
43
+ },
44
+ {
45
+ id: "gpt-4o",
46
+ displayName: "GPT-4o",
47
+ shortDescription: "Fast reasoning",
48
+ speedTier: "fast",
49
+ provider: "openai",
50
+ harness: "native",
51
+ costTier: "standard",
52
+ featured: true,
53
+ pricing: { inputPricePerMillion: 2.5, outputPricePerMillion: 10, cacheWritePricePerMillion: 2.5, cacheReadPricePerMillion: 1.25 },
54
+ },
55
+ {
56
+ id: "default",
57
+ displayName: "Cursor Auto",
58
+ shortDescription: "Automatic model selection",
59
+ speedTier: "fast",
60
+ provider: "cursor",
61
+ harness: "cursor",
62
+ costTier: "standard",
63
+ featured: true,
64
+ pricing: { inputPricePerMillion: 1.25, outputPricePerMillion: 6, cacheWritePricePerMillion: 1.25, cacheReadPricePerMillion: 0.25 },
65
+ },
66
+ {
67
+ id: "claude-4.6-sonnet",
68
+ displayName: "Claude 4.6 Sonnet",
69
+ shortDescription: "Balanced Cursor model",
70
+ speedTier: "fast",
71
+ provider: "anthropic",
72
+ harness: "cursor",
73
+ costTier: "standard",
74
+ featured: false,
75
+ pricing: { inputPricePerMillion: 3, outputPricePerMillion: 15, cacheWritePricePerMillion: 3.75, cacheReadPricePerMillion: 0.3 },
76
+ },
77
+ {
78
+ id: "ollama-local",
79
+ displayName: "Ollama Local",
80
+ shortDescription: "Local model",
81
+ speedTier: "fast",
82
+ provider: "ollama",
83
+ harness: "native",
84
+ costTier: "economy",
85
+ featured: false,
86
+ pricing: { inputPricePerMillion: 0, outputPricePerMillion: 0, cacheWritePricePerMillion: 0, cacheReadPricePerMillion: 0 },
87
+ },
88
+ ],
89
+ };
90
+
91
+ const TEST_MODELS: readonly ModelInfo[] = parseRegistryJson(TEST_REGISTRY_JSON);
92
+
93
+ function createWrapper(models: readonly ModelInfo[] = TEST_MODELS) {
94
+ const state: ModelRegistryState = { models, isLoading: false, error: null };
95
+ return function Wrapper({ children }: { children: ReactNode }) {
96
+ return (
97
+ <ModelRegistryContext.Provider value={state}>
98
+ {children}
99
+ </ModelRegistryContext.Provider>
100
+ );
101
+ };
102
+ }
11
103
 
12
104
  describe("useModelRegistry", () => {
13
105
  describe("unified mode (no harness)", () => {
14
106
  it("excludes DISABLED_PROVIDERS from models", () => {
15
- const { result } = renderHook(() => useModelRegistry());
107
+ const { result } = renderHook(() => useModelRegistry(), { wrapper: createWrapper() });
16
108
  for (const model of result.current.models) {
17
109
  expect(DISABLED_PROVIDERS.has(model.provider)).toBe(false);
18
110
  }
19
111
  });
20
112
 
21
113
  it("includes models from both native and cursor harnesses", () => {
22
- const { result } = renderHook(() => useModelRegistry());
114
+ const { result } = renderHook(() => useModelRegistry(), { wrapper: createWrapper() });
23
115
  const harnesses = new Set(result.current.models.map((m) => m.harness));
24
116
  expect(harnesses.has("native")).toBe(true);
25
117
  expect(harnesses.has("cursor")).toBe(true);
26
118
  });
27
119
 
28
- it("includes all non-disabled models from MODEL_REGISTRY", () => {
29
- const { result } = renderHook(() => useModelRegistry());
30
- const expected = MODEL_REGISTRY.filter(
120
+ it("includes all non-disabled models", () => {
121
+ const { result } = renderHook(() => useModelRegistry(), { wrapper: createWrapper() });
122
+ const expected = TEST_MODELS.filter(
31
123
  (m) => !DISABLED_PROVIDERS.has(m.provider),
32
124
  );
33
125
  expect(result.current.models).toHaveLength(expected.length);
34
126
  });
35
127
 
36
128
  it("resolves defaultModel to DEFAULT_MODEL_ID", () => {
37
- const { result } = renderHook(() => useModelRegistry());
38
- expect(result.current.defaultModel.modelId).toBe(DEFAULT_MODEL_ID);
129
+ const { result } = renderHook(() => useModelRegistry(), { wrapper: createWrapper() });
130
+ expect(result.current.defaultModel!.modelId).toBe(DEFAULT_MODEL_ID);
39
131
  });
40
132
 
41
133
  it("groups models by provider in byProvider map", () => {
42
- const { result } = renderHook(() => useModelRegistry());
134
+ const { result } = renderHook(() => useModelRegistry(), { wrapper: createWrapper() });
43
135
  for (const [provider, models] of result.current.byProvider) {
44
136
  for (const m of models) {
45
137
  expect(m.provider).toBe(provider);
@@ -48,21 +140,21 @@ describe("useModelRegistry", () => {
48
140
  });
49
141
 
50
142
  it("returns providers matching byProvider keys in order", () => {
51
- const { result } = renderHook(() => useModelRegistry());
143
+ const { result } = renderHook(() => useModelRegistry(), { wrapper: createWrapper() });
52
144
  const fromMap = Array.from(result.current.byProvider.keys());
53
145
  expect(result.current.providers).toEqual(fromMap);
54
146
  });
55
147
 
56
148
  it("looks up enabled models by getModel", () => {
57
- const { result } = renderHook(() => useModelRegistry());
149
+ const { result } = renderHook(() => useModelRegistry(), { wrapper: createWrapper() });
58
150
  const model = result.current.getModel(DEFAULT_MODEL_ID);
59
151
  expect(model).toBeDefined();
60
152
  expect(model!.modelId).toBe(DEFAULT_MODEL_ID);
61
153
  });
62
154
 
63
155
  it("returns undefined for disabled provider models via getModel", () => {
64
- const { result } = renderHook(() => useModelRegistry());
65
- const disabledModel = MODEL_REGISTRY.find((m) =>
156
+ const { result } = renderHook(() => useModelRegistry(), { wrapper: createWrapper() });
157
+ const disabledModel = TEST_MODELS.find((m) =>
66
158
  DISABLED_PROVIDERS.has(m.provider),
67
159
  );
68
160
  if (disabledModel) {
@@ -71,12 +163,12 @@ describe("useModelRegistry", () => {
71
163
  });
72
164
 
73
165
  it("returns undefined for unknown model IDs via getModel", () => {
74
- const { result } = renderHook(() => useModelRegistry());
166
+ const { result } = renderHook(() => useModelRegistry(), { wrapper: createWrapper() });
75
167
  expect(result.current.getModel("nonexistent-model")).toBeUndefined();
76
168
  });
77
169
 
78
170
  it("returns featured models subset", () => {
79
- const { result } = renderHook(() => useModelRegistry());
171
+ const { result } = renderHook(() => useModelRegistry(), { wrapper: createWrapper() });
80
172
  expect(result.current.featured.length).toBeGreaterThan(0);
81
173
  for (const model of result.current.featured) {
82
174
  expect(model.featured).toBe(true);
@@ -84,7 +176,7 @@ describe("useModelRegistry", () => {
84
176
  });
85
177
 
86
178
  it("featured models are a subset of all models", () => {
87
- const { result } = renderHook(() => useModelRegistry());
179
+ const { result } = renderHook(() => useModelRegistry(), { wrapper: createWrapper() });
88
180
  const allKeys = new Set(
89
181
  result.current.models.map((m) => modelKey(m.harness, m.modelId)),
90
182
  );
@@ -94,7 +186,7 @@ describe("useModelRegistry", () => {
94
186
  });
95
187
 
96
188
  it("resolves models by compound key via getByKey", () => {
97
- const { result } = renderHook(() => useModelRegistry());
189
+ const { result } = renderHook(() => useModelRegistry(), { wrapper: createWrapper() });
98
190
  const key = modelKey("native", DEFAULT_MODEL_ID);
99
191
  const model = result.current.getByKey(key);
100
192
  expect(model).toBeDefined();
@@ -103,7 +195,7 @@ describe("useModelRegistry", () => {
103
195
  });
104
196
 
105
197
  it("resolves cursor models by compound key via getByKey", () => {
106
- const { result } = renderHook(() => useModelRegistry());
198
+ const { result } = renderHook(() => useModelRegistry(), { wrapper: createWrapper() });
107
199
  const key = modelKey("cursor", DEFAULT_CURSOR_MODEL_ID);
108
200
  const model = result.current.getByKey(key);
109
201
  expect(model).toBeDefined();
@@ -112,7 +204,7 @@ describe("useModelRegistry", () => {
112
204
  });
113
205
 
114
206
  it("returns undefined for unknown compound keys via getByKey", () => {
115
- const { result } = renderHook(() => useModelRegistry());
207
+ const { result } = renderHook(() => useModelRegistry(), { wrapper: createWrapper() });
116
208
  expect(result.current.getByKey("native/nonexistent")).toBeUndefined();
117
209
  });
118
210
  });
@@ -121,7 +213,7 @@ describe("useModelRegistry", () => {
121
213
  it("shows only native-harness models", () => {
122
214
  const { result } = renderHook(() =>
123
215
  useModelRegistry({ harness: "native" }),
124
- );
216
+ { wrapper: createWrapper() });
125
217
  for (const model of result.current.models) {
126
218
  expect(model.harness).toBe("native");
127
219
  }
@@ -130,7 +222,7 @@ describe("useModelRegistry", () => {
130
222
  it("excludes cursor-harness models", () => {
131
223
  const { result } = renderHook(() =>
132
224
  useModelRegistry({ harness: "native" }),
133
- );
225
+ { wrapper: createWrapper() });
134
226
  const cursorModels = result.current.models.filter(
135
227
  (m) => m.harness === "cursor",
136
228
  );
@@ -140,10 +232,10 @@ describe("useModelRegistry", () => {
140
232
  it("resolves defaultModel to the first featured native model", () => {
141
233
  const { result } = renderHook(() =>
142
234
  useModelRegistry({ harness: "native" }),
143
- );
235
+ { wrapper: createWrapper() });
144
236
  const featured = result.current.featured;
145
237
  expect(featured.length).toBeGreaterThan(0);
146
- expect(result.current.defaultModel.modelId).toBe(featured[0].modelId);
238
+ expect(result.current.defaultModel!.modelId).toBe(featured[0].modelId);
147
239
  });
148
240
  });
149
241
 
@@ -151,7 +243,7 @@ describe("useModelRegistry", () => {
151
243
  it("shows only cursor-harness models", () => {
152
244
  const { result } = renderHook(() =>
153
245
  useModelRegistry({ harness: "cursor" }),
154
- );
246
+ { wrapper: createWrapper() });
155
247
  for (const model of result.current.models) {
156
248
  expect(model.harness).toBe("cursor");
157
249
  }
@@ -160,26 +252,16 @@ describe("useModelRegistry", () => {
160
252
  it("includes cursor-harness models from multiple providers", () => {
161
253
  const { result } = renderHook(() =>
162
254
  useModelRegistry({ harness: "cursor" }),
163
- );
255
+ { wrapper: createWrapper() });
164
256
  const providers = new Set(result.current.models.map((m) => m.provider));
165
257
  expect(providers.size).toBeGreaterThan(1);
166
258
  });
167
259
 
168
- it("includes all cursor-harness models from MODEL_REGISTRY", () => {
169
- const { result } = renderHook(() =>
170
- useModelRegistry({ harness: "cursor" }),
171
- );
172
- const cursorModels = MODEL_REGISTRY.filter(
173
- (m) => m.harness === "cursor",
174
- );
175
- expect(result.current.models).toHaveLength(cursorModels.length);
176
- });
177
-
178
260
  it("resolves defaultModel to DEFAULT_CURSOR_MODEL_ID", () => {
179
261
  const { result } = renderHook(() =>
180
262
  useModelRegistry({ harness: "cursor" }),
181
- );
182
- expect(result.current.defaultModel.modelId).toBe(
263
+ { wrapper: createWrapper() });
264
+ expect(result.current.defaultModel!.modelId).toBe(
183
265
  DEFAULT_CURSOR_MODEL_ID,
184
266
  );
185
267
  });
@@ -187,7 +269,7 @@ describe("useModelRegistry", () => {
187
269
  it("looks up cursor models via getModel", () => {
188
270
  const { result } = renderHook(() =>
189
271
  useModelRegistry({ harness: "cursor" }),
190
- );
272
+ { wrapper: createWrapper() });
191
273
  expect(
192
274
  result.current.getModel(DEFAULT_CURSOR_MODEL_ID),
193
275
  ).toBeDefined();
@@ -196,14 +278,41 @@ describe("useModelRegistry", () => {
196
278
  it("cannot look up native-only models via getModel", () => {
197
279
  const { result } = renderHook(() =>
198
280
  useModelRegistry({ harness: "cursor" }),
199
- );
281
+ { wrapper: createWrapper() });
200
282
  expect(result.current.getModel(DEFAULT_MODEL_ID)).toBeUndefined();
201
283
  });
202
284
  });
203
285
 
286
+ describe("loading state", () => {
287
+ it("exposes isLoading from context", () => {
288
+ const loadingState: ModelRegistryState = { models: [], isLoading: true, error: null };
289
+ const wrapper = ({ children }: { children: ReactNode }) => (
290
+ <ModelRegistryContext.Provider value={loadingState}>
291
+ {children}
292
+ </ModelRegistryContext.Provider>
293
+ );
294
+ const { result } = renderHook(() => useModelRegistry(), { wrapper });
295
+ expect(result.current.isLoading).toBe(true);
296
+ expect(result.current.models).toHaveLength(0);
297
+ });
298
+
299
+ it("exposes error from context", () => {
300
+ const err = new Error("fetch failed");
301
+ const errorState: ModelRegistryState = { models: [], isLoading: false, error: err };
302
+ const wrapper = ({ children }: { children: ReactNode }) => (
303
+ <ModelRegistryContext.Provider value={errorState}>
304
+ {children}
305
+ </ModelRegistryContext.Provider>
306
+ );
307
+ const { result } = renderHook(() => useModelRegistry(), { wrapper });
308
+ expect(result.current.error).toBe(err);
309
+ expect(result.current.isLoading).toBe(false);
310
+ });
311
+ });
312
+
204
313
  describe("defaultModel fallback", () => {
205
314
  it("falls back to the first enabled model when default ID is missing", () => {
206
- const { result } = renderHook(() => useModelRegistry());
315
+ const { result } = renderHook(() => useModelRegistry(), { wrapper: createWrapper() });
207
316
  expect(result.current.defaultModel).toBeDefined();
208
317
  expect(result.current.models).toContain(result.current.defaultModel);
209
318
  });
@@ -1,8 +1,10 @@
1
- export { MODEL_REGISTRY, DEFAULT_MODEL_ID, DEFAULT_CURSOR_MODEL_ID, DISABLED_PROVIDERS, modelKey, parseModelKey, resolveDefaultModelId } from "./registry";
1
+ export { DEFAULT_MODEL_ID, DEFAULT_CURSOR_MODEL_ID, DISABLED_PROVIDERS, modelKey, parseModelKey, resolveDefaultModelId, fetchModelRegistry, parseRegistryJson } from "./registry";
2
2
  export type { ParsedModelKey, DefaultModelResolution, DefaultModelSource } from "./registry";
3
3
  export type { ModelInfo, Provider, CostTier, SpeedTier } from "./registry";
4
4
  export { useModelRegistry } from "./useModelRegistry";
5
5
  export type { UseModelRegistryReturn, UseModelRegistryOptions } from "./useModelRegistry";
6
+ export { ModelRegistryContext, useModelRegistryContext } from "./ModelRegistryContext";
7
+ export type { ModelRegistryState } from "./ModelRegistryContext";
6
8
  export { ModelSelector } from "./ModelSelector";
7
9
  export type { ModelSelectorProps } from "./ModelSelector";
8
10
  export { HarnessSelector } from "./HarnessSelector";