@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.
Files changed (37) hide show
  1. package/mcp-server/McpServerDetailView.d.ts.map +1 -1
  2. package/mcp-server/McpServerDetailView.js +3 -3
  3. package/mcp-server/McpServerDetailView.js.map +1 -1
  4. package/mcp-server/useMcpServerOAuthConnect.d.ts.map +1 -1
  5. package/mcp-server/useMcpServerOAuthConnect.js +37 -9
  6. package/mcp-server/useMcpServerOAuthConnect.js.map +1 -1
  7. package/models/ModelSelector.d.ts +34 -18
  8. package/models/ModelSelector.d.ts.map +1 -1
  9. package/models/ModelSelector.js +76 -47
  10. package/models/ModelSelector.js.map +1 -1
  11. package/models/__tests__/useModelRegistry.test.js +4 -2
  12. package/models/__tests__/useModelRegistry.test.js.map +1 -1
  13. package/models/harness.d.ts +26 -2
  14. package/models/harness.d.ts.map +1 -1
  15. package/models/harness.js +29 -4
  16. package/models/harness.js.map +1 -1
  17. package/models/index.d.ts +5 -5
  18. package/models/index.d.ts.map +1 -1
  19. package/models/index.js +2 -2
  20. package/models/index.js.map +1 -1
  21. package/models/registry.d.ts +49 -2
  22. package/models/registry.d.ts.map +1 -1
  23. package/models/registry.js +45 -3
  24. package/models/registry.js.map +1 -1
  25. package/models/useModelRegistry.d.ts.map +1 -1
  26. package/models/useModelRegistry.js +7 -12
  27. package/models/useModelRegistry.js.map +1 -1
  28. package/package.json +4 -4
  29. package/src/mcp-server/McpServerDetailView.tsx +15 -0
  30. package/src/mcp-server/useMcpServerOAuthConnect.ts +37 -9
  31. package/src/models/ModelSelector.tsx +234 -97
  32. package/src/models/__tests__/useModelRegistry.test.tsx +4 -2
  33. package/src/models/harness.ts +51 -5
  34. package/src/models/index.ts +5 -5
  35. package/src/models/registry.ts +96 -3
  36. package/src/models/useModelRegistry.ts +6 -8
  37. 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 { modelKey, parseModelKey, type ModelInfo, type CostTier } from "./registry";
8
- import { HARNESS_LABELS, type HarnessOption } from "./harness";
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 compound key (`"native/claude-sonnet-4.6"`) or plain `modelId`. */
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, restricts the catalog to a single harness (backward compat).
24
- * When omitted, shows the unified picker with models from both engines.
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
- * Fires when the selected model belongs to a different harness than
29
- * the previous selection. Only relevant in unified mode (no `harness` prop).
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 onHarnessResolved?: (harness: HarnessOption) => void;
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
- * Cursor-style model picker: a flat searchable list inside a popover.
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
- * Each model row shows the display name, an engine tag
45
- * ("Stigmer" / "Cursor"), and a cost-tier indicator.
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
- * In unified mode (no `harness` prop), selecting a model implicitly
48
- * resolves the harness via {@link ModelSelectorProps.onHarnessResolved}.
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={selectedModelId}
54
- * onValueChange={setSelectedModelId}
55
- * onHarnessResolved={setHarness}
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 isUnified = harness === undefined;
68
- const { models, featured, defaultModel, getModel, getByKey } = useModelRegistry(
69
- isUnified ? undefined : { harness },
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 resolveSelected = useCallback((): ModelInfo | undefined => {
81
- if (!value) return undefined;
82
- if (isUnified) {
83
- const byKey = getByKey(value);
84
- if (byKey) return byKey;
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 = resolveSelected() ?? defaultModel;
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
- || HARNESS_LABELS[m.harness].toLowerCase().includes(lowerQuery),
143
+ || m.shortDescription.toLowerCase().includes(lowerQuery),
100
144
  );
101
145
  }
102
146
  if (showAll) return models;
103
- return featured.length > 0 ? featured : models;
104
- }, [models, featured, isSearching, showAll, lowerQuery]);
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
- const key = isUnified ? modelKey(model.harness, model.modelId) : model.modelId;
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
- [isUnified, onValueChange, onHarnessResolved, selectedModel],
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 && featured.length > 0 && featured.length < models.length;
244
+ const showShowAllButton = !isSearching && !showAll && featuredModels.length > 0 && featuredModels.length < models.length;
179
245
 
180
246
  const triggerLabel = selectedModel.displayName;
181
- const triggerHarness = isUnified ? HARNESS_LABELS[selectedModel.harness] : undefined;
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-[18rem] max-sm:max-w-[10rem]",
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 rounded bg-muted px-1 py-0.5 text-[0.6rem] font-medium text-muted-foreground">
200
- {triggerHarness}
201
- </span>
263
+ <span className="shrink-0 text-muted-foreground">{triggerHarness}</span>
202
264
  )}
203
- <span className="shrink-0 text-[0.6rem] text-muted-foreground">{triggerCost}</span>
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-72 rounded-lg border border-border bg-popover shadow-md",
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-2 py-1.5">
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-64 overflow-y-auto p-1"
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
- {visibleModels.map((model, idx) => {
247
- const key = modelKey(model.harness, model.modelId);
248
- const isSelected = selectedModel
249
- ? model.harness === selectedModel.harness && model.modelId === selectedModel.modelId
250
- : false;
251
- const isHighlighted = idx === highlightIdx;
252
-
253
- return (
254
- <button
255
- key={key}
256
- data-model-option=""
257
- role="option"
258
- aria-selected={isSelected}
259
- type="button"
260
- className={cn(
261
- "flex w-full cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-xs outline-none",
262
- "transition-colors",
263
- isHighlighted && "bg-accent text-accent-foreground",
264
- !isHighlighted && "hover:bg-accent-hover",
265
- isSelected && "font-medium",
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
- {/* Model name */}
271
- <span className="flex-1 truncate text-left">{model.displayName}</span>
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 All Models
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 DEFAULT_MODEL_ID", () => {
140
+ it("resolves defaultModel to the first featured native model", () => {
141
141
  const { result } = renderHook(() =>
142
142
  useModelRegistry({ harness: "native" }),
143
143
  );
144
- expect(result.current.defaultModel.modelId).toBe(DEFAULT_MODEL_ID);
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
 
@@ -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 = "native" | "cursor";
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
- /** User-facing labels for each harness option. */
12
- export const HARNESS_LABELS: Readonly<Record<HarnessOption, string>> = {
13
- native: "Stigmer",
14
- cursor: "Cursor",
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
 
@@ -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";