@stigmer/react 0.4.1 → 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.
@@ -43,7 +43,8 @@ export function parseModelKey(key) {
43
43
  return { harness, modelId: key.slice(idx + 1) };
44
44
  }
45
45
  const VALID_COST_TIERS = new Set(["economy", "standard", "premium"]);
46
- const VALID_HARNESSES = new Set(["native", "cursor"]);
46
+ const VALID_SPEED_TIERS = new Set(["fastest", "fast", "balanced", "slow"]);
47
+ const VALID_HARNESSES = new Set(["native", "cursor", "copilot", "claude_code", "codex", "devin"]);
47
48
  function isModelEntry(entry) {
48
49
  return (typeof entry.id === "string" &&
49
50
  typeof entry.displayName === "string" &&
@@ -64,12 +65,53 @@ export const MODEL_REGISTRY = registryData.models
64
65
  modelId: m.id,
65
66
  provider: m.provider,
66
67
  displayName: m.displayName,
68
+ shortDescription: m.shortDescription ?? "",
69
+ speedTier: (VALID_SPEED_TIERS.has(m.speedTier ?? "") ? m.speedTier : "fast"),
67
70
  costTier: m.costTier,
68
71
  harness: m.harness,
69
72
  featured: m.featured ?? false,
70
73
  }));
71
- /** Model ID used when no user preference is set (native harness). */
72
- export const DEFAULT_MODEL_ID = "claude-sonnet-4.5";
74
+ /**
75
+ * Model ID used when no user preference is set (native harness).
76
+ *
77
+ * @deprecated Use {@link resolveDefaultModelId} for dynamic resolution
78
+ * based on the active harness and featured models. This constant is kept
79
+ * as the last-resort platform fallback.
80
+ */
81
+ export const DEFAULT_MODEL_ID = "claude-sonnet-4.6";
73
82
  /** Model ID used when the Cursor harness is selected and no user preference is set. */
74
83
  export const DEFAULT_CURSOR_MODEL_ID = "default";
