@stigmer/react 0.4.1 → 0.4.3
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 -2
- package/composer/ComposerToolbar.js.map +1 -1
- package/github/useGitHubConnection.d.ts.map +1 -1
- package/github/useGitHubConnection.js +2 -4
- package/github/useGitHubConnection.js.map +1 -1
- package/models/ModelSelector.d.ts +34 -18
- package/models/ModelSelector.d.ts.map +1 -1
- package/models/ModelSelector.js +96 -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/composer/ComposerToolbar.tsx +2 -3
- package/src/github/useGitHubConnection.ts +2 -4
- package/src/models/ModelSelector.tsx +274 -96
- 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/src/workspace/WorkspaceEditor.tsx +37 -11
- package/styles.css +1 -1
- package/workspace/WorkspaceEditor.js +18 -4
- package/workspace/WorkspaceEditor.js.map +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,16 +84,27 @@ 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);
|
|
107
|
+
const [harnessOpen, setHarnessOpen] = useState(false);
|
|
73
108
|
const [searchQuery, setSearchQuery] = useState("");
|
|
74
109
|
const [showAll, setShowAll] = useState(false);
|
|
75
110
|
const [highlightIdx, setHighlightIdx] = useState(-1);
|
|
@@ -77,31 +112,56 @@ export function ModelSelector({
|
|
|
77
112
|
const searchRef = useRef<HTMLInputElement>(null);
|
|
78
113
|
const listRef = useRef<HTMLDivElement>(null);
|
|
79
114
|
|
|
80
|
-
const
|
|
81
|
-
if (
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
return getModel(value);
|
|
87
|
-
}, [value, isUnified, getByKey, getModel]);
|
|
115
|
+
const resolvedHarnesses = useMemo(() => {
|
|
116
|
+
if (availableHarnesses) return availableHarnesses;
|
|
117
|
+
// For now, show native and cursor (the two harnesses with models in the registry).
|
|
118
|
+
// Future harnesses will be added to the registry and appear here automatically.
|
|
119
|
+
return HARNESS_OPTIONS.filter((h) => h === "native" || h === "cursor");
|
|
120
|
+
}, [availableHarnesses]);
|
|
88
121
|
|
|
89
|
-
const selectedModel =
|
|
122
|
+
const selectedModel = (value ? getModel(value) : undefined) ?? defaultModel;
|
|
90
123
|
|
|
91
124
|
const isSearching = searchQuery.length > 0;
|
|
92
125
|
const lowerQuery = searchQuery.toLowerCase();
|
|
93
126
|
|
|
127
|
+
const curatedSet = useMemo(() => {
|
|
128
|
+
if (curatedModels) return new Set(curatedModels);
|
|
129
|
+
return null;
|
|
130
|
+
}, [curatedModels]);
|
|
131
|
+
|
|
132
|
+
const featuredModels = useMemo(() => {
|
|
133
|
+
if (curatedSet) {
|
|
134
|
+
return models.filter((m) => curatedSet.has(m.modelId));
|
|
135
|
+
}
|
|
136
|
+
return featured;
|
|
137
|
+
}, [models, featured, curatedSet]);
|
|
138
|
+
|
|
94
139
|
const visibleModels: readonly ModelInfo[] = useMemo(() => {
|
|
95
140
|
if (isSearching) {
|
|
96
141
|
return models.filter((m) =>
|
|
97
142
|
m.displayName.toLowerCase().includes(lowerQuery)
|
|
98
143
|
|| m.modelId.toLowerCase().includes(lowerQuery)
|
|
99
|
-
||
|
|
144
|
+
|| m.shortDescription.toLowerCase().includes(lowerQuery),
|
|
100
145
|
);
|
|
101
146
|
}
|
|
102
147
|
if (showAll) return models;
|
|
103
|
-
return
|
|
104
|
-
}, [models,
|
|
148
|
+
return featuredModels.length > 0 ? featuredModels : models;
|
|
149
|
+
}, [models, featuredModels, isSearching, showAll, lowerQuery]);
|
|
150
|
+
|
|
151
|
+
const groupedModels = useMemo(() => {
|
|
152
|
+
if (!showAll || groupBy === "none" || isSearching) return null;
|
|
153
|
+
const groups = new Map<string, ModelInfo[]>();
|
|
154
|
+
for (const model of models) {
|
|
155
|
+
const key = groupBy === "provider" ? model.provider : model.costTier;
|
|
156
|
+
const group = groups.get(key);
|
|
157
|
+
if (group) {
|
|
158
|
+
group.push(model);
|
|
159
|
+
} else {
|
|
160
|
+
groups.set(key, [model]);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return groups;
|
|
164
|
+
}, [models, showAll, groupBy, isSearching]);
|
|
105
165
|
|
|
106
166
|
useEffect(() => {
|
|
107
167
|
setHighlightIdx(-1);
|
|
@@ -112,6 +172,7 @@ export function ModelSelector({
|
|
|
112
172
|
setSearchQuery("");
|
|
113
173
|
setShowAll(false);
|
|
114
174
|
setHighlightIdx(-1);
|
|
175
|
+
setHarnessOpen(false);
|
|
115
176
|
}
|
|
116
177
|
}, [open]);
|
|
117
178
|
|
|
@@ -121,16 +182,23 @@ export function ModelSelector({
|
|
|
121
182
|
}
|
|
122
183
|
}, [open]);
|
|
123
184
|
|
|
185
|
+
const handleHarnessChange = useCallback(
|
|
186
|
+
(newHarness: HarnessOption) => {
|
|
187
|
+
setInternalHarness(newHarness);
|
|
188
|
+
onHarnessChange?.(newHarness);
|
|
189
|
+
onHarnessResolved?.(newHarness);
|
|
190
|
+
setShowAll(false);
|
|
191
|
+
setSearchQuery("");
|
|
192
|
+
},
|
|
193
|
+
[onHarnessChange, onHarnessResolved],
|
|
194
|
+
);
|
|
195
|
+
|
|
124
196
|
const selectModel = useCallback(
|
|
125
197
|
(model: ModelInfo) => {
|
|
126
|
-
|
|
127
|
-
onValueChange(key);
|
|
128
|
-
if (isUnified && onHarnessResolved && model.harness !== selectedModel?.harness) {
|
|
129
|
-
onHarnessResolved(model.harness);
|
|
130
|
-
}
|
|
198
|
+
onValueChange(model.modelId);
|
|
131
199
|
setOpen(false);
|
|
132
200
|
},
|
|
133
|
-
[
|
|
201
|
+
[onValueChange],
|
|
134
202
|
);
|
|
135
203
|
|
|
136
204
|
const scrollHighlightIntoView = useCallback((idx: number) => {
|
|
@@ -175,11 +243,10 @@ export function ModelSelector({
|
|
|
175
243
|
[visibleModels, highlightIdx, selectModel, scrollHighlightIntoView],
|
|
176
244
|
);
|
|
177
245
|
|
|
178
|
-
const showShowAllButton = !isSearching && !showAll &&
|
|
246
|
+
const showShowAllButton = !isSearching && !showAll && featuredModels.length > 0 && featuredModels.length < models.length;
|
|
179
247
|
|
|
180
248
|
const triggerLabel = selectedModel.displayName;
|
|
181
|
-
const triggerHarness =
|
|
182
|
-
const triggerCost = COST_TIER_LABEL[selectedModel.costTier];
|
|
249
|
+
const triggerHarness = !isHarnessLocked ? HARNESS_META[activeHarness].label : undefined;
|
|
183
250
|
|
|
184
251
|
return (
|
|
185
252
|
<Popover.Root open={open} onOpenChange={setOpen}>
|
|
@@ -190,17 +257,17 @@ export function ModelSelector({
|
|
|
190
257
|
"bg-background px-2.5 py-1.5 text-xs text-foreground",
|
|
191
258
|
"hover:bg-accent-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
192
259
|
"disabled:pointer-events-none disabled:opacity-50",
|
|
193
|
-
"transition-colors max-w-[
|
|
260
|
+
"transition-colors max-w-[20rem] max-sm:max-w-[12rem]",
|
|
194
261
|
className,
|
|
195
262
|
)}
|
|
196
263
|
>
|
|
197
|
-
<span className="truncate">{triggerLabel}</span>
|
|
198
264
|
{triggerHarness && (
|
|
199
|
-
<span className="shrink-0
|
|
200
|
-
|
|
201
|
-
|
|
265
|
+
<span className="shrink-0 text-muted-foreground">{triggerHarness}</span>
|
|
266
|
+
)}
|
|
267
|
+
{triggerHarness && (
|
|
268
|
+
<span className="shrink-0 text-border" aria-hidden>·</span>
|
|
202
269
|
)}
|
|
203
|
-
<span className="
|
|
270
|
+
<span className="truncate">{triggerLabel}</span>
|
|
204
271
|
<ChevronIcon />
|
|
205
272
|
</Popover.Trigger>
|
|
206
273
|
|
|
@@ -214,8 +281,76 @@ export function ModelSelector({
|
|
|
214
281
|
"text-popover-foreground",
|
|
215
282
|
)}
|
|
216
283
|
>
|
|
284
|
+
{/* Harness selector — inline label + compact dropdown; disabled when locked */}
|
|
285
|
+
<div className="relative flex items-center justify-between border-b border-border px-3 py-2">
|
|
286
|
+
<span className="text-xs text-muted-foreground">Harness</span>
|
|
287
|
+
<button
|
|
288
|
+
type="button"
|
|
289
|
+
aria-haspopup="listbox"
|
|
290
|
+
aria-expanded={harnessOpen}
|
|
291
|
+
aria-label="Select harness"
|
|
292
|
+
disabled={isHarnessLocked}
|
|
293
|
+
className={cn(
|
|
294
|
+
"inline-flex items-center gap-1.5 rounded-md border border-border",
|
|
295
|
+
"bg-background px-2.5 py-1.5 text-xs text-foreground",
|
|
296
|
+
"transition-colors",
|
|
297
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
298
|
+
isHarnessLocked
|
|
299
|
+
? "cursor-not-allowed opacity-50"
|
|
300
|
+
: "hover:bg-accent-hover",
|
|
301
|
+
)}
|
|
302
|
+
onClick={() => {
|
|
303
|
+
if (!isHarnessLocked) setHarnessOpen(!harnessOpen);
|
|
304
|
+
}}
|
|
305
|
+
onKeyDown={(e) => {
|
|
306
|
+
if (e.key === "Escape" && harnessOpen) {
|
|
307
|
+
e.stopPropagation();
|
|
308
|
+
setHarnessOpen(false);
|
|
309
|
+
}
|
|
310
|
+
}}
|
|
311
|
+
>
|
|
312
|
+
<span>{HARNESS_META[activeHarness].label}</span>
|
|
313
|
+
{!isHarnessLocked && <ChevronIcon />}
|
|
314
|
+
</button>
|
|
315
|
+
|
|
316
|
+
{!isHarnessLocked && harnessOpen && (
|
|
317
|
+
<div
|
|
318
|
+
role="listbox"
|
|
319
|
+
aria-label="Available harnesses"
|
|
320
|
+
className={cn(
|
|
321
|
+
"absolute right-3 top-full z-10 mt-1 overflow-hidden rounded-md border border-border",
|
|
322
|
+
"bg-popover shadow-md",
|
|
323
|
+
)}
|
|
324
|
+
>
|
|
325
|
+
{resolvedHarnesses.map((h) => {
|
|
326
|
+
const isActive = h === activeHarness;
|
|
327
|
+
return (
|
|
328
|
+
<button
|
|
329
|
+
key={h}
|
|
330
|
+
type="button"
|
|
331
|
+
role="option"
|
|
332
|
+
aria-selected={isActive}
|
|
333
|
+
className={cn(
|
|
334
|
+
"flex w-full items-center gap-2 px-2.5 py-1.5 text-xs transition-colors",
|
|
335
|
+
"hover:bg-accent-hover",
|
|
336
|
+
isActive && "font-medium",
|
|
337
|
+
)}
|
|
338
|
+
onClick={() => {
|
|
339
|
+
handleHarnessChange(h);
|
|
340
|
+
setHarnessOpen(false);
|
|
341
|
+
}}
|
|
342
|
+
>
|
|
343
|
+
<span className="flex-1 text-left">{HARNESS_META[h].label}</span>
|
|
344
|
+
{isActive && <CheckIcon className="shrink-0 text-primary" />}
|
|
345
|
+
</button>
|
|
346
|
+
);
|
|
347
|
+
})}
|
|
348
|
+
</div>
|
|
349
|
+
)}
|
|
350
|
+
</div>
|
|
351
|
+
|
|
217
352
|
{/* Search input */}
|
|
218
|
-
<div className="border-b border-border px-
|
|
353
|
+
<div className="border-b border-border px-3 py-1.5">
|
|
219
354
|
<input
|
|
220
355
|
ref={searchRef}
|
|
221
356
|
role="searchbox"
|
|
@@ -236,59 +371,47 @@ export function ModelSelector({
|
|
|
236
371
|
ref={listRef}
|
|
237
372
|
role="listbox"
|
|
238
373
|
aria-label="Available models"
|
|
239
|
-
className="max-h-
|
|
374
|
+
className="max-h-72 overflow-y-auto p-1"
|
|
240
375
|
>
|
|
241
376
|
{visibleModels.length === 0 && (
|
|
242
377
|
<div className="px-2 py-3 text-center text-xs text-muted-foreground">
|
|
243
378
|
No models found
|
|
244
379
|
</div>
|
|
245
380
|
)}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
381
|
+
|
|
382
|
+
{/* Grouped rendering */}
|
|
383
|
+
{groupedModels ? (
|
|
384
|
+
Array.from(groupedModels.entries()).map(([group, groupModels]) => (
|
|
385
|
+
<div key={group}>
|
|
386
|
+
<div className="px-2 pb-0.5 pt-2 text-[0.6rem] font-medium uppercase tracking-wider text-muted-foreground">
|
|
387
|
+
{group}
|
|
388
|
+
</div>
|
|
389
|
+
{groupModels.map((model) => (
|
|
390
|
+
<ModelRow
|
|
391
|
+
key={model.modelId}
|
|
392
|
+
model={model}
|
|
393
|
+
isSelected={model.modelId === selectedModel.modelId}
|
|
394
|
+
showDescription={false}
|
|
395
|
+
showSpeedBadge={showSpeedBadge}
|
|
396
|
+
onClick={() => selectModel(model)}
|
|
397
|
+
/>
|
|
398
|
+
))}
|
|
399
|
+
</div>
|
|
400
|
+
))
|
|
401
|
+
) : (
|
|
402
|
+
visibleModels.map((model, idx) => (
|
|
403
|
+
<ModelRow
|
|
404
|
+
key={model.modelId}
|
|
405
|
+
model={model}
|
|
406
|
+
isSelected={model.modelId === selectedModel.modelId}
|
|
407
|
+
isHighlighted={idx === highlightIdx}
|
|
408
|
+
showDescription={showDescriptions && !compact && !isSearching && !showAll}
|
|
409
|
+
showSpeedBadge={showSpeedBadge}
|
|
267
410
|
onClick={() => selectModel(model)}
|
|
268
411
|
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
|
-
})}
|
|
412
|
+
/>
|
|
413
|
+
))
|
|
414
|
+
)}
|
|
292
415
|
|
|
293
416
|
{/* Show All Models */}
|
|
294
417
|
{showShowAllButton && (
|
|
@@ -301,7 +424,7 @@ export function ModelSelector({
|
|
|
301
424
|
)}
|
|
302
425
|
onClick={() => setShowAll(true)}
|
|
303
426
|
>
|
|
304
|
-
Show
|
|
427
|
+
Show all models
|
|
305
428
|
</button>
|
|
306
429
|
)}
|
|
307
430
|
</div>
|
|
@@ -312,6 +435,61 @@ export function ModelSelector({
|
|
|
312
435
|
);
|
|
313
436
|
}
|
|
314
437
|
|
|
438
|
+
interface ModelRowProps {
|
|
439
|
+
model: ModelInfo;
|
|
440
|
+
isSelected: boolean;
|
|
441
|
+
isHighlighted?: boolean;
|
|
442
|
+
showDescription: boolean;
|
|
443
|
+
showSpeedBadge: boolean;
|
|
444
|
+
onClick: () => void;
|
|
445
|
+
onMouseEnter?: () => void;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function ModelRow({
|
|
449
|
+
model,
|
|
450
|
+
isSelected,
|
|
451
|
+
isHighlighted,
|
|
452
|
+
showDescription,
|
|
453
|
+
showSpeedBadge,
|
|
454
|
+
onClick,
|
|
455
|
+
onMouseEnter,
|
|
456
|
+
}: ModelRowProps) {
|
|
457
|
+
return (
|
|
458
|
+
<button
|
|
459
|
+
data-model-option=""
|
|
460
|
+
role="option"
|
|
461
|
+
aria-selected={isSelected}
|
|
462
|
+
type="button"
|
|
463
|
+
className={cn(
|
|
464
|
+
"flex w-full cursor-pointer flex-col rounded-md px-2 py-1.5 text-xs outline-none",
|
|
465
|
+
"transition-colors",
|
|
466
|
+
isHighlighted && "bg-accent text-accent-foreground",
|
|
467
|
+
!isHighlighted && "hover:bg-accent-hover",
|
|
468
|
+
)}
|
|
469
|
+
onClick={onClick}
|
|
470
|
+
onMouseEnter={onMouseEnter}
|
|
471
|
+
>
|
|
472
|
+
<div className="flex w-full items-center gap-2">
|
|
473
|
+
<span className="flex-1 truncate text-left font-medium">{model.displayName}</span>
|
|
474
|
+
|
|
475
|
+
<span className="shrink-0 text-[0.6rem] text-muted-foreground">
|
|
476
|
+
{showSpeedBadge
|
|
477
|
+
? `${SPEED_TIER_LABEL[model.speedTier]} ${COST_TIER_LABEL[model.costTier]}`
|
|
478
|
+
: COST_TIER_LABEL[model.costTier]}
|
|
479
|
+
</span>
|
|
480
|
+
|
|
481
|
+
{isSelected && <CheckIcon className="shrink-0 text-primary" />}
|
|
482
|
+
</div>
|
|
483
|
+
|
|
484
|
+
{showDescription && model.shortDescription && (
|
|
485
|
+
<span className="mt-0.5 block text-left text-[0.65rem] text-muted-foreground">
|
|
486
|
+
{model.shortDescription}
|
|
487
|
+
</span>
|
|
488
|
+
)}
|
|
489
|
+
</button>
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
|
|
315
493
|
function ChevronIcon() {
|
|
316
494
|
return (
|
|
317
495
|
<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";
|