@dexto/tui 1.6.8 → 1.6.9

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 (43) hide show
  1. package/dist/components/overlays/LoginOverlay.cjs +41 -50
  2. package/dist/components/overlays/LoginOverlay.d.ts.map +1 -1
  3. package/dist/components/overlays/LoginOverlay.js +43 -44
  4. package/dist/components/overlays/ModelSelectorRefactored.cjs +543 -221
  5. package/dist/components/overlays/ModelSelectorRefactored.d.ts.map +1 -1
  6. package/dist/components/overlays/ModelSelectorRefactored.js +553 -223
  7. package/dist/components/overlays/SessionSelectorRefactored.cjs +3 -0
  8. package/dist/components/overlays/SessionSelectorRefactored.d.ts.map +1 -1
  9. package/dist/components/overlays/SessionSelectorRefactored.js +3 -0
  10. package/dist/containers/OverlayContainer.cjs +35 -3
  11. package/dist/containers/OverlayContainer.d.ts.map +1 -1
  12. package/dist/containers/OverlayContainer.js +36 -3
  13. package/dist/hooks/useInputOrchestrator.cjs +1 -1
  14. package/dist/hooks/useInputOrchestrator.d.ts.map +1 -1
  15. package/dist/hooks/useInputOrchestrator.js +1 -1
  16. package/dist/host/index.cjs +12 -13
  17. package/dist/host/index.d.ts +23 -15
  18. package/dist/host/index.d.ts.map +1 -1
  19. package/dist/host/index.js +10 -11
  20. package/dist/index.d.cts +17 -12
  21. package/dist/interactive-commands/auth/index.d.ts +1 -1
  22. package/dist/interactive-commands/commands.cjs +2 -0
  23. package/dist/interactive-commands/commands.d.ts.map +1 -1
  24. package/dist/interactive-commands/commands.js +3 -1
  25. package/dist/interactive-commands/model/index.cjs +1 -1
  26. package/dist/interactive-commands/model/index.js +1 -1
  27. package/dist/interactive-commands/session/index.cjs +2 -0
  28. package/dist/interactive-commands/session/index.d.ts +2 -1
  29. package/dist/interactive-commands/session/index.d.ts.map +1 -1
  30. package/dist/interactive-commands/session/index.js +2 -1
  31. package/dist/interactive-commands/session/session-commands.cjs +26 -0
  32. package/dist/interactive-commands/session/session-commands.d.ts +5 -0
  33. package/dist/interactive-commands/session/session-commands.d.ts.map +1 -1
  34. package/dist/interactive-commands/session/session-commands.js +25 -0
  35. package/dist/utils/modelOrdering.cjs +106 -0
  36. package/dist/utils/modelOrdering.d.ts +7 -0
  37. package/dist/utils/modelOrdering.d.ts.map +1 -0
  38. package/dist/utils/modelOrdering.js +81 -0
  39. package/dist/utils/modelOrdering.test.cjs +59 -0
  40. package/dist/utils/modelOrdering.test.d.ts +2 -0
  41. package/dist/utils/modelOrdering.test.d.ts.map +1 -0
  42. package/dist/utils/modelOrdering.test.js +61 -0
  43. package/package.json +4 -4
@@ -1,4 +1,4 @@
1
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
2
  import {
3
3
  useState,
4
4
  useEffect,
@@ -14,6 +14,7 @@ import {
14
14
  listOllamaModels,
15
15
  DEFAULT_OLLAMA_URL,
16
16
  getLocalModelById,
17
+ getCuratedModelRefsForProviders,
17
18
  getOpenRouterModelCacheInfo,
18
19
  getReasoningProfile,
19
20
  refreshOpenRouterModelCache
@@ -23,37 +24,198 @@ import {
23
24
  deleteCustomModel,
24
25
  getAllInstalledModels,
25
26
  loadGlobalPreferences,
26
- isDextoAuthEnabled
27
+ isDextoAuthEnabled,
28
+ loadModelPickerState,
29
+ toggleFavoriteModel,
30
+ toModelPickerKey
27
31
  } from "@dexto/agent-management";
28
32
  import { getLLMProviderDisplayName } from "../../utils/llm-provider-display.js";
29
33
  import { getMaxVisibleItemsForTerminalRows } from "../../utils/overlaySizing.js";
34
+ import { compareModelsLatestFirst, isDeprecatedModelStatus } from "../../utils/modelOrdering.js";
35
+ import {
36
+ getCachedStringWidth,
37
+ stripUnsafeCharacters,
38
+ toCodePoints
39
+ } from "../../utils/textUtils.js";
30
40
  import { HintBar } from "../shared/HintBar.js";
41
+ const FEATURED_SECTION_LIMIT = 8;
42
+ const MODEL_SELECTOR_TABS = [
43
+ { id: "all-models", label: "All" },
44
+ { id: "featured", label: "Featured" },
45
+ { id: "recents", label: "Recents" },
46
+ { id: "favorites", label: "Favorites" },
47
+ { id: "custom", label: "Custom" }
48
+ ];
49
+ const PROVIDER_COLLATOR = new Intl.Collator("en", { sensitivity: "base" });
50
+ const PROVIDER_TOKEN_PATTERN = /[^a-z0-9]/g;
51
+ function normalizeProviderToken(value) {
52
+ return value.toLowerCase().replace(PROVIDER_TOKEN_PATTERN, "");
53
+ }
54
+ function toReleaseDateLookupKey(provider, modelName) {
55
+ return `${provider}::${modelName.toLowerCase()}`;
56
+ }
57
+ function splitGatewayModelName(modelName) {
58
+ const slashIndex = modelName.indexOf("/");
59
+ if (slashIndex <= 0 || slashIndex >= modelName.length - 1) {
60
+ return null;
61
+ }
62
+ return {
63
+ providerPrefix: modelName.slice(0, slashIndex),
64
+ unprefixedName: modelName.slice(slashIndex + 1)
65
+ };
66
+ }
67
+ function createReleaseDateResolver({
68
+ allModels,
69
+ providers
70
+ }) {
71
+ const releaseDateByProviderAndName = /* @__PURE__ */ new Map();
72
+ const releaseDateCandidatesByName = /* @__PURE__ */ new Map();
73
+ const latestReleaseDateByName = /* @__PURE__ */ new Map();
74
+ const providerByToken = /* @__PURE__ */ new Map();
75
+ for (const provider of providers) {
76
+ const token = normalizeProviderToken(provider);
77
+ if (token && !providerByToken.has(token)) {
78
+ providerByToken.set(token, provider);
79
+ }
80
+ const modelsForProvider = allModels[provider];
81
+ if (!modelsForProvider || modelsForProvider.length === 0) {
82
+ continue;
83
+ }
84
+ for (const model of modelsForProvider) {
85
+ const releaseDate = model.releaseDate;
86
+ if (!releaseDate) {
87
+ continue;
88
+ }
89
+ const lowerName = model.name.toLowerCase();
90
+ releaseDateByProviderAndName.set(
91
+ toReleaseDateLookupKey(provider, lowerName),
92
+ releaseDate
93
+ );
94
+ const existingLatest = latestReleaseDateByName.get(lowerName);
95
+ if (!existingLatest || releaseDate > existingLatest) {
96
+ latestReleaseDateByName.set(lowerName, releaseDate);
97
+ }
98
+ const candidates = releaseDateCandidatesByName.get(lowerName) ?? [];
99
+ candidates.push({ provider, releaseDate });
100
+ releaseDateCandidatesByName.set(lowerName, candidates);
101
+ }
102
+ }
103
+ return (provider, modelName, explicitReleaseDate) => {
104
+ if (explicitReleaseDate) {
105
+ return explicitReleaseDate;
106
+ }
107
+ const lowerName = modelName.toLowerCase();
108
+ const sameProviderDate = releaseDateByProviderAndName.get(
109
+ toReleaseDateLookupKey(provider, lowerName)
110
+ );
111
+ if (sameProviderDate) {
112
+ return sameProviderDate;
113
+ }
114
+ const openRouterDate = releaseDateByProviderAndName.get(
115
+ toReleaseDateLookupKey("openrouter", lowerName)
116
+ );
117
+ if (openRouterDate) {
118
+ return openRouterDate;
119
+ }
120
+ const parsedGatewayModel = splitGatewayModelName(modelName);
121
+ if (!parsedGatewayModel) {
122
+ return void 0;
123
+ }
124
+ const unprefixedName = parsedGatewayModel.unprefixedName.toLowerCase();
125
+ const preferredProvider = providerByToken.get(
126
+ normalizeProviderToken(parsedGatewayModel.providerPrefix)
127
+ );
128
+ if (preferredProvider) {
129
+ const providerDate = releaseDateByProviderAndName.get(
130
+ toReleaseDateLookupKey(preferredProvider, unprefixedName)
131
+ );
132
+ if (providerDate) {
133
+ return providerDate;
134
+ }
135
+ }
136
+ const candidates = releaseDateCandidatesByName.get(unprefixedName);
137
+ if (candidates && preferredProvider) {
138
+ const preferredProviderCandidate = candidates.find(
139
+ (candidate) => candidate.provider === preferredProvider
140
+ );
141
+ if (preferredProviderCandidate) {
142
+ return preferredProviderCandidate.releaseDate;
143
+ }
144
+ }
145
+ return latestReleaseDateByName.get(unprefixedName);
146
+ };
147
+ }
148
+ function getNextModelSelectorTab(current) {
149
+ const currentIndex = MODEL_SELECTOR_TABS.findIndex((tab) => tab.id === current);
150
+ const nextIndex = currentIndex < 0 ? 0 : (currentIndex + 1) % MODEL_SELECTOR_TABS.length;
151
+ return MODEL_SELECTOR_TABS[nextIndex]?.id ?? "all-models";
152
+ }
153
+ function getPreviousModelSelectorTab(current) {
154
+ const currentIndex = MODEL_SELECTOR_TABS.findIndex((tab) => tab.id === current);
155
+ const previousIndex = currentIndex < 0 ? 0 : (currentIndex - 1 + MODEL_SELECTOR_TABS.length) % MODEL_SELECTOR_TABS.length;
156
+ return MODEL_SELECTOR_TABS[previousIndex]?.id ?? "all-models";
157
+ }
158
+ function compareModelOptionsForDisplay(left, right) {
159
+ const byRecency = compareModelsLatestFirst(left, right);
160
+ if (byRecency !== 0) {
161
+ return byRecency;
162
+ }
163
+ const byProvider = PROVIDER_COLLATOR.compare(left.provider, right.provider);
164
+ if (byProvider !== 0) {
165
+ return byProvider;
166
+ }
167
+ return 0;
168
+ }
169
+ function toModelIdentityKey(model) {
170
+ return toModelPickerKey({ provider: model.provider, model: model.name });
171
+ }
172
+ function normalizeLineText(value) {
173
+ return stripUnsafeCharacters(value).replace(/\r?\n/g, " ").replace(/\s+/g, " ").trim();
174
+ }
175
+ function formatLineToWidth(value, width) {
176
+ if (width <= 0) return "";
177
+ const normalized = normalizeLineText(value);
178
+ if (!normalized) {
179
+ return " ".repeat(width);
180
+ }
181
+ const normalizedWidth = getCachedStringWidth(normalized);
182
+ if (normalizedWidth <= width) {
183
+ return normalized + " ".repeat(width - normalizedWidth);
184
+ }
185
+ if (width === 1) {
186
+ return "\u2026";
187
+ }
188
+ const ellipsis = "\u2026";
189
+ const targetWidth = width - getCachedStringWidth(ellipsis);
190
+ let truncated = "";
191
+ for (const char of toCodePoints(normalized)) {
192
+ const candidate = `${truncated}${char}`;
193
+ if (getCachedStringWidth(candidate) > targetWidth) {
194
+ break;
195
+ }
196
+ truncated = candidate;
197
+ }
198
+ const withEllipsis = `${truncated}${ellipsis}`;
199
+ const finalWidth = getCachedStringWidth(withEllipsis);
200
+ if (finalWidth >= width) {
201
+ return withEllipsis;
202
+ }
203
+ return withEllipsis + " ".repeat(width - finalWidth);
204
+ }
31
205
  function isAddCustomOption(item) {
32
206
  return "type" in item && item.type === "add-custom";
33
207
  }
208
+ function isModelOption(item) {
209
+ return !("type" in item);
210
+ }
34
211
  function getRowPrefix({
35
212
  isSelected,
36
213
  isDefault,
37
214
  isCurrent,
38
- isCustom
215
+ isCustom,
216
+ isFavorite
39
217
  }) {
40
- return `${isSelected ? "\u203A" : " "} ${isDefault ? "\u2713" : " "} ${isCurrent ? "\u25CF" : " "} ${isCustom ? "\u2605" : " "}`;
41
- }
42
- function computeNextSelection(currentIndex, itemsLength, viewportItems) {
43
- const nextIndex = currentIndex;
44
- let nextOffset = 0;
45
- const modelsLength = Math.max(0, itemsLength - 1);
46
- if (nextIndex > 0) {
47
- const modelIndex = nextIndex - 1;
48
- if (modelIndex < nextOffset) {
49
- nextOffset = modelIndex;
50
- } else if (modelIndex >= nextOffset + viewportItems) {
51
- nextOffset = Math.max(0, modelIndex - viewportItems + 1);
52
- }
53
- }
54
- const maxOffset = Math.max(0, modelsLength - viewportItems);
55
- nextOffset = Math.min(maxOffset, Math.max(0, nextOffset));
56
- return { index: nextIndex, offset: nextOffset };
218
+ return `${isSelected ? "\u203A" : " "} ${isDefault ? "\u2713" : " "} ${isCurrent ? "\u25CF" : " "} ${isFavorite ? "\u2605" : isCustom ? "\u25C7" : " "}`;
57
219
  }
58
220
  const REASONING_VARIANT_DESCRIPTIONS = {
59
221
  disabled: "Disable reasoning (fastest)",
@@ -88,7 +250,8 @@ const ModelSelector = forwardRef(function ModelSelector2({
88
250
  onEditCustomModel,
89
251
  agent
90
252
  }, ref) {
91
- const { rows: terminalRows } = useTerminalSize();
253
+ const { rows: terminalRows, columns: terminalColumns } = useTerminalSize();
254
+ const overlayWidth = useMemo(() => Math.max(20, terminalColumns - 2), [terminalColumns]);
92
255
  const maxVisibleItems = useMemo(() => {
93
256
  return getMaxVisibleItemsForTerminalRows({
94
257
  rows: terminalRows,
@@ -98,6 +261,8 @@ const ModelSelector = forwardRef(function ModelSelector2({
98
261
  }, [terminalRows]);
99
262
  const [models, setModels] = useState([]);
100
263
  const [customModels, setCustomModels] = useState([]);
264
+ const [modelPickerState, setModelPickerState] = useState(null);
265
+ const [activeTab, setActiveTab] = useState("all-models");
101
266
  const [isLoading, setIsLoading] = useState(false);
102
267
  const [selection, setSelection] = useState({ index: 0, offset: 0 });
103
268
  const [searchQuery, setSearchQuery] = useState("");
@@ -141,6 +306,8 @@ const ModelSelector = forwardRef(function ModelSelector2({
141
306
  setPendingReasoningModel(null);
142
307
  setIsSettingDefault(false);
143
308
  setReasoningVariantIndex(0);
309
+ setActiveTab("all-models");
310
+ setModelPickerState(null);
144
311
  if (deleteTimeoutRef.current) {
145
312
  clearTimeout(deleteTimeoutRef.current);
146
313
  deleteTimeoutRef.current = null;
@@ -164,11 +331,13 @@ const ModelSelector = forwardRef(function ModelSelector2({
164
331
  loadCustomModels(),
165
332
  loadGlobalPreferences().catch(() => null)
166
333
  ]);
334
+ const pickerState = await loadModelPickerState().catch(() => null);
167
335
  const modelList = [];
168
336
  const defaultProvider = preferences?.llm.provider;
169
337
  const defaultModel = preferences?.llm.model;
170
338
  const defaultBaseURL = preferences?.llm.baseURL;
171
339
  const defaultReasoningVariant = preferences?.llm.reasoning?.variant;
340
+ const resolveReleaseDate = createReleaseDateResolver({ allModels, providers });
172
341
  let ollamaModels = [];
173
342
  let localModels = [];
174
343
  try {
@@ -209,8 +378,16 @@ const ModelSelector = forwardRef(function ModelSelector2({
209
378
  if (provider === "dexto-nova" && !isDextoAuthEnabled()) {
210
379
  continue;
211
380
  }
212
- const providerModels = allModels[provider];
381
+ const providerModels = [...allModels[provider]].sort(compareModelsLatestFirst);
213
382
  for (const model of providerModels) {
383
+ if (isDeprecatedModelStatus(model.status)) {
384
+ continue;
385
+ }
386
+ const releaseDate = resolveReleaseDate(
387
+ provider,
388
+ model.name,
389
+ model.releaseDate
390
+ );
214
391
  const originalProvider = "originalProvider" in model ? model.originalProvider : void 0;
215
392
  modelList.push({
216
393
  provider,
@@ -220,6 +397,8 @@ const ModelSelector = forwardRef(function ModelSelector2({
220
397
  isDefault: provider === defaultProvider && model.name === defaultModel,
221
398
  isCurrent: provider === currentConfig.provider && model.name === currentConfig.model,
222
399
  isCustom: false,
400
+ ...releaseDate !== void 0 ? { releaseDate } : {},
401
+ ...model.status !== void 0 ? { status: model.status } : {},
223
402
  ...defaultReasoningVariant && provider === defaultProvider && model.name === defaultModel ? { reasoningVariant: defaultReasoningVariant } : {},
224
403
  ...defaultBaseURL && provider === defaultProvider && model.name === defaultModel ? { baseURL: defaultBaseURL } : {},
225
404
  // Store original provider for display purposes
@@ -255,7 +434,15 @@ const ModelSelector = forwardRef(function ModelSelector2({
255
434
  }
256
435
  const vertexModels = allModels["vertex"];
257
436
  if (vertexModels) {
258
- for (const model of vertexModels) {
437
+ for (const model of [...vertexModels].sort(compareModelsLatestFirst)) {
438
+ if (isDeprecatedModelStatus(model.status)) {
439
+ continue;
440
+ }
441
+ const releaseDate = resolveReleaseDate(
442
+ "vertex",
443
+ model.name,
444
+ model.releaseDate
445
+ );
259
446
  modelList.push({
260
447
  provider: "vertex",
261
448
  name: model.name,
@@ -264,23 +451,73 @@ const ModelSelector = forwardRef(function ModelSelector2({
264
451
  isDefault: defaultProvider === "vertex" && defaultModel === model.name,
265
452
  isCurrent: currentConfig.provider === "vertex" && currentConfig.model === model.name,
266
453
  isCustom: false,
454
+ ...releaseDate !== void 0 ? { releaseDate } : {},
455
+ ...model.status !== void 0 ? { status: model.status } : {},
267
456
  ...defaultReasoningVariant && defaultProvider === "vertex" && defaultModel === model.name ? { reasoningVariant: defaultReasoningVariant } : {}
268
457
  });
269
458
  }
270
459
  }
271
460
  if (!cancelled) {
272
- setModels(modelList);
461
+ const dedupedByKey = /* @__PURE__ */ new Map();
462
+ const dedupeOrder = [];
463
+ for (const model of modelList) {
464
+ const key = toModelIdentityKey(model);
465
+ const existing = dedupedByKey.get(key);
466
+ if (!existing) {
467
+ dedupedByKey.set(key, model);
468
+ dedupeOrder.push(key);
469
+ continue;
470
+ }
471
+ const preferred = model.isCustom && !existing.isCustom ? model : existing;
472
+ const secondary = preferred === existing ? model : existing;
473
+ const mergedBaseURL = preferred.baseURL ?? secondary.baseURL;
474
+ const mergedReasoningVariant = preferred.reasoningVariant ?? secondary.reasoningVariant;
475
+ const mergedOriginalProvider = preferred.originalProvider ?? secondary.originalProvider;
476
+ const mergedReleaseDate = preferred.releaseDate ?? secondary.releaseDate;
477
+ const mergedStatus = preferred.status ?? secondary.status;
478
+ const mergedModel = {
479
+ ...preferred,
480
+ isDefault: preferred.isDefault || secondary.isDefault,
481
+ isCurrent: preferred.isCurrent || secondary.isCurrent,
482
+ displayName: preferred.displayName ?? secondary.displayName,
483
+ maxInputTokens: Math.max(
484
+ preferred.maxInputTokens,
485
+ secondary.maxInputTokens
486
+ )
487
+ };
488
+ if (mergedBaseURL !== void 0) {
489
+ mergedModel.baseURL = mergedBaseURL;
490
+ }
491
+ if (mergedReasoningVariant !== void 0) {
492
+ mergedModel.reasoningVariant = mergedReasoningVariant;
493
+ }
494
+ if (mergedOriginalProvider !== void 0) {
495
+ mergedModel.originalProvider = mergedOriginalProvider;
496
+ }
497
+ if (mergedReleaseDate !== void 0) {
498
+ mergedModel.releaseDate = mergedReleaseDate;
499
+ }
500
+ if (mergedStatus !== void 0) {
501
+ mergedModel.status = mergedStatus;
502
+ }
503
+ dedupedByKey.set(key, mergedModel);
504
+ }
505
+ const dedupedModelList = dedupeOrder.map((key) => dedupedByKey.get(key)).filter((model) => model !== void 0);
506
+ setModels(dedupedModelList);
273
507
  setCustomModels(loadedCustomModels);
508
+ setModelPickerState(pickerState);
274
509
  setIsLoading(false);
275
- const currentIndex = modelList.findIndex((m) => m.isCurrent);
510
+ const currentIndex = dedupedModelList.findIndex((m) => m.isCurrent);
276
511
  if (currentIndex >= 0) {
277
- const nextIndex = currentIndex + 1;
512
+ const nextIndex = currentIndex;
278
513
  const nextMaxVisibleItems = maxVisibleItemsRef.current;
279
- const nextModelsViewportItems = Math.max(1, nextMaxVisibleItems - 1);
280
- const maxOffset = Math.max(0, modelList.length - nextModelsViewportItems);
514
+ const maxOffset = Math.max(
515
+ 0,
516
+ dedupedModelList.length - nextMaxVisibleItems
517
+ );
281
518
  const nextOffset = Math.min(
282
519
  maxOffset,
283
- Math.max(0, currentIndex - nextModelsViewportItems + 1)
520
+ Math.max(0, currentIndex - nextMaxVisibleItems + 1)
284
521
  );
285
522
  selectedIndexRef.current = nextIndex;
286
523
  setSelection({ index: nextIndex, offset: nextOffset });
@@ -292,6 +529,7 @@ const ModelSelector = forwardRef(function ModelSelector2({
292
529
  `Failed to fetch models: ${error instanceof Error ? error.message : "Unknown error"}`
293
530
  );
294
531
  setModels([]);
532
+ setModelPickerState(null);
295
533
  setIsLoading(false);
296
534
  }
297
535
  }
@@ -301,35 +539,99 @@ const ModelSelector = forwardRef(function ModelSelector2({
301
539
  cancelled = true;
302
540
  };
303
541
  }, [isVisible, agent, refreshVersion]);
304
- const filteredItems = useMemo(() => {
305
- const addCustomOption = { type: "add-custom" };
306
- if (!searchQuery.trim()) {
307
- return [addCustomOption, ...models];
308
- }
309
- const query = searchQuery.toLowerCase().replace(/[\s-]+/g, "");
310
- const filtered = models.filter((model) => {
542
+ const favoriteKeySet = useMemo(
543
+ () => new Set(
544
+ (modelPickerState?.favorites ?? []).map(
545
+ (entry) => toModelPickerKey({
546
+ provider: entry.provider,
547
+ model: entry.model
548
+ })
549
+ )
550
+ ),
551
+ [modelPickerState]
552
+ );
553
+ const matchesSearch = useCallback(
554
+ (model) => {
555
+ if (!searchQuery.trim()) {
556
+ return true;
557
+ }
558
+ const query = searchQuery.toLowerCase().replace(/[\s-]+/g, "");
311
559
  const name = model.name.toLowerCase().replace(/[\s-]+/g, "");
312
560
  const displayName = (model.displayName || "").toLowerCase().replace(/[\s-]+/g, "");
313
561
  const provider = model.provider.toLowerCase().replace(/[\s-]+/g, "");
314
562
  return name.includes(query) || displayName.includes(query) || provider.includes(query);
315
- });
316
- return [addCustomOption, ...filtered];
317
- }, [models, searchQuery]);
563
+ },
564
+ [searchQuery]
565
+ );
566
+ const filteredItems = useMemo(() => {
567
+ const addCustomOption = { type: "add-custom" };
568
+ const hasSearchQuery = searchQuery.trim().length > 0;
569
+ const allCandidates = [...models].sort(compareModelOptionsForDisplay);
570
+ const modelsByKey = new Map(
571
+ allCandidates.map((model) => [
572
+ toModelPickerKey({ provider: model.provider, model: model.name }),
573
+ model
574
+ ])
575
+ );
576
+ const toUniqueMatchingModels = (candidates, limit) => {
577
+ const deduped = [];
578
+ const seen = /* @__PURE__ */ new Set();
579
+ for (const candidate of candidates) {
580
+ if (!candidate || !matchesSearch(candidate)) {
581
+ continue;
582
+ }
583
+ const key = toModelPickerKey({
584
+ provider: candidate.provider,
585
+ model: candidate.name
586
+ });
587
+ if (seen.has(key)) {
588
+ continue;
589
+ }
590
+ seen.add(key);
591
+ deduped.push(candidate);
592
+ if (limit !== void 0 && deduped.length >= limit) {
593
+ break;
594
+ }
595
+ }
596
+ return deduped;
597
+ };
598
+ const providersInModels = Array.from(
599
+ new Set(models.map((model) => model.provider))
600
+ );
601
+ const featuredCandidates = getCuratedModelRefsForProviders({
602
+ providers: providersInModels,
603
+ max: FEATURED_SECTION_LIMIT
604
+ }).map((ref2) => modelsByKey.get(toModelPickerKey(ref2)));
605
+ const recentsFromState = (modelPickerState?.recents ?? []).map(
606
+ (entry) => modelsByKey.get(toModelPickerKey({ provider: entry.provider, model: entry.model }))
607
+ );
608
+ const favoritesFromState = (modelPickerState?.favorites ?? []).map(
609
+ (entry) => modelsByKey.get(toModelPickerKey({ provider: entry.provider, model: entry.model }))
610
+ );
611
+ const customCandidates = allCandidates.filter((model) => model.isCustom);
612
+ const tabModels = hasSearchQuery ? toUniqueMatchingModels(allCandidates) : activeTab === "all-models" ? toUniqueMatchingModels(allCandidates) : activeTab === "featured" ? toUniqueMatchingModels(featuredCandidates) : activeTab === "recents" ? toUniqueMatchingModels(recentsFromState) : activeTab === "favorites" ? toUniqueMatchingModels(favoritesFromState) : toUniqueMatchingModels(customCandidates);
613
+ return activeTab === "custom" && !hasSearchQuery ? [addCustomOption, ...tabModels] : tabModels;
614
+ }, [activeTab, matchesSearch, modelPickerState, models, searchQuery]);
615
+ const hasAddCustomOption = activeTab === "custom" && searchQuery.trim().length === 0;
616
+ const modelStartIndex = hasAddCustomOption ? 1 : 0;
617
+ const listViewportItems = hasAddCustomOption ? modelsViewportItems : maxVisibleItems;
318
618
  useEffect(() => {
319
619
  setSelection((prev) => {
320
620
  const maxIndex = Math.max(0, filteredItems.length - 1);
321
621
  const nextIndex = Math.min(prev.index, maxIndex);
322
622
  let nextOffset = prev.offset;
323
- const nextModelsLength = Math.max(0, filteredItems.length - 1);
324
- if (nextIndex > 0) {
325
- const modelIndex = nextIndex - 1;
623
+ const nextModelsLength = Math.max(0, filteredItems.length - modelStartIndex);
624
+ if (nextIndex >= modelStartIndex) {
625
+ const modelIndex = nextIndex - modelStartIndex;
326
626
  if (modelIndex < nextOffset) {
327
627
  nextOffset = modelIndex;
328
- } else if (modelIndex >= nextOffset + modelsViewportItems) {
329
- nextOffset = Math.max(0, modelIndex - modelsViewportItems + 1);
628
+ } else if (modelIndex >= nextOffset + listViewportItems) {
629
+ nextOffset = Math.max(0, modelIndex - listViewportItems + 1);
330
630
  }
631
+ } else {
632
+ nextOffset = 0;
331
633
  }
332
- const maxOffset = Math.max(0, nextModelsLength - modelsViewportItems);
634
+ const maxOffset = Math.max(0, nextModelsLength - listViewportItems);
333
635
  nextOffset = Math.min(maxOffset, Math.max(0, nextOffset));
334
636
  if (nextIndex === prev.index && nextOffset === prev.offset) {
335
637
  return prev;
@@ -337,7 +639,7 @@ const ModelSelector = forwardRef(function ModelSelector2({
337
639
  selectedIndexRef.current = nextIndex;
338
640
  return { index: nextIndex, offset: nextOffset };
339
641
  });
340
- }, [filteredItems.length, modelsViewportItems]);
642
+ }, [filteredItems.length, listViewportItems, modelStartIndex]);
341
643
  const handleDeleteCustomModel = useCallback(
342
644
  async (model) => {
343
645
  if (!model.isCustom) return;
@@ -354,6 +656,23 @@ const ModelSelector = forwardRef(function ModelSelector2({
354
656
  },
355
657
  [agent]
356
658
  );
659
+ const handleToggleFavoriteModel = useCallback(
660
+ async (model) => {
661
+ try {
662
+ await toggleFavoriteModel({
663
+ provider: model.provider,
664
+ model: model.name
665
+ });
666
+ const nextState = await loadModelPickerState();
667
+ setModelPickerState(nextState);
668
+ } catch (error) {
669
+ agent.logger.error(
670
+ `Failed to toggle favorite model: ${error instanceof Error ? error.message : "Unknown error"}`
671
+ );
672
+ }
673
+ },
674
+ [agent]
675
+ );
357
676
  const clearActionState = () => {
358
677
  setCustomModelAction(null);
359
678
  setPendingDeleteConfirm(false);
@@ -452,27 +771,44 @@ const ModelSelector = forwardRef(function ModelSelector2({
452
771
  }
453
772
  const itemsLength = filteredItems.length;
454
773
  const currentItem = filteredItems[selectedIndexRef.current];
455
- const isCustomActionItem = currentItem && !isAddCustomOption(currentItem) && currentItem.isCustom;
456
- const isSelectableItem = currentItem && !isAddCustomOption(currentItem);
457
- if (key.rightArrow) {
774
+ const selectedModel = currentItem && isModelOption(currentItem) ? currentItem : null;
775
+ const isCustomActionItem = selectedModel?.isCustom ?? false;
776
+ const isSelectableItem = selectedModel !== null;
777
+ if (key.tab) {
778
+ clearActionState();
779
+ setActiveTab((prev) => getNextModelSelectorTab(prev));
780
+ selectedIndexRef.current = 0;
781
+ setSelection({ index: 0, offset: 0 });
782
+ return true;
783
+ }
784
+ if (key.ctrl && input === "f" && isSelectableItem) {
785
+ const item = selectedModel;
786
+ if (!item) return true;
787
+ clearActionState();
788
+ void handleToggleFavoriteModel(item);
789
+ return true;
790
+ }
791
+ if (key.ctrl && key.rightArrow) {
458
792
  if (!isSelectableItem) return false;
459
793
  if (customModelAction === null) {
460
- if (isCustomActionItem) {
461
- setCustomModelAction("edit");
462
- } else {
463
- setCustomModelAction("default");
464
- }
794
+ setCustomModelAction("favorite");
465
795
  return true;
466
796
  }
467
- if (customModelAction === "edit") {
797
+ if (customModelAction === "favorite") {
468
798
  setCustomModelAction("default");
469
799
  return true;
470
800
  }
471
801
  if (customModelAction === "default") {
802
+ if (isCustomActionItem) {
803
+ setCustomModelAction("edit");
804
+ return true;
805
+ }
806
+ return true;
807
+ }
808
+ if (customModelAction === "edit") {
472
809
  if (isCustomActionItem) {
473
810
  setCustomModelAction("delete");
474
811
  setPendingDeleteConfirm(false);
475
- return true;
476
812
  }
477
813
  return true;
478
814
  }
@@ -480,9 +816,9 @@ const ModelSelector = forwardRef(function ModelSelector2({
480
816
  return true;
481
817
  }
482
818
  }
483
- if (key.leftArrow) {
819
+ if (key.ctrl && key.leftArrow) {
484
820
  if (customModelAction === "delete") {
485
- setCustomModelAction("default");
821
+ setCustomModelAction("edit");
486
822
  setPendingDeleteConfirm(false);
487
823
  if (deleteTimeoutRef.current) {
488
824
  clearTimeout(deleteTimeoutRef.current);
@@ -491,19 +827,33 @@ const ModelSelector = forwardRef(function ModelSelector2({
491
827
  return true;
492
828
  }
493
829
  if (customModelAction === "default") {
494
- if (isCustomActionItem) {
495
- setCustomModelAction("edit");
496
- } else {
497
- setCustomModelAction(null);
498
- }
830
+ setCustomModelAction("favorite");
499
831
  return true;
500
832
  }
501
833
  if (customModelAction === "edit") {
834
+ setCustomModelAction("default");
835
+ return true;
836
+ }
837
+ if (customModelAction === "favorite") {
502
838
  setCustomModelAction(null);
503
839
  return true;
504
840
  }
505
841
  return false;
506
842
  }
843
+ if (key.rightArrow) {
844
+ clearActionState();
845
+ setActiveTab((prev) => getNextModelSelectorTab(prev));
846
+ selectedIndexRef.current = 0;
847
+ setSelection({ index: 0, offset: 0 });
848
+ return true;
849
+ }
850
+ if (key.leftArrow) {
851
+ clearActionState();
852
+ setActiveTab((prev) => getPreviousModelSelectorTab(prev));
853
+ selectedIndexRef.current = 0;
854
+ setSelection({ index: 0, offset: 0 });
855
+ return true;
856
+ }
507
857
  if (input && !key.return && !key.upArrow && !key.downArrow && !key.tab) {
508
858
  if (customModelAction) {
509
859
  clearActionState();
@@ -532,16 +882,18 @@ const ModelSelector = forwardRef(function ModelSelector2({
532
882
  selectedIndexRef.current = nextIndex;
533
883
  setSelection((prev) => {
534
884
  let nextOffset = prev.offset;
535
- const nextModelsLength = Math.max(0, itemsLength - 1);
536
- if (nextIndex > 0) {
537
- const modelIndex = nextIndex - 1;
885
+ const nextModelsLength = Math.max(0, itemsLength - modelStartIndex);
886
+ if (nextIndex >= modelStartIndex) {
887
+ const modelIndex = nextIndex - modelStartIndex;
538
888
  if (modelIndex < prev.offset) {
539
889
  nextOffset = modelIndex;
540
- } else if (modelIndex >= prev.offset + modelsViewportItems) {
541
- nextOffset = Math.max(0, modelIndex - modelsViewportItems + 1);
890
+ } else if (modelIndex >= prev.offset + listViewportItems) {
891
+ nextOffset = Math.max(0, modelIndex - listViewportItems + 1);
542
892
  }
893
+ } else {
894
+ nextOffset = 0;
543
895
  }
544
- const maxOffset = Math.max(0, nextModelsLength - modelsViewportItems);
896
+ const maxOffset = Math.max(0, nextModelsLength - listViewportItems);
545
897
  nextOffset = Math.min(maxOffset, Math.max(0, nextOffset));
546
898
  return { index: nextIndex, offset: nextOffset };
547
899
  });
@@ -555,16 +907,18 @@ const ModelSelector = forwardRef(function ModelSelector2({
555
907
  selectedIndexRef.current = nextIndex;
556
908
  setSelection((prev) => {
557
909
  let nextOffset = prev.offset;
558
- const nextModelsLength = Math.max(0, itemsLength - 1);
559
- if (nextIndex > 0) {
560
- const modelIndex = nextIndex - 1;
910
+ const nextModelsLength = Math.max(0, itemsLength - modelStartIndex);
911
+ if (nextIndex >= modelStartIndex) {
912
+ const modelIndex = nextIndex - modelStartIndex;
561
913
  if (modelIndex < prev.offset) {
562
914
  nextOffset = modelIndex;
563
- } else if (modelIndex >= prev.offset + modelsViewportItems) {
564
- nextOffset = Math.max(0, modelIndex - modelsViewportItems + 1);
915
+ } else if (modelIndex >= prev.offset + listViewportItems) {
916
+ nextOffset = Math.max(0, modelIndex - listViewportItems + 1);
565
917
  }
918
+ } else {
919
+ nextOffset = 0;
566
920
  }
567
- const maxOffset = Math.max(0, nextModelsLength - modelsViewportItems);
921
+ const maxOffset = Math.max(0, nextModelsLength - listViewportItems);
568
922
  nextOffset = Math.min(maxOffset, Math.max(0, nextOffset));
569
923
  return { index: nextIndex, offset: nextOffset };
570
924
  });
@@ -577,6 +931,10 @@ const ModelSelector = forwardRef(function ModelSelector2({
577
931
  onAddCustomModel();
578
932
  return true;
579
933
  }
934
+ if (customModelAction === "favorite") {
935
+ void handleToggleFavoriteModel(item);
936
+ return true;
937
+ }
580
938
  if (customModelAction === "edit" && item.isCustom) {
581
939
  const customModel = customModels.find(
582
940
  (cm) => cm.name === item.name && (cm.provider ?? "openai-compatible") === item.provider
@@ -635,7 +993,8 @@ const ModelSelector = forwardRef(function ModelSelector2({
635
993
  isLoading,
636
994
  filteredItems,
637
995
  maxVisibleItems,
638
- modelsViewportItems,
996
+ listViewportItems,
997
+ modelStartIndex,
639
998
  onClose,
640
999
  onSelectModel,
641
1000
  onSetDefaultModel,
@@ -645,14 +1004,18 @@ const ModelSelector = forwardRef(function ModelSelector2({
645
1004
  pendingDeleteConfirm,
646
1005
  customModels,
647
1006
  handleDeleteCustomModel,
1007
+ handleToggleFavoriteModel,
648
1008
  pendingReasoningModel,
649
1009
  reasoningVariantIndex,
650
1010
  reasoningVariantOptions,
651
1011
  isSettingDefault,
1012
+ activeTab,
1013
+ agent,
652
1014
  beginReasoningVariantSelection
653
1015
  ]
654
1016
  );
655
1017
  if (!isVisible) return null;
1018
+ const blankLine = " ".repeat(overlayWidth);
656
1019
  if (pendingReasoningModel) {
657
1020
  const totalOptions = reasoningVariantOptions.length;
658
1021
  const reasoningVisibleItems = Math.min(maxVisibleItems, totalOptions);
@@ -665,200 +1028,167 @@ const ModelSelector = forwardRef(function ModelSelector2({
665
1028
  reasoningOffset + reasoningVisibleItems
666
1029
  );
667
1030
  const selectedReasoningOption = reasoningVariantOptions[reasoningVariantIndex] ?? reasoningVariantOptions[0];
668
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
669
- /* @__PURE__ */ jsx(Box, { paddingX: 0, paddingY: 0, children: /* @__PURE__ */ jsxs(Text, { color: "cyan", bold: true, children: [
1031
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width: overlayWidth, children: [
1032
+ /* @__PURE__ */ jsx(Box, { paddingX: 0, paddingY: 0, width: overlayWidth, children: /* @__PURE__ */ jsxs(Text, { color: "cyan", bold: true, children: [
670
1033
  "Reasoning Variant",
671
1034
  isSettingDefault ? /* @__PURE__ */ jsx(Text, { color: "gray", children: " (default)" }) : null
672
1035
  ] }) }),
673
- /* @__PURE__ */ jsx(Box, { paddingX: 0, paddingY: 0, children: /* @__PURE__ */ jsx(Text, { color: "gray", wrap: "truncate-end", children: pendingReasoningModel.displayName || pendingReasoningModel.name }) }),
674
- /* @__PURE__ */ jsx(Box, { flexDirection: "column", height: maxVisibleItems, marginTop: 1, children: Array.from({ length: maxVisibleItems }, (_, rowIndex) => {
675
- const option = visibleReasoningOptions[rowIndex];
676
- if (!option) {
677
- return /* @__PURE__ */ jsx(Box, { paddingX: 0, paddingY: 0, children: /* @__PURE__ */ jsx(Text, { children: " " }) }, `reasoning-empty-${rowIndex}`);
1036
+ /* @__PURE__ */ jsx(Box, { paddingX: 0, paddingY: 0, width: overlayWidth, children: /* @__PURE__ */ jsx(Text, { color: "gray", children: formatLineToWidth(
1037
+ pendingReasoningModel.displayName || pendingReasoningModel.name,
1038
+ overlayWidth
1039
+ ) }) }),
1040
+ /* @__PURE__ */ jsx(
1041
+ Box,
1042
+ {
1043
+ flexDirection: "column",
1044
+ height: maxVisibleItems,
1045
+ marginTop: 1,
1046
+ width: overlayWidth,
1047
+ children: Array.from({ length: maxVisibleItems }, (_, rowIndex) => {
1048
+ const option = visibleReasoningOptions[rowIndex];
1049
+ if (!option) {
1050
+ return /* @__PURE__ */ jsx(
1051
+ Box,
1052
+ {
1053
+ paddingX: 0,
1054
+ paddingY: 0,
1055
+ width: overlayWidth,
1056
+ children: /* @__PURE__ */ jsx(Text, { children: blankLine })
1057
+ },
1058
+ `reasoning-empty-${rowIndex}`
1059
+ );
1060
+ }
1061
+ const actualIndex = reasoningOffset + rowIndex;
1062
+ const isSelected = actualIndex === reasoningVariantIndex;
1063
+ return /* @__PURE__ */ jsx(Box, { paddingX: 0, paddingY: 0, width: overlayWidth, children: /* @__PURE__ */ jsx(Text, { color: isSelected ? "cyan" : "gray", bold: isSelected, children: formatLineToWidth(
1064
+ `${isSelected ? "\u203A" : " "} ${option.label}`,
1065
+ overlayWidth
1066
+ ) }) }, option.value);
1067
+ })
678
1068
  }
679
- const actualIndex = reasoningOffset + rowIndex;
680
- const isSelected = actualIndex === reasoningVariantIndex;
681
- return /* @__PURE__ */ jsx(Box, { paddingX: 0, paddingY: 0, children: /* @__PURE__ */ jsxs(
682
- Text,
683
- {
684
- color: isSelected ? "cyan" : "gray",
685
- bold: isSelected,
686
- wrap: "truncate-end",
687
- children: [
688
- isSelected ? "\u203A" : " ",
689
- " ",
690
- option.label
691
- ]
692
- }
693
- ) }, option.value);
694
- }) }),
695
- /* @__PURE__ */ jsx(Box, { paddingX: 0, paddingY: 0, marginTop: 1, children: /* @__PURE__ */ jsx(Text, { color: "gray", wrap: "truncate-end", children: selectedReasoningOption?.description ?? "" }) }),
696
- /* @__PURE__ */ jsx(Box, { paddingX: 0, paddingY: 0, children: /* @__PURE__ */ jsx(HintBar, { hints: ["\u2191\u2193 navigate", "Enter select", "Esc back"] }) })
1069
+ ),
1070
+ /* @__PURE__ */ jsx(Box, { paddingX: 0, paddingY: 0, marginTop: 1, width: overlayWidth, children: /* @__PURE__ */ jsx(Text, { color: "gray", children: formatLineToWidth(
1071
+ selectedReasoningOption?.description ?? "",
1072
+ overlayWidth
1073
+ ) }) }),
1074
+ /* @__PURE__ */ jsx(Box, { paddingX: 0, paddingY: 0, width: overlayWidth, children: /* @__PURE__ */ jsx(HintBar, { hints: ["\u2191\u2193 navigate", "Enter select", "Esc back"] }) })
697
1075
  ] });
698
1076
  }
699
1077
  const selectedIndex = selection.index;
700
1078
  const scrollOffset = selection.offset;
701
- const modelsOnly = filteredItems.filter(
702
- (item) => !isAddCustomOption(item)
703
- );
704
- const visibleModels = modelsOnly.slice(scrollOffset, scrollOffset + modelsViewportItems);
1079
+ const listItems = filteredItems.filter((item) => !isAddCustomOption(item));
1080
+ const visibleItems = listItems.slice(scrollOffset, scrollOffset + listViewportItems);
705
1081
  const selectedItem = filteredItems[selectedIndex];
706
- const hasActionableItems = selectedItem && !isAddCustomOption(selectedItem);
707
- let detailLine = "";
708
- if (isLoading) {
709
- detailLine = "Loading models\u2026";
710
- } else if (customModelAction === "delete" && pendingDeleteConfirm) {
711
- detailLine = "Confirm delete: press Enter again";
712
- } else if (customModelAction) {
713
- const label = customModelAction === "edit" ? "Edit" : customModelAction === "default" ? "Set as default" : "Delete";
714
- detailLine = `Action: ${label}`;
715
- } else if (searchQuery.trim() && filteredItems.length <= 1) {
716
- detailLine = "No models match your search";
717
- } else if (!selectedItem) {
718
- detailLine = "";
719
- } else if (isAddCustomOption(selectedItem)) {
720
- detailLine = "Enter to add a custom model";
721
- } else {
722
- const provider = getLLMProviderDisplayName(selectedItem.provider);
723
- const name = selectedItem.displayName || selectedItem.name;
724
- const flags = [];
725
- if (selectedItem.isDefault) flags.push("default");
726
- if (selectedItem.isCurrent) flags.push("current");
727
- detailLine = flags.length > 0 ? `${name} (${provider}) \u2022 ${flags.join(", ")}` : `${name} (${provider})`;
728
- }
729
- return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
730
- /* @__PURE__ */ jsx(Box, { paddingX: 0, paddingY: 0, children: /* @__PURE__ */ jsx(Text, { color: "cyan", bold: true, children: "Models" }) }),
731
- /* @__PURE__ */ jsxs(Box, { paddingX: 0, paddingY: 0, marginTop: 1, children: [
732
- /* @__PURE__ */ jsx(Text, { color: "gray", children: "Search: " }),
733
- /* @__PURE__ */ jsx(Text, { color: searchQuery ? "white" : "gray", wrap: "truncate-end", children: searchQuery || "Type to filter models\u2026" })
734
- ] }),
735
- /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
736
- /* @__PURE__ */ jsx(Box, { paddingX: 0, paddingY: 0, children: /* @__PURE__ */ jsxs(
1082
+ const hasActionableItems = Boolean(selectedItem && isModelOption(selectedItem));
1083
+ const searchLine = formatLineToWidth(
1084
+ `Search: ${searchQuery || "Type to filter models\u2026"}`,
1085
+ overlayWidth
1086
+ );
1087
+ const addCustomLine = hasAddCustomOption ? formatLineToWidth(
1088
+ `${getRowPrefix({
1089
+ isSelected: selectedIndex === 0,
1090
+ isDefault: false,
1091
+ isCurrent: false,
1092
+ isCustom: false,
1093
+ isFavorite: false
1094
+ })} Add custom model\u2026`,
1095
+ overlayWidth
1096
+ ) : "";
1097
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", width: overlayWidth, children: [
1098
+ /* @__PURE__ */ jsx(Box, { paddingX: 0, paddingY: 0, width: overlayWidth, children: /* @__PURE__ */ jsx(Text, { color: "cyan", bold: true, children: "Models" }) }),
1099
+ /* @__PURE__ */ jsx(Box, { paddingX: 0, paddingY: 0, width: overlayWidth, flexDirection: "row", children: MODEL_SELECTOR_TABS.map((tab) => /* @__PURE__ */ jsx(
1100
+ Box,
1101
+ {
1102
+ marginRight: 1,
1103
+ borderStyle: "round",
1104
+ borderColor: activeTab === tab.id ? "cyan" : "gray",
1105
+ paddingX: 1,
1106
+ children: /* @__PURE__ */ jsx(
1107
+ Text,
1108
+ {
1109
+ color: activeTab === tab.id ? "cyan" : "gray",
1110
+ bold: activeTab === tab.id,
1111
+ children: tab.label
1112
+ }
1113
+ )
1114
+ },
1115
+ tab.id
1116
+ )) }),
1117
+ /* @__PURE__ */ jsx(Box, { paddingX: 0, paddingY: 0, width: overlayWidth, children: /* @__PURE__ */ jsx(Text, { color: searchQuery ? "white" : "gray", children: searchLine }) }),
1118
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, width: overlayWidth, children: [
1119
+ hasAddCustomOption && /* @__PURE__ */ jsx(Box, { paddingX: 0, paddingY: 0, width: overlayWidth, children: /* @__PURE__ */ jsx(
737
1120
  Text,
738
1121
  {
739
1122
  color: selectedIndex === 0 ? "green" : "gray",
740
1123
  bold: selectedIndex === 0,
741
- wrap: "truncate-end",
742
- children: [
743
- getRowPrefix({
744
- isSelected: selectedIndex === 0,
745
- isDefault: false,
746
- isCurrent: false,
747
- isCustom: false
748
- }),
749
- " ",
750
- "Add custom model\u2026"
751
- ]
1124
+ children: addCustomLine
752
1125
  }
753
1126
  ) }),
754
- /* @__PURE__ */ jsx(Box, { flexDirection: "column", height: modelsViewportItems, children: isLoading || modelsOnly.length === 0 ? Array.from({ length: modelsViewportItems }, (_, index) => /* @__PURE__ */ jsx(Box, { paddingX: 0, paddingY: 0, children: /* @__PURE__ */ jsx(Text, { children: " " }) }, `model-empty-${index}`)) : Array.from({ length: modelsViewportItems }, (_, rowIndex) => {
755
- const item = visibleModels[rowIndex];
1127
+ /* @__PURE__ */ jsx(Box, { flexDirection: "column", height: listViewportItems, width: overlayWidth, children: isLoading || listItems.length === 0 ? Array.from({ length: listViewportItems }, (_, index) => /* @__PURE__ */ jsx(
1128
+ Box,
1129
+ {
1130
+ paddingX: 0,
1131
+ paddingY: 0,
1132
+ width: overlayWidth,
1133
+ children: /* @__PURE__ */ jsx(Text, { children: blankLine })
1134
+ },
1135
+ `model-empty-${index}`
1136
+ )) : Array.from({ length: listViewportItems }, (_, rowIndex) => {
1137
+ const item = visibleItems[rowIndex];
756
1138
  if (!item) {
757
1139
  return /* @__PURE__ */ jsx(
758
1140
  Box,
759
1141
  {
760
1142
  paddingX: 0,
761
1143
  paddingY: 0,
762
- children: /* @__PURE__ */ jsx(Text, { children: " " })
1144
+ width: overlayWidth,
1145
+ children: /* @__PURE__ */ jsx(Text, { children: blankLine })
763
1146
  },
764
1147
  `model-empty-${rowIndex}`
765
1148
  );
766
1149
  }
767
- const actualIndex = 1 + scrollOffset + rowIndex;
1150
+ const actualIndex = modelStartIndex + scrollOffset + rowIndex;
768
1151
  const isSelected = actualIndex === selectedIndex;
769
1152
  const providerDisplay = getLLMProviderDisplayName(item.provider);
770
1153
  const name = item.displayName || item.name;
1154
+ const isFavorite = favoriteKeySet.has(
1155
+ toModelPickerKey({
1156
+ provider: item.provider,
1157
+ model: item.name
1158
+ })
1159
+ );
771
1160
  const prefix = getRowPrefix({
772
1161
  isSelected,
773
1162
  isDefault: item.isDefault,
774
1163
  isCurrent: item.isCurrent,
775
- isCustom: item.isCustom
1164
+ isCustom: item.isCustom,
1165
+ isFavorite
776
1166
  });
777
- return /* @__PURE__ */ jsxs(
1167
+ return /* @__PURE__ */ jsx(
778
1168
  Box,
779
1169
  {
780
1170
  flexDirection: "row",
781
1171
  paddingX: 0,
782
1172
  paddingY: 0,
783
- children: [
784
- /* @__PURE__ */ jsx(Box, { flexGrow: 1, children: /* @__PURE__ */ jsxs(
785
- Text,
786
- {
787
- color: isSelected ? "cyan" : "gray",
788
- bold: isSelected,
789
- wrap: "truncate-end",
790
- children: [
791
- prefix,
792
- " ",
793
- name,
794
- " (",
795
- providerDisplay,
796
- ")"
797
- ]
798
- }
799
- ) }),
800
- isSelected && /* @__PURE__ */ jsxs(Box, { flexDirection: "row", marginLeft: 1, children: [
801
- item.isCustom && /* @__PURE__ */ jsxs(Fragment, { children: [
802
- /* @__PURE__ */ jsxs(
803
- Text,
804
- {
805
- color: customModelAction === "edit" ? "green" : "gray",
806
- bold: customModelAction === "edit",
807
- inverse: customModelAction === "edit",
808
- children: [
809
- " ",
810
- "Edit",
811
- " "
812
- ]
813
- }
814
- ),
815
- /* @__PURE__ */ jsx(Text, { children: " " })
816
- ] }),
817
- /* @__PURE__ */ jsxs(
818
- Text,
819
- {
820
- color: customModelAction === "default" ? "cyan" : "gray",
821
- bold: customModelAction === "default",
822
- inverse: customModelAction === "default",
823
- children: [
824
- " ",
825
- "Set as Default",
826
- " "
827
- ]
828
- }
829
- ),
830
- item.isCustom && /* @__PURE__ */ jsxs(Fragment, { children: [
831
- /* @__PURE__ */ jsx(Text, { children: " " }),
832
- /* @__PURE__ */ jsxs(
833
- Text,
834
- {
835
- color: customModelAction === "delete" ? "red" : "gray",
836
- bold: customModelAction === "delete",
837
- inverse: customModelAction === "delete",
838
- children: [
839
- " ",
840
- "Delete",
841
- " "
842
- ]
843
- }
844
- )
845
- ] })
846
- ] })
847
- ]
1173
+ width: overlayWidth,
1174
+ children: /* @__PURE__ */ jsx(Text, { color: isSelected ? "cyan" : "gray", bold: isSelected, children: formatLineToWidth(
1175
+ `${prefix} ${name} (${providerDisplay})`,
1176
+ overlayWidth
1177
+ ) })
848
1178
  },
849
- `${item.provider}-${item.name}-${item.isCustom ? "custom" : "registry"}`
1179
+ `model-${activeTab}-${actualIndex}-${toModelIdentityKey(item)}`
850
1180
  );
851
1181
  }) })
852
1182
  ] }),
853
- /* @__PURE__ */ jsx(Box, { paddingX: 0, paddingY: 0, marginTop: 1, children: /* @__PURE__ */ jsx(Text, { color: "gray", wrap: "truncate-end", children: detailLine }) }),
854
- /* @__PURE__ */ jsx(Box, { paddingX: 0, paddingY: 0, children: /* @__PURE__ */ jsx(
1183
+ /* @__PURE__ */ jsx(Box, { paddingX: 0, paddingY: 0, marginTop: 1, width: overlayWidth, children: /* @__PURE__ */ jsx(
855
1184
  HintBar,
856
1185
  {
857
1186
  hints: [
858
1187
  "\u2191\u2193 navigate",
859
- "Enter select",
1188
+ "Enter select/apply",
860
1189
  "Esc close",
861
- hasActionableItems ? "\u2190\u2192 actions" : ""
1190
+ "\u2190\u2192 switch tab",
1191
+ hasActionableItems ? "Ctrl+F quick favorite" : ""
862
1192
  ]
863
1193
  }
864
1194
  ) })