84
+ /**
85
+ * Resolve the default model for a given harness using a priority chain.
86
+ *
87
+ * Priority (Phase 1 — no backend):
88
+ * 1. localStorage user preference (passed in as `userPreference`)
89
+ * 2. First featured model for the harness from the registry
90
+ * 3. Hardcoded platform fallback
91
+ *
92
+ * Future phases will add org-level and agent-level defaults between
93
+ * user preference and harness default.
94
+ */
95
+ export function resolveDefaultModelId(harness, options) {
96
+ if (options?.userPreference) {
97
+ const model = MODEL_REGISTRY.find((m) => m.harness === harness && m.modelId === options.userPreference);
98
+ if (model)
99
+ return { modelId: model.modelId, source: "user_preference" };
100
+ }
101
+ if (options?.orgDefault) {
102
+ const model = MODEL_REGISTRY.find((m) => m.harness === harness && m.modelId === options.orgDefault);
103
+ if (model)
104
+ return { modelId: model.modelId, source: "org_default" };
105
+ }
106
+ if (options?.agentDefault) {
107
+ const model = MODEL_REGISTRY.find((m) => m.harness === harness && m.modelId === options.agentDefault);
108
+ if (model)
109
+ return { modelId: model.modelId, source: "agent_default" };
110
+ }
111
+ const featured = MODEL_REGISTRY.find((m) => m.harness === harness && m.featured);
112
+ if (featured)
113
+ return { modelId: featured.modelId, source: "harness_default" };
114
+ const fallbackId = harness === "cursor" ? DEFAULT_CURSOR_MODEL_ID : DEFAULT_MODEL_ID;
115
+ return { modelId: fallbackId, source: "platform_fallback" };
116
+ }
75
117
  //# sourceMappingURL=registry.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/models/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,YAAY,MAAM,gCAAgC,CAAC;AA0B1D;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAA0B,IAAI,GAAG,CAAC;IAC/D,QAAQ;CACT,CAAC,CAAC;AA4CH;;;;;GAKG;AACH,MAAM,UAAU,QAAQ,CAAC,OAAsB,EAAE,OAAe;IAC9D,OAAO,GAAG,OAAO,IAAI,OAAO,EAAE,CAAC;AACjC,CAAC;AAUD;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,GAAW;IACvC,MAAM,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAC7B,IAAI,GAAG,GAAG,CAAC;QAAE,OAAO,SAAS,CAAC;IAC9B,MAAM,OAAO,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IAClC,IAAI,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC;IACnE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,EAAE,CAAC;AAClD,CAAC;AAsBD,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAAC,CAAC,SAAS,EAAE,UAAU,EAAE,SAAS,CAAC,CAAC,CAAC;AACrE,MAAM,eAAe,GAAG,IAAI,GAAG,CAAC,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC;AAEtD,SAAS,YAAY,CAAC,KAAwB;IAC5C,OAAO,CACL,OAAO,KAAK,CAAC,EAAE,KAAK,QAAQ;QAC5B,OAAO,KAAK,CAAC,WAAW,KAAK,QAAQ;QACrC,OAAO,KAAK,CAAC,QAAQ,KAAK,QAAQ;QAClC,OAAO,KAAK,CAAC,OAAO,KAAK,QAAQ,IAAI,eAAe,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC;QACvE,OAAO,KAAK,CAAC,QAAQ,KAAK,QAAQ,IAAI,gBAAgB,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,CAC3E,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,cAAc,GACzB,YAAY,CAAC,MACd;KACE,MAAM,CAAC,YAAY,CAAC;KACpB,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACX,OAAO,EAAE,CAAC,CAAC,EAAE;IACb,QAAQ,EAAE,CAAC,CAAC,QAAoB;IAChC,WAAW,EAAE,CAAC,CAAC,WAAW;IAC1B,QAAQ,EAAE,CAAC,CAAC,QAAoB;IAChC,OAAO,EAAE,CAAC,CAAC,OAAwB;IACnC,QAAQ,EAAE,CAAC,CAAC,QAAQ,IAAI,KAAK;CAC9B,CAAC,CAAC,CAAC;AAEN,qEAAqE;AACrE,MAAM,CAAC,MAAM,gBAAgB,GAAG,mBAAmB,CAAC;AAEpD,uFAAuF;AACvF,MAAM,CAAC,MAAM,uBAAuB,GAAG,SAAS,CAAC"}
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/models/registry.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"useModelRegistry.d.ts","sourceRoot":"","sources":["../../src/models/useModelRegistry.ts"],"names":[],"mappings":"AAGA,OAAO,EAML,KAAK,SAAS,EACd,KAAK,QAAQ,EACd,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAE/C,4CAA4C;AAC5C,MAAM,WAAW,uBAAuB;IACtC;;;;;;OAMG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,aAAa,CAAC;CAClC;AAED,gDAAgD;AAChD,MAAM,WAAW,sBAAsB;IACrC,gDAAgD;IAChD,QAAQ,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,CAAC;IACtC,0DAA0D;IAC1D,QAAQ,CAAC,UAAU,EAAE,WAAW,CAAC,QAAQ,EAAE,SAAS,SAAS,EAAE,CAAC,CAAC;IACjE,uDAAuD;IACvD,QAAQ,CAAC,YAAY,EAAE,SAAS,CAAC;IACjC;;;;;;;OAOG;IACH,QAAQ,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,SAAS,GAAG,SAAS,CAAC;IAC9D,oDAAoD;IACpD,QAAQ,CAAC,SAAS,EAAE,SAAS,QAAQ,EAAE,CAAC;IACxC,qEAAqE;IACrE,QAAQ,CAAC,QAAQ,EAAE,SAAS,SAAS,EAAE,CAAC;IACxC;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,SAAS,GAAG,SAAS,CAAC;CAC3D;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,CAAC,EAAE,uBAAuB,GAAG,sBAAsB,CA8D1F"}
1
+ {"version":3,"file":"useModelRegistry.d.ts","sourceRoot":"","sources":["../../src/models/useModelRegistry.ts"],"names":[],"mappings":"AAGA,OAAO,EAOL,KAAK,SAAS,EACd,KAAK,QAAQ,EACd,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAE/C,4CAA4C;AAC5C,MAAM,WAAW,uBAAuB;IACtC;;;;;;OAMG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,aAAa,CAAC;CAClC;AAED,gDAAgD;AAChD,MAAM,WAAW,sBAAsB;IACrC,gDAAgD;IAChD,QAAQ,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,CAAC;IACtC,0DAA0D;IAC1D,QAAQ,CAAC,UAAU,EAAE,WAAW,CAAC,QAAQ,EAAE,SAAS,SAAS,EAAE,CAAC,CAAC;IACjE,uDAAuD;IACvD,QAAQ,CAAC,YAAY,EAAE,SAAS,CAAC;IACjC;;;;;;;OAOG;IACH,QAAQ,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,SAAS,GAAG,SAAS,CAAC;IAC9D,oDAAoD;IACpD,QAAQ,CAAC,SAAS,EAAE,SAAS,QAAQ,EAAE,CAAC;IACxC,qEAAqE;IACrE,QAAQ,CAAC,QAAQ,EAAE,SAAS,SAAS,EAAE,CAAC;IACxC;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,SAAS,GAAG,SAAS,CAAC;CAC3D;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,CAAC,EAAE,uBAAuB,GAAG,sBAAsB,CA2D1F"}
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { useMemo } from "react";
3
- import { MODEL_REGISTRY, DEFAULT_MODEL_ID, DEFAULT_CURSOR_MODEL_ID, DISABLED_PROVIDERS, modelKey, } from "./registry";
3
+ import { MODEL_REGISTRY, DEFAULT_MODEL_ID, DISABLED_PROVIDERS, resolveDefaultModelId, modelKey, } from "./registry";
4
4
  /**
5
5
  * Data hook that exposes the platform model registry with grouping
6
6
  * and lookup helpers.
@@ -26,10 +26,10 @@ import { MODEL_REGISTRY, DEFAULT_MODEL_ID, DEFAULT_CURSOR_MODEL_ID, DISABLED_PRO
26
26
  export function useModelRegistry(options) {
27
27
  const harness = options?.harness;
28
28
  return useMemo(() => {
29
- const isCursor = harness === "cursor";
30
- const isNative = harness === "native";
31
29
  const isUnified = harness === undefined;
32
- const defaultId = isCursor ? DEFAULT_CURSOR_MODEL_ID : DEFAULT_MODEL_ID;
30
+ const { modelId: defaultId } = harness
31
+ ? resolveDefaultModelId(harness)
32
+ : { modelId: DEFAULT_MODEL_ID };
33
33
  const byProvider = new Map();
34
34
  const byId = new Map();
35
35
  const byCompoundKey = new Map();
@@ -37,18 +37,13 @@ export function useModelRegistry(options) {
37
37
  const featuredModels = [];
38
38
  let defaultModel;
39
39
  for (const model of MODEL_REGISTRY) {
40
- if (isCursor) {
41
- if (model.harness !== "cursor")
42
- continue;
43
- }
44
- else if (isNative) {
45
- if (model.harness !== "native")
46
- continue;
40
+ if (isUnified) {
47
41
  if (DISABLED_PROVIDERS.has(model.provider))
48
42
  continue;
49
43
  }
50
44
  else {
51
- // Unified include both harnesses, still respect DISABLED_PROVIDERS
45
+ if (model.harness !== harness)
46
+ continue;
52
47
  if (DISABLED_PROVIDERS.has(model.provider))
53
48
  continue;
54
49
  }
@@ -1 +1 @@
1
- {"version":3,"file":"useModelRegistry.js","sourceRoot":"","sources":["../../src/models/useModelRegistry.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"useModelRegistry.js","sourceRoot":"","sources":["../../src/models/useModelRegistry.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stigmer/react",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "React provider and client hook for the Stigmer platform SDK",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -35,7 +35,7 @@
35
35
  }
36
36
  },
37
37
  "dependencies": {
38
- "@stigmer/theme": "0.4.1",
38
+ "@stigmer/theme": "0.4.2",
39
39
  "react-markdown": "^10.1.0",
40
40
  "remark-gfm": "^4.0.1",
41
41
  "streamdown": "^2.5.0",
@@ -44,8 +44,8 @@
44
44
  "peerDependencies": {
45
45
  "@base-ui/react": "^1.0.0",
46
46
  "@bufbuild/protobuf": "^2.0.0",
47
- "@stigmer/protos": "0.4.1",
48
- "@stigmer/sdk": "0.4.1",
47
+ "@stigmer/protos": "0.4.2",
48
+ "@stigmer/sdk": "0.4.2",
49
49
  "lucide-react": ">=0.400.0",
50
50
  "react": "^19.0.0",
51
51
  "react-dom": "^19.0.0",
@@ -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