@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.
- package/composer/ComposerToolbar.d.ts.map +1 -1
- package/composer/ComposerToolbar.js +1 -1
- package/composer/ComposerToolbar.js.map +1 -1
- package/index.d.ts +2 -2
- package/index.d.ts.map +1 -1
- package/index.js +1 -1
- package/index.js.map +1 -1
- package/models/ModelRegistryContext.d.ts +21 -0
- package/models/ModelRegistryContext.d.ts.map +1 -0
- package/models/ModelRegistryContext.js +22 -0
- package/models/ModelRegistryContext.js.map +1 -0
- package/models/ModelSelector.d.ts +9 -1
- package/models/ModelSelector.d.ts.map +1 -1
- package/models/ModelSelector.js +10 -5
- package/models/ModelSelector.js.map +1 -1
- package/models/__tests__/useModelRegistry.test.js +127 -32
- package/models/__tests__/useModelRegistry.test.js.map +1 -1
- package/models/index.d.ts +3 -1
- package/models/index.d.ts.map +1 -1
- package/models/index.js +2 -1
- package/models/index.js.map +1 -1
- package/models/registry.d.ts +20 -12
- package/models/registry.d.ts.map +1 -1
- package/models/registry.js +51 -27
- package/models/registry.js.map +1 -1
- package/models/useModelRegistry.d.ts +11 -3
- package/models/useModelRegistry.d.ts.map +1 -1
- package/models/useModelRegistry.js +13 -5
- package/models/useModelRegistry.js.map +1 -1
- package/package.json +4 -4
- package/provider.d.ts.map +1 -1
- package/provider.js +42 -1
- package/provider.js.map +1 -1
- package/session/__tests__/useNewSessionFlow.test.js +89 -18
- package/session/__tests__/useNewSessionFlow.test.js.map +1 -1
- package/session/__tests__/usePersistedModel.test.js +26 -12
- package/session/__tests__/usePersistedModel.test.js.map +1 -1
- package/session/useNewSessionFlow.d.ts.map +1 -1
- package/session/useNewSessionFlow.js +20 -6
- package/session/useNewSessionFlow.js.map +1 -1
- package/src/composer/ComposerToolbar.tsx +1 -0
- package/src/index.ts +4 -1
- package/src/models/ModelRegistryContext.ts +32 -0
- package/src/models/ModelSelector.tsx +22 -5
- package/src/models/__tests__/useModelRegistry.test.tsx +150 -41
- package/src/models/index.ts +3 -1
- package/src/models/registry.ts +51 -30
- package/src/models/useModelRegistry.ts +18 -7
- package/src/provider.tsx +58 -8
- package/src/session/__tests__/useNewSessionFlow.test.tsx +120 -18
- package/src/session/__tests__/usePersistedModel.test.tsx +33 -12
- 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
|
|
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
|
|
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;
|
|
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>(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
29
|
-
const { result } = renderHook(() => useModelRegistry());
|
|
30
|
-
const expected =
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
});
|
package/src/models/index.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
export {
|
|
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";
|