@stigmer/react 0.4.0 → 0.4.2
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/mcp-server/McpServerDetailView.d.ts.map +1 -1
- package/mcp-server/McpServerDetailView.js +3 -3
- package/mcp-server/McpServerDetailView.js.map +1 -1
- package/mcp-server/useMcpServerOAuthConnect.d.ts.map +1 -1
- package/mcp-server/useMcpServerOAuthConnect.js +37 -9
- package/mcp-server/useMcpServerOAuthConnect.js.map +1 -1
- package/models/ModelSelector.d.ts +34 -18
- package/models/ModelSelector.d.ts.map +1 -1
- package/models/ModelSelector.js +76 -47
- package/models/ModelSelector.js.map +1 -1
- package/models/__tests__/useModelRegistry.test.js +4 -2
- package/models/__tests__/useModelRegistry.test.js.map +1 -1
- package/models/harness.d.ts +26 -2
- package/models/harness.d.ts.map +1 -1
- package/models/harness.js +29 -4
- package/models/harness.js.map +1 -1
- package/models/index.d.ts +5 -5
- package/models/index.d.ts.map +1 -1
- package/models/index.js +2 -2
- package/models/index.js.map +1 -1
- package/models/registry.d.ts +49 -2
- package/models/registry.d.ts.map +1 -1
- package/models/registry.js +45 -3
- package/models/registry.js.map +1 -1
- package/models/useModelRegistry.d.ts.map +1 -1
- package/models/useModelRegistry.js +7 -12
- package/models/useModelRegistry.js.map +1 -1
- package/package.json +4 -4
- package/src/mcp-server/McpServerDetailView.tsx +15 -0
- package/src/mcp-server/useMcpServerOAuthConnect.ts +37 -9
- package/src/models/ModelSelector.tsx +234 -97
- package/src/models/__tests__/useModelRegistry.test.tsx +4 -2
- package/src/models/harness.ts +51 -5
- package/src/models/index.ts +5 -5
- package/src/models/registry.ts +96 -3
- package/src/models/useModelRegistry.ts +6 -8
- package/styles.css +1 -1
|
@@ -4,8 +4,8 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
|
4
4
|
import { Popover } from "@base-ui/react/popover";
|
|
5
5
|
import { cn } from "@stigmer/theme";
|
|
6
6
|
import { useModelRegistry } from "./useModelRegistry";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import type { ModelInfo, CostTier, SpeedTier } from "./registry";
|
|
8
|
+
import { HARNESS_META, HARNESS_OPTIONS, type HarnessOption } from "./harness";
|
|
9
9
|
|
|
10
10
|
const COST_TIER_LABEL: Record<CostTier, string> = {
|
|
11
11
|
economy: "$",
|
|
@@ -13,46 +13,70 @@ const COST_TIER_LABEL: Record<CostTier, string> = {
|
|
|
13
13
|
premium: "$$$",
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
+
const SPEED_TIER_LABEL: Record<SpeedTier, string> = {
|
|
17
|
+
fastest: "Fastest",
|
|
18
|
+
fast: "Fast",
|
|
19
|
+
balanced: "Balanced",
|
|
20
|
+
slow: "Powerful",
|
|
21
|
+
};
|
|
22
|
+
|
|
16
23
|
/** Props for {@link ModelSelector}. */
|
|
17
24
|
export interface ModelSelectorProps {
|
|
18
|
-
/** Currently selected
|
|
25
|
+
/** Currently selected model ID. */
|
|
19
26
|
readonly value?: string;
|
|
20
27
|
/** Called when the user picks a different model. Receives the `modelId`. */
|
|
21
28
|
readonly onValueChange: (modelId: string) => void;
|
|
22
29
|
/**
|
|
23
|
-
* When provided
|
|
24
|
-
* When omitted, shows the
|
|
30
|
+
* Current harness. When provided as a single value, locks the selector
|
|
31
|
+
* to that harness (dropdown hidden). When omitted, shows the harness dropdown.
|
|
25
32
|
*/
|
|
26
33
|
readonly harness?: HarnessOption;
|
|
34
|
+
/** Called when user changes harness in the dropdown. */
|
|
35
|
+
readonly onHarnessChange?: (harness: HarnessOption) => void;
|
|
27
36
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
37
|
+
* Restrict which harnesses appear in the dropdown.
|
|
38
|
+
* When omitted, shows all registered harnesses that have models in the registry.
|
|
30
39
|
*/
|
|
31
|
-
readonly
|
|
40
|
+
readonly availableHarnesses?: readonly HarnessOption[];
|
|
41
|
+
/** Override the curated (featured) list for the current harness. */
|
|
42
|
+
readonly curatedModels?: readonly string[];
|
|
43
|
+
/** Grouping in the "Show All" expanded view. Default: "provider". */
|
|
44
|
+
readonly groupBy?: "provider" | "tier" | "none";
|
|
45
|
+
/** Show speed tier badge. Default: true. */
|
|
46
|
+
readonly showSpeedBadge?: boolean;
|
|
47
|
+
/** Show short descriptions in curated view. Default: true. */
|
|
48
|
+
readonly showDescriptions?: boolean;
|
|
49
|
+
/** Compact mode: smaller trigger, no descriptions. Default: false. */
|
|
50
|
+
readonly compact?: boolean;
|
|
32
51
|
/** Additional CSS class names for the trigger button. */
|
|
33
52
|
readonly className?: string;
|
|
34
53
|
/** When true, disables the selector. */
|
|
35
54
|
readonly disabled?: boolean;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @deprecated Use {@link onHarnessChange} instead.
|
|
58
|
+
*/
|
|
59
|
+
readonly onHarnessResolved?: (harness: HarnessOption) => void;
|
|
36
60
|
}
|
|
37
61
|
|
|
38
62
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* Shows a curated list of featured models by default. The user can
|
|
42
|
-
* expand via "Show All Models" or type to search the full catalog.
|
|
63
|
+
* Combined harness + model picker with a compact trigger button.
|
|
43
64
|
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
65
|
+
* Shows a harness dropdown at the top of the popover (when not locked
|
|
66
|
+
* to a single harness), followed by a curated model list scoped to
|
|
67
|
+
* the selected harness. Supports search and progressive disclosure
|
|
68
|
+
* via "Show All Models."
|
|
46
69
|
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
70
|
+
* The trigger button displays the current selection in compact format:
|
|
71
|
+
* `Harness · Model Name ▾` (or just `Model Name ▾` when harness is locked).
|
|
49
72
|
*
|
|
50
73
|
* @example
|
|
51
74
|
* ```tsx
|
|
52
75
|
* <ModelSelector
|
|
53
|
-
* value={
|
|
54
|
-
* onValueChange={
|
|
55
|
-
*
|
|
76
|
+
* value={modelId}
|
|
77
|
+
* onValueChange={setModelId}
|
|
78
|
+
* harness={harness}
|
|
79
|
+
* onHarnessChange={setHarness}
|
|
56
80
|
* />
|
|
57
81
|
* ```
|
|
58
82
|
*/
|
|
@@ -60,13 +84,23 @@ export function ModelSelector({
|
|
|
60
84
|
value,
|
|
61
85
|
onValueChange,
|
|
62
86
|
harness,
|
|
87
|
+
onHarnessChange,
|
|
63
88
|
onHarnessResolved,
|
|
89
|
+
availableHarnesses,
|
|
90
|
+
curatedModels,
|
|
91
|
+
groupBy = "provider",
|
|
92
|
+
showSpeedBadge = true,
|
|
93
|
+
showDescriptions = true,
|
|
94
|
+
compact = false,
|
|
64
95
|
className,
|
|
65
96
|
disabled,
|
|
66
97
|
}: ModelSelectorProps) {
|
|
67
|
-
const
|
|
68
|
-
const
|
|
69
|
-
|
|
98
|
+
const isHarnessLocked = harness !== undefined;
|
|
99
|
+
const [internalHarness, setInternalHarness] = useState<HarnessOption>(harness ?? "native");
|
|
100
|
+
const activeHarness = harness ?? internalHarness;
|
|
101
|
+
|
|
102
|
+
const { models, featured, defaultModel, getModel, byProvider } = useModelRegistry(
|
|
103
|
+
{ harness: activeHarness },
|
|
70
104
|
);
|
|
71
105
|
|
|
72
106
|
const [open, setOpen] = useState(false);
|
|
@@ -77,31 +111,56 @@ export function ModelSelector({
|
|
|
77
111
|
const searchRef = useRef<HTMLInputElement>(null);
|
|
78
112
|
const listRef = useRef<HTMLDivElement>(null);
|
|
79
113
|
|
|
80
|
-
const
|
|
81
|
-
if (
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
return getModel(value);
|
|
87
|
-
}, [value, isUnified, getByKey, getModel]);
|
|
114
|
+
const resolvedHarnesses = useMemo(() => {
|
|
115
|
+
if (availableHarnesses) return availableHarnesses;
|
|
116
|
+
// For now, show native and cursor (the two harnesses with models in the registry).
|
|
117
|
+
// Future harnesses will be added to the registry and appear here automatically.
|
|
118
|
+
return HARNESS_OPTIONS.filter((h) => h === "native" || h === "cursor");
|
|
119
|
+
}, [availableHarnesses]);
|
|
88
120
|
|
|
89
|
-
const selectedModel =
|
|
121
|
+
const selectedModel = (value ? getModel(value) : undefined) ?? defaultModel;
|
|
90
122
|
|
|
91
123
|
const isSearching = searchQuery.length > 0;
|
|
92
124
|
const lowerQuery = searchQuery.toLowerCase();
|
|
93
125
|
|
|
126
|
+
const curatedSet = useMemo(() => {
|
|
127
|
+
if (curatedModels) return new Set(curatedModels);
|
|
128
|
+
return null;
|
|
129
|
+
}, [curatedModels]);
|
|
130
|
+
|
|
131
|
+
const featuredModels = useMemo(() => {
|
|
132
|
+
if (curatedSet) {
|
|
133
|
+
return models.filter((m) => curatedSet.has(m.modelId));
|
|
134
|
+
}
|
|
135
|
+
return featured;
|
|
136
|
+
}, [models, featured, curatedSet]);
|
|
137
|
+
|
|
94
138
|
const visibleModels: readonly ModelInfo[] = useMemo(() => {
|
|
95
139
|
if (isSearching) {
|
|
96
140
|
return models.filter((m) =>
|
|
97
141
|
m.displayName.toLowerCase().includes(lowerQuery)
|
|
98
142
|
|| m.modelId.toLowerCase().includes(lowerQuery)
|
|
99
|
-
||
|
|
143
|
+
|| m.shortDescription.toLowerCase().includes(lowerQuery),
|
|
100
144
|
);
|
|
101
145
|
}
|
|
102
146
|
if (showAll) return models;
|
|
103
|
-
return
|
|
104
|
-
}, [models,
|
|
147
|
+
return featuredModels.length > 0 ? featuredModels : models;
|
|
148
|
+
}, [models, featuredModels, isSearching, showAll, lowerQuery]);
|
|
149
|
+
|
|
150
|
+
const groupedModels = useMemo(() => {
|
|
151
|
+
if (!showAll || groupBy === "none" || isSearching) return null;
|
|
152
|
+
const groups = new Map<string, ModelInfo[]>();
|
|
153
|
+
for (const model of models) {
|
|
154
|
+
const key = groupBy === "provider" ? model.provider : model.costTier;
|
|
155
|
+
const group = groups.get(key);
|
|
156
|
+
if (group) {
|
|
157
|
+
group.push(model);
|
|
158
|
+
} else {
|
|
159
|
+
groups.set(key, [model]);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return groups;
|
|
163
|
+
}, [models, showAll, groupBy, isSearching]);
|
|
105
164
|
|
|
106
165
|
useEffect(() => {
|
|
107
166
|
setHighlightIdx(-1);
|
|
@@ -121,16 +180,23 @@ export function ModelSelector({
|
|
|
121
180
|
}
|
|
122
181
|
}, [open]);
|
|
123
182
|
|
|
183
|
+
const handleHarnessChange = useCallback(
|
|
184
|
+
(newHarness: HarnessOption) => {
|
|
185
|
+
setInternalHarness(newHarness);
|
|
186
|
+
onHarnessChange?.(newHarness);
|
|
187
|
+
onHarnessResolved?.(newHarness);
|
|
188
|
+
setShowAll(false);
|
|
189
|
+
setSearchQuery("");
|
|
190
|
+
},
|
|
191
|
+
[onHarnessChange, onHarnessResolved],
|
|
192
|
+
);
|
|
193
|
+
|
|
124
194
|
const selectModel = useCallback(
|
|
125
195
|
(model: ModelInfo) => {
|
|
126
|
-
|
|
127
|
-
onValueChange(key);
|
|
128
|
-
if (isUnified && onHarnessResolved && model.harness !== selectedModel?.harness) {
|
|
129
|
-
onHarnessResolved(model.harness);
|
|
130
|
-
}
|
|
196
|
+
onValueChange(model.modelId);
|
|
131
197
|
setOpen(false);
|
|
132
198
|
},
|
|
133
|
-
[
|
|
199
|
+
[onValueChange],
|
|
134
200
|
);
|
|
135
201
|
|
|
136
202
|
const scrollHighlightIntoView = useCallback((idx: number) => {
|
|
@@ -175,11 +241,10 @@ export function ModelSelector({
|
|
|
175
241
|
[visibleModels, highlightIdx, selectModel, scrollHighlightIntoView],
|
|
176
242
|
);
|
|
177
243
|
|
|
178
|
-
const showShowAllButton = !isSearching && !showAll &&
|
|
244
|
+
const showShowAllButton = !isSearching && !showAll && featuredModels.length > 0 && featuredModels.length < models.length;
|
|
179
245
|
|
|
180
246
|
const triggerLabel = selectedModel.displayName;
|
|
181
|
-
const triggerHarness =
|
|
182
|
-
const triggerCost = COST_TIER_LABEL[selectedModel.costTier];
|
|
247
|
+
const triggerHarness = !isHarnessLocked ? HARNESS_META[activeHarness].label : undefined;
|
|
183
248
|
|
|
184
249
|
return (
|
|
185
250
|
<Popover.Root open={open} onOpenChange={setOpen}>
|
|
@@ -190,17 +255,17 @@ export function ModelSelector({
|
|
|
190
255
|
"bg-background px-2.5 py-1.5 text-xs text-foreground",
|
|
191
256
|
"hover:bg-accent-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
192
257
|
"disabled:pointer-events-none disabled:opacity-50",
|
|
193
|
-
"transition-colors max-w-[
|
|
258
|
+
"transition-colors max-w-[20rem] max-sm:max-w-[12rem]",
|
|
194
259
|
className,
|
|
195
260
|
)}
|
|
196
261
|
>
|
|
197
|
-
<span className="truncate">{triggerLabel}</span>
|
|
198
262
|
{triggerHarness && (
|
|
199
|
-
<span className="shrink-0
|
|
200
|
-
{triggerHarness}
|
|
201
|
-
</span>
|
|
263
|
+
<span className="shrink-0 text-muted-foreground">{triggerHarness}</span>
|
|
202
264
|
)}
|
|
203
|
-
|
|
265
|
+
{triggerHarness && (
|
|
266
|
+
<span className="shrink-0 text-border" aria-hidden>·</span>
|
|
267
|
+
)}
|
|
268
|
+
<span className="truncate">{triggerLabel}</span>
|
|
204
269
|
<ChevronIcon />
|
|
205
270
|
</Popover.Trigger>
|
|
206
271
|
|
|
@@ -210,12 +275,36 @@ export function ModelSelector({
|
|
|
210
275
|
role="dialog"
|
|
211
276
|
aria-label="Model selector"
|
|
212
277
|
className={cn(
|
|
213
|
-
"z-popover w-
|
|
278
|
+
"z-popover w-80 rounded-lg border border-border bg-popover shadow-md",
|
|
214
279
|
"text-popover-foreground",
|
|
215
280
|
)}
|
|
216
281
|
>
|
|
282
|
+
{/* Harness dropdown (only when not locked) */}
|
|
283
|
+
{!isHarnessLocked && (
|
|
284
|
+
<div className="border-b border-border px-3 py-2">
|
|
285
|
+
<label className="mb-1 block text-[0.6rem] font-medium uppercase tracking-wider text-muted-foreground">
|
|
286
|
+
Harness
|
|
287
|
+
</label>
|
|
288
|
+
<select
|
|
289
|
+
value={activeHarness}
|
|
290
|
+
onChange={(e) => handleHarnessChange(e.target.value as HarnessOption)}
|
|
291
|
+
className={cn(
|
|
292
|
+
"w-full rounded-md border border-border bg-background px-2 py-1.5 text-xs text-foreground",
|
|
293
|
+
"focus:outline-none focus:ring-2 focus:ring-ring",
|
|
294
|
+
)}
|
|
295
|
+
aria-label="Select harness"
|
|
296
|
+
>
|
|
297
|
+
{resolvedHarnesses.map((h) => (
|
|
298
|
+
<option key={h} value={h}>
|
|
299
|
+
{HARNESS_META[h].label}
|
|
300
|
+
</option>
|
|
301
|
+
))}
|
|
302
|
+
</select>
|
|
303
|
+
</div>
|
|
304
|
+
)}
|
|
305
|
+
|
|
217
306
|
{/* Search input */}
|
|
218
|
-
<div className="border-b border-border px-
|
|
307
|
+
<div className="border-b border-border px-3 py-1.5">
|
|
219
308
|
<input
|
|
220
309
|
ref={searchRef}
|
|
221
310
|
role="searchbox"
|
|
@@ -236,59 +325,47 @@ export function ModelSelector({
|
|
|
236
325
|
ref={listRef}
|
|
237
326
|
role="listbox"
|
|
238
327
|
aria-label="Available models"
|
|
239
|
-
className="max-h-
|
|
328
|
+
className="max-h-72 overflow-y-auto p-1"
|
|
240
329
|
>
|
|
241
330
|
{visibleModels.length === 0 && (
|
|
242
331
|
<div className="px-2 py-3 text-center text-xs text-muted-foreground">
|
|
243
332
|
No models found
|
|
244
333
|
</div>
|
|
245
334
|
)}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
335
|
+
|
|
336
|
+
{/* Grouped rendering */}
|
|
337
|
+
{groupedModels ? (
|
|
338
|
+
Array.from(groupedModels.entries()).map(([group, groupModels]) => (
|
|
339
|
+
<div key={group}>
|
|
340
|
+
<div className="px-2 pb-0.5 pt-2 text-[0.6rem] font-medium uppercase tracking-wider text-muted-foreground">
|
|
341
|
+
{group}
|
|
342
|
+
</div>
|
|
343
|
+
{groupModels.map((model) => (
|
|
344
|
+
<ModelRow
|
|
345
|
+
key={model.modelId}
|
|
346
|
+
model={model}
|
|
347
|
+
isSelected={model.modelId === selectedModel.modelId}
|
|
348
|
+
showDescription={false}
|
|
349
|
+
showSpeedBadge={showSpeedBadge}
|
|
350
|
+
onClick={() => selectModel(model)}
|
|
351
|
+
/>
|
|
352
|
+
))}
|
|
353
|
+
</div>
|
|
354
|
+
))
|
|
355
|
+
) : (
|
|
356
|
+
visibleModels.map((model, idx) => (
|
|
357
|
+
<ModelRow
|
|
358
|
+
key={model.modelId}
|
|
359
|
+
model={model}
|
|
360
|
+
isSelected={model.modelId === selectedModel.modelId}
|
|
361
|
+
isHighlighted={idx === highlightIdx}
|
|
362
|
+
showDescription={showDescriptions && !compact && !isSearching && !showAll}
|
|
363
|
+
showSpeedBadge={showSpeedBadge}
|
|
267
364
|
onClick={() => selectModel(model)}
|
|
268
365
|
onMouseEnter={() => setHighlightIdx(idx)}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
{/* Engine tag (unified mode only) */}
|
|
274
|
-
{isUnified && (
|
|
275
|
-
<span className="shrink-0 rounded bg-muted px-1 py-0.5 text-[0.55rem] font-medium text-muted-foreground">
|
|
276
|
-
{HARNESS_LABELS[model.harness]}
|
|
277
|
-
</span>
|
|
278
|
-
)}
|
|
279
|
-
|
|
280
|
-
{/* Cost tier */}
|
|
281
|
-
<span className="shrink-0 text-[0.6rem] text-muted-foreground">
|
|
282
|
-
{COST_TIER_LABEL[model.costTier]}
|
|
283
|
-
</span>
|
|
284
|
-
|
|
285
|
-
{/* Selected checkmark */}
|
|
286
|
-
{isSelected && (
|
|
287
|
-
<CheckIcon className="shrink-0 text-primary" />
|
|
288
|
-
)}
|
|
289
|
-
</button>
|
|
290
|
-
);
|
|
291
|
-
})}
|
|
366
|
+
/>
|
|
367
|
+
))
|
|
368
|
+
)}
|
|
292
369
|
|
|
293
370
|
{/* Show All Models */}
|
|
294
371
|
{showShowAllButton && (
|
|
@@ -301,7 +378,7 @@ export function ModelSelector({
|
|
|
301
378
|
)}
|
|
302
379
|
onClick={() => setShowAll(true)}
|
|
303
380
|
>
|
|
304
|
-
Show
|
|
381
|
+
Show all models
|
|
305
382
|
</button>
|
|
306
383
|
)}
|
|
307
384
|
</div>
|
|
@@ -312,6 +389,66 @@ export function ModelSelector({
|
|
|
312
389
|
);
|
|
313
390
|
}
|
|
314
391
|
|
|
392
|
+
interface ModelRowProps {
|
|
393
|
+
model: ModelInfo;
|
|
394
|
+
isSelected: boolean;
|
|
395
|
+
isHighlighted?: boolean;
|
|
396
|
+
showDescription: boolean;
|
|
397
|
+
showSpeedBadge: boolean;
|
|
398
|
+
onClick: () => void;
|
|
399
|
+
onMouseEnter?: () => void;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function ModelRow({
|
|
403
|
+
model,
|
|
404
|
+
isSelected,
|
|
405
|
+
isHighlighted,
|
|
406
|
+
showDescription,
|
|
407
|
+
showSpeedBadge,
|
|
408
|
+
onClick,
|
|
409
|
+
onMouseEnter,
|
|
410
|
+
}: ModelRowProps) {
|
|
411
|
+
return (
|
|
412
|
+
<button
|
|
413
|
+
data-model-option=""
|
|
414
|
+
role="option"
|
|
415
|
+
aria-selected={isSelected}
|
|
416
|
+
type="button"
|
|
417
|
+
className={cn(
|
|
418
|
+
"flex w-full cursor-pointer flex-col rounded-md px-2 py-1.5 text-xs outline-none",
|
|
419
|
+
"transition-colors",
|
|
420
|
+
isHighlighted && "bg-accent text-accent-foreground",
|
|
421
|
+
!isHighlighted && "hover:bg-accent-hover",
|
|
422
|
+
isSelected && "font-medium",
|
|
423
|
+
)}
|
|
424
|
+
onClick={onClick}
|
|
425
|
+
onMouseEnter={onMouseEnter}
|
|
426
|
+
>
|
|
427
|
+
<div className="flex w-full items-center gap-2">
|
|
428
|
+
<span className="flex-1 truncate text-left">{model.displayName}</span>
|
|
429
|
+
|
|
430
|
+
{showSpeedBadge && (
|
|
431
|
+
<span className="shrink-0 text-[0.6rem] text-muted-foreground">
|
|
432
|
+
{SPEED_TIER_LABEL[model.speedTier]}
|
|
433
|
+
</span>
|
|
434
|
+
)}
|
|
435
|
+
|
|
436
|
+
<span className="shrink-0 text-[0.6rem] text-muted-foreground">
|
|
437
|
+
{COST_TIER_LABEL[model.costTier]}
|
|
438
|
+
</span>
|
|
439
|
+
|
|
440
|
+
{isSelected && <CheckIcon className="shrink-0 text-primary" />}
|
|
441
|
+
</div>
|
|
442
|
+
|
|
443
|
+
{showDescription && model.shortDescription && (
|
|
444
|
+
<span className="mt-0.5 block text-left text-[0.6rem] text-muted-foreground">
|
|
445
|
+
{model.shortDescription}
|
|
446
|
+
</span>
|
|
447
|
+
)}
|
|
448
|
+
</button>
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
|
|
315
452
|
function ChevronIcon() {
|
|
316
453
|
return (
|
|
317
454
|
<svg
|
|
@@ -137,11 +137,13 @@ describe("useModelRegistry", () => {
|
|
|
137
137
|
expect(cursorModels).toHaveLength(0);
|
|
138
138
|
});
|
|
139
139
|
|
|
140
|
-
it("resolves defaultModel to
|
|
140
|
+
it("resolves defaultModel to the first featured native model", () => {
|
|
141
141
|
const { result } = renderHook(() =>
|
|
142
142
|
useModelRegistry({ harness: "native" }),
|
|
143
143
|
);
|
|
144
|
-
|
|
144
|
+
const featured = result.current.featured;
|
|
145
|
+
expect(featured.length).toBeGreaterThan(0);
|
|
146
|
+
expect(result.current.defaultModel.modelId).toBe(featured[0].modelId);
|
|
145
147
|
});
|
|
146
148
|
});
|
|
147
149
|
|
package/src/models/harness.ts
CHANGED
|
@@ -5,15 +5,61 @@ import { Harness } from "@stigmer/protos/ai/stigmer/agentic/session/v1/enum_pb";
|
|
|
5
5
|
*
|
|
6
6
|
* Used on component props and hook options so platform builders
|
|
7
7
|
* do not need to import proto enums to use the SDK.
|
|
8
|
+
*
|
|
9
|
+
* This union grows as new execution engines are integrated.
|
|
10
|
+
* Each value must have a corresponding entry in {@link HARNESS_META}.
|
|
8
11
|
*/
|
|
9
|
-
export type HarnessOption =
|
|
12
|
+
export type HarnessOption =
|
|
13
|
+
| "native"
|
|
14
|
+
| "cursor"
|
|
15
|
+
| "copilot"
|
|
16
|
+
| "claude_code"
|
|
17
|
+
| "codex"
|
|
18
|
+
| "devin";
|
|
19
|
+
|
|
20
|
+
/** Display metadata for a single harness. */
|
|
21
|
+
export interface HarnessDisplayInfo {
|
|
22
|
+
/** User-facing label shown in the harness dropdown. */
|
|
23
|
+
readonly label: string;
|
|
24
|
+
/** One-line description shown as a tooltip or subtitle. */
|
|
25
|
+
readonly description: string;
|
|
26
|
+
}
|
|
10
27
|
|
|
11
|
-
/**
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
28
|
+
/**
|
|
29
|
+
* Display metadata for all registered harnesses.
|
|
30
|
+
*
|
|
31
|
+
* Drives the harness dropdown in {@link ModelSelector} and provides
|
|
32
|
+
* labels for the compact trigger button.
|
|
33
|
+
*/
|
|
34
|
+
export const HARNESS_META: Readonly<Record<HarnessOption, HarnessDisplayInfo>> = {
|
|
35
|
+
native: { label: "Stigmer", description: "Stigmer's native agent runtime" },
|
|
36
|
+
cursor: { label: "Cursor", description: "Cursor IDE agent with codebase indexing" },
|
|
37
|
+
copilot: { label: "GitHub Copilot", description: "GitHub-native sub-agent orchestration" },
|
|
38
|
+
claude_code: { label: "Claude Agent SDK", description: "Anthropic's agent SDK with built-in tools" },
|
|
39
|
+
codex: { label: "OpenAI Codex", description: "Thread-based execution with structured output" },
|
|
40
|
+
devin: { label: "Devin", description: "Full autonomous engineer, session-based" },
|
|
15
41
|
};
|
|
16
42
|
|
|
43
|
+
/**
|
|
44
|
+
* User-facing labels for each harness option.
|
|
45
|
+
*
|
|
46
|
+
* @deprecated Use {@link HARNESS_META} instead for full display metadata.
|
|
47
|
+
* Kept for backward compatibility with existing consumers.
|
|
48
|
+
*/
|
|
49
|
+
export const HARNESS_LABELS: Readonly<Record<HarnessOption, string>> = Object.fromEntries(
|
|
50
|
+
Object.entries(HARNESS_META).map(([k, v]) => [k, v.label]),
|
|
51
|
+
) as Record<HarnessOption, string>;
|
|
52
|
+
|
|
53
|
+
/** Ordered list of all registered harness IDs. */
|
|
54
|
+
export const HARNESS_OPTIONS: readonly HarnessOption[] = [
|
|
55
|
+
"native",
|
|
56
|
+
"cursor",
|
|
57
|
+
"copilot",
|
|
58
|
+
"claude_code",
|
|
59
|
+
"codex",
|
|
60
|
+
"devin",
|
|
61
|
+
];
|
|
62
|
+
|
|
17
63
|
/** Platform default — resolves to the native engine. */
|
|
18
64
|
export const DEFAULT_HARNESS: HarnessOption = "native";
|
|
19
65
|
|
package/src/models/index.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
export { MODEL_REGISTRY, DEFAULT_MODEL_ID, DEFAULT_CURSOR_MODEL_ID, DISABLED_PROVIDERS, modelKey, parseModelKey } from "./registry";
|
|
2
|
-
export type { ParsedModelKey } from "./registry";
|
|
3
|
-
export type { ModelInfo, Provider, CostTier } from "./registry";
|
|
1
|
+
export { MODEL_REGISTRY, DEFAULT_MODEL_ID, DEFAULT_CURSOR_MODEL_ID, DISABLED_PROVIDERS, modelKey, parseModelKey, resolveDefaultModelId } from "./registry";
|
|
2
|
+
export type { ParsedModelKey, DefaultModelResolution, DefaultModelSource } from "./registry";
|
|
3
|
+
export type { ModelInfo, Provider, CostTier, SpeedTier } from "./registry";
|
|
4
4
|
export { useModelRegistry } from "./useModelRegistry";
|
|
5
5
|
export type { UseModelRegistryReturn, UseModelRegistryOptions } from "./useModelRegistry";
|
|
6
6
|
export { ModelSelector } from "./ModelSelector";
|
|
7
7
|
export type { ModelSelectorProps } from "./ModelSelector";
|
|
8
8
|
export { HarnessSelector } from "./HarnessSelector";
|
|
9
9
|
export type { HarnessSelectorProps } from "./HarnessSelector";
|
|
10
|
-
export { DEFAULT_HARNESS, HARNESS_LABELS, toProtoHarness, fromProtoHarness } from "./harness";
|
|
11
|
-
export type { HarnessOption } from "./harness";
|
|
10
|
+
export { DEFAULT_HARNESS, HARNESS_LABELS, HARNESS_META, HARNESS_OPTIONS, toProtoHarness, fromProtoHarness } from "./harness";
|
|
11
|
+
export type { HarnessOption, HarnessDisplayInfo } from "./harness";
|