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