free-coding-models 0.3.21 → 0.3.23

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.
@@ -1,64 +1,295 @@
1
1
  /**
2
2
  * @file command-palette.js
3
3
  * @description Command palette registry and fuzzy search helpers for the main TUI.
4
+ * Now supports hierarchical categories with expandable/collapsible groups.
4
5
  *
5
6
  * @functions
6
- * → `buildCommandPaletteEntries` — builds the current command list with dynamic provider/tier context
7
+ * → `buildCommandPaletteTree` — builds the hierarchical command tree with categories and subcategories
8
+ * → `flattenCommandTree` — converts tree to flat list for filtering (respects expansion state)
7
9
  * → `fuzzyMatchCommand` — scores a query against one string and returns match positions
8
10
  * → `filterCommandPaletteEntries` — returns sorted command matches for a query
9
11
  *
10
- * @exports { COMMAND_CATEGORY_ORDER, buildCommandPaletteEntries, fuzzyMatchCommand, filterCommandPaletteEntries }
12
+ * @exports { buildCommandPaletteTree, flattenCommandTree, fuzzyMatchCommand, filterCommandPaletteEntries }
11
13
  *
12
14
  * @see src/key-handler.js
13
15
  * @see src/overlays.js
14
16
  */
15
17
 
16
- export const COMMAND_CATEGORY_ORDER = ['Filters', 'Sort', 'Pages', 'Actions']
17
-
18
- const COMMANDS = [
19
- // 📖 Filters
20
- { id: 'filter-tier-all', category: 'Filters', label: 'Filter tiers: all', shortcut: 'T', keywords: ['filter', 'tier', 'all'] },
21
- { id: 'filter-tier-splus', category: 'Filters', label: 'Filter tiers: S+', shortcut: null, keywords: ['filter', 'tier', 's+'] },
22
- { id: 'filter-tier-s', category: 'Filters', label: 'Filter tiers: S', shortcut: null, keywords: ['filter', 'tier', 's'] },
23
- { id: 'filter-tier-aplus', category: 'Filters', label: 'Filter tiers: A+', shortcut: null, keywords: ['filter', 'tier', 'a+'] },
24
- { id: 'filter-tier-a', category: 'Filters', label: 'Filter tiers: A', shortcut: null, keywords: ['filter', 'tier', 'a'] },
25
- { id: 'filter-tier-aminus', category: 'Filters', label: 'Filter tiers: A-', shortcut: null, keywords: ['filter', 'tier', 'a-'] },
26
- { id: 'filter-tier-bplus', category: 'Filters', label: 'Filter tiers: B+', shortcut: null, keywords: ['filter', 'tier', 'b+'] },
27
- { id: 'filter-tier-b', category: 'Filters', label: 'Filter tiers: B', shortcut: null, keywords: ['filter', 'tier', 'b'] },
28
- { id: 'filter-tier-c', category: 'Filters', label: 'Filter tiers: C', shortcut: null, keywords: ['filter', 'tier', 'c'] },
29
- { id: 'filter-provider-cycle', category: 'Filters', label: 'Filter provider: cycle', shortcut: 'D', keywords: ['filter', 'provider', 'origin'] },
30
- { id: 'filter-configured-toggle', category: 'Filters', label: 'Toggle configured-only models', shortcut: 'E', keywords: ['filter', 'configured', 'keys'] },
31
-
32
- // 📖 Sorting
33
- { id: 'sort-rank', category: 'Sort', label: 'Sort by rank', shortcut: 'R', keywords: ['sort', 'rank'] },
34
- { id: 'sort-tier', category: 'Sort', label: 'Sort by tier', shortcut: null, keywords: ['sort', 'tier'] },
35
- { id: 'sort-provider', category: 'Sort', label: 'Sort by provider', shortcut: 'O', keywords: ['sort', 'origin', 'provider'] },
36
- { id: 'sort-model', category: 'Sort', label: 'Sort by model name', shortcut: 'M', keywords: ['sort', 'model', 'name'] },
37
- { id: 'sort-latest-ping', category: 'Sort', label: 'Sort by latest ping', shortcut: 'L', keywords: ['sort', 'latest', 'ping'] },
38
- { id: 'sort-avg-ping', category: 'Sort', label: 'Sort by average ping', shortcut: 'A', keywords: ['sort', 'avg', 'average', 'ping'] },
39
- { id: 'sort-swe', category: 'Sort', label: 'Sort by SWE score', shortcut: 'S', keywords: ['sort', 'swe', 'score'] },
40
- { id: 'sort-ctx', category: 'Sort', label: 'Sort by context window', shortcut: 'C', keywords: ['sort', 'context', 'ctx'] },
41
- { id: 'sort-health', category: 'Sort', label: 'Sort by health', shortcut: 'H', keywords: ['sort', 'health', 'condition'] },
42
- { id: 'sort-verdict', category: 'Sort', label: 'Sort by verdict', shortcut: 'V', keywords: ['sort', 'verdict'] },
43
- { id: 'sort-stability', category: 'Sort', label: 'Sort by stability', shortcut: 'B', keywords: ['sort', 'stability'] },
44
- { id: 'sort-uptime', category: 'Sort', label: 'Sort by uptime', shortcut: 'U', keywords: ['sort', 'uptime'] },
45
-
46
- // 📖 Pages / overlays
47
- { id: 'open-settings', category: 'Pages', label: 'Open settings', shortcut: 'P', keywords: ['settings', 'config', 'api key'] },
48
- { id: 'open-help', category: 'Pages', label: 'Open help', shortcut: 'K', keywords: ['help', 'shortcuts', 'hotkeys'] },
49
- { id: 'open-changelog', category: 'Pages', label: 'Open changelog', shortcut: 'N', keywords: ['changelog', 'release'] },
50
- { id: 'open-feedback', category: 'Pages', label: 'Open feedback', shortcut: 'I', keywords: ['feedback', 'bug', 'request'] },
51
- { id: 'open-recommend', category: 'Pages', label: 'Open smart recommend', shortcut: 'Q', keywords: ['recommend', 'best model'] },
52
- { id: 'open-install-endpoints', category: 'Pages', label: 'Open install endpoints', shortcut: 'Y', keywords: ['install', 'endpoints', 'providers'] },
53
-
54
- // 📖 Actions
55
- { id: 'action-cycle-theme', category: 'Actions', label: 'Cycle theme', shortcut: 'G', keywords: ['theme', 'dark', 'light', 'auto'] },
56
- { id: 'action-cycle-tool-mode', category: 'Actions', label: 'Cycle tool mode', shortcut: 'Z', keywords: ['tool', 'mode', 'launcher'] },
57
- { id: 'action-cycle-ping-mode', category: 'Actions', label: 'Cycle ping mode', shortcut: 'W', keywords: ['ping', 'cadence', 'speed', 'slow'] },
58
- { id: 'action-toggle-favorite', category: 'Actions', label: 'Toggle favorite on selected model', shortcut: 'F', keywords: ['favorite', 'star'] },
59
- { id: 'action-reset-view', category: 'Actions', label: 'Reset view settings', shortcut: 'Shift+R', keywords: ['reset', 'view', 'sort', 'filters'] },
18
+ import { TOOL_METADATA, TOOL_MODE_ORDER } from './tool-metadata.js'
19
+
20
+ const TOOL_MODE_DESCRIPTIONS = {
21
+ opencode: 'Launch in OpenCode CLI with the selected model.',
22
+ 'opencode-desktop': 'Set model in shared config, then open OpenCode Desktop.',
23
+ openclaw: 'Set default model in OpenClaw and launch it.',
24
+ crush: 'Launch Crush with this provider/model pair.',
25
+ goose: 'Launch Goose and preselect the active model.',
26
+ pi: 'Launch Pi with model/provider flags.',
27
+ aider: 'Launch Aider configured on the selected model.',
28
+ qwen: 'Launch Qwen Code using the selected provider model.',
29
+ openhands: 'Launch OpenHands with the selected model endpoint.',
30
+ amp: 'Launch Amp with this model as active target.',
31
+ }
32
+
33
+ const TOOL_MODE_COMMANDS = TOOL_MODE_ORDER.map((toolMode) => {
34
+ const meta = TOOL_METADATA[toolMode] || { label: toolMode, emoji: '🧰' }
35
+ return {
36
+ id: `action-set-tool-${toolMode}`,
37
+ label: meta.label,
38
+ toolMode,
39
+ icon: meta.emoji,
40
+ description: TOOL_MODE_DESCRIPTIONS[toolMode] || 'Set this as the active launch target.',
41
+ keywords: ['tool', 'target', 'mode', toolMode, meta.label.toLowerCase()],
42
+ }
43
+ })
44
+
45
+ const PING_MODE_COMMANDS = [
46
+ {
47
+ id: 'action-cycle-ping-mode',
48
+ label: 'Cycle ping mode',
49
+ shortcut: 'W',
50
+ icon: '',
51
+ description: 'Rotate speed normal slow forced.',
52
+ keywords: ['ping', 'mode', 'cycle', 'speed', 'normal', 'slow', 'forced'],
53
+ },
54
+ {
55
+ id: 'action-set-ping-speed',
56
+ label: 'Speed mode (2s)',
57
+ pingMode: 'speed',
58
+ description: 'Fast 2s bursts for short live checks.',
59
+ keywords: ['ping', 'mode', 'speed', '2s', 'fast'],
60
+ },
61
+ {
62
+ id: 'action-set-ping-normal',
63
+ label: 'Normal mode (10s)',
64
+ pingMode: 'normal',
65
+ description: 'Balanced default cadence for daily use.',
66
+ keywords: ['ping', 'mode', 'normal', '10s', 'default'],
67
+ },
68
+ {
69
+ id: 'action-set-ping-slow',
70
+ label: 'Slow mode (30s)',
71
+ pingMode: 'slow',
72
+ description: 'Lower refresh cost when you are mostly idle.',
73
+ keywords: ['ping', 'mode', 'slow', '30s', 'idle'],
74
+ },
75
+ {
76
+ id: 'action-set-ping-forced',
77
+ label: 'Forced mode (4s)',
78
+ pingMode: 'forced',
79
+ description: 'Keeps 4s cadence until manually changed.',
80
+ keywords: ['ping', 'mode', 'forced', '4s', 'manual'],
81
+ },
60
82
  ]
61
83
 
84
+ // 📖 Base command tree template (will be enhanced with dynamic model list)
85
+ const BASE_COMMAND_TREE = [
86
+ {
87
+ id: 'filters',
88
+ label: 'Filters',
89
+ icon: '🔍',
90
+ children: [
91
+ {
92
+ id: 'filter-tier',
93
+ label: 'Filter by tier',
94
+ icon: '📊',
95
+ children: [
96
+ { id: 'filter-tier-all', label: 'All tiers', tier: null, shortcut: 'T', description: 'Show all models', keywords: ['filter', 'tier', 'all'] },
97
+ { id: 'filter-tier-splus', label: 'S+ tier', tier: 'S+', description: 'Best coding models', keywords: ['filter', 'tier', 's+'] },
98
+ { id: 'filter-tier-s', label: 'S tier', tier: 'S', description: 'Excellent models', keywords: ['filter', 'tier', 's'] },
99
+ { id: 'filter-tier-aplus', label: 'A+ tier', tier: 'A+', description: 'Very good models', keywords: ['filter', 'tier', 'a+'] },
100
+ { id: 'filter-tier-a', label: 'A tier', tier: 'A', description: 'Good models', keywords: ['filter', 'tier', 'a'] },
101
+ { id: 'filter-tier-aminus', label: 'A- tier', tier: 'A-', description: 'Solid models', keywords: ['filter', 'tier', 'a-'] },
102
+ { id: 'filter-tier-bplus', label: 'B+ tier', tier: 'B+', description: 'Fair models', keywords: ['filter', 'tier', 'b+'] },
103
+ { id: 'filter-tier-b', label: 'B tier', tier: 'B', description: 'Basic models', keywords: ['filter', 'tier', 'b'] },
104
+ { id: 'filter-tier-c', label: 'C tier', tier: 'C', description: 'Limited models', keywords: ['filter', 'tier', 'c'] },
105
+ ]
106
+ },
107
+ {
108
+ id: 'filter-provider',
109
+ label: 'Filter by provider',
110
+ icon: '🏢',
111
+ children: [
112
+ { id: 'filter-provider-cycle', label: 'Cycle provider', shortcut: 'D', description: 'Switch between providers', keywords: ['filter', 'provider', 'origin'] },
113
+ { id: 'filter-provider-all', label: 'All providers', providerKey: null, description: 'Show all providers', keywords: ['filter', 'provider', 'all'] },
114
+ { id: 'filter-provider-nvidia', label: 'NVIDIA NIM', providerKey: 'nvidiaNim', description: 'NVIDIA models', keywords: ['filter', 'provider', 'nvidia', 'nim'] },
115
+ { id: 'filter-provider-groq', label: 'Groq', providerKey: 'groq', description: 'Groq models', keywords: ['filter', 'provider', 'groq'] },
116
+ { id: 'filter-provider-cerebras', label: 'Cerebras', providerKey: 'cerebras', description: 'Cerebras models', keywords: ['filter', 'provider', 'cerebras'] },
117
+ { id: 'filter-provider-sambanova', label: 'SambaNova', providerKey: 'sambanova', description: 'SambaNova models', keywords: ['filter', 'provider', 'sambanova'] },
118
+ { id: 'filter-provider-openrouter', label: 'OpenRouter', providerKey: 'openrouter', description: 'OpenRouter models', keywords: ['filter', 'provider', 'openrouter'] },
119
+ { id: 'filter-provider-together', label: 'Together AI', providerKey: 'together', description: 'Together models', keywords: ['filter', 'provider', 'together'] },
120
+ { id: 'filter-provider-deepinfra', label: 'DeepInfra', providerKey: 'deepinfra', description: 'DeepInfra models', keywords: ['filter', 'provider', 'deepinfra'] },
121
+ { id: 'filter-provider-fireworks', label: 'Fireworks AI', providerKey: 'fireworks', description: 'Fireworks models', keywords: ['filter', 'provider', 'fireworks'] },
122
+ { id: 'filter-provider-hyperbolic', label: 'Hyperbolic', providerKey: 'hyperbolic', description: 'Hyperbolic models', keywords: ['filter', 'provider', 'hyperbolic'] },
123
+ { id: 'filter-provider-google', label: 'Google AI', providerKey: 'google', description: 'Google models', keywords: ['filter', 'provider', 'google'] },
124
+ { id: 'filter-provider-huggingface', label: 'Hugging Face', providerKey: 'huggingface', description: 'Hugging Face models', keywords: ['filter', 'provider', 'huggingface'] },
125
+ ]
126
+ },
127
+ {
128
+ id: 'filter-model',
129
+ label: 'Filter by model',
130
+ icon: '🤖',
131
+ children: []
132
+ },
133
+ {
134
+ id: 'filter-other',
135
+ label: 'Other filters',
136
+ icon: '⚙️',
137
+ children: [
138
+ { id: 'filter-configured-toggle', label: 'Toggle configured-only', shortcut: 'E', description: 'Show only configured providers', keywords: ['filter', 'configured', 'keys'] },
139
+ ]
140
+ },
141
+ ]
142
+ },
143
+ {
144
+ id: 'sort',
145
+ label: 'Sort',
146
+ icon: '📶',
147
+ children: [
148
+ { id: 'sort-rank', label: 'Sort by rank', shortcut: 'R', description: 'Rank by SWE score', keywords: ['sort', 'rank'] },
149
+ { id: 'sort-tier', label: 'Sort by tier', description: 'Group by quality tier', keywords: ['sort', 'tier'] },
150
+ { id: 'sort-provider', label: 'Sort by provider', shortcut: 'O', description: 'Group by provider', keywords: ['sort', 'origin', 'provider'] },
151
+ { id: 'sort-model', label: 'Sort by model', shortcut: 'M', description: 'Alphabetical order', keywords: ['sort', 'model', 'name'] },
152
+ { id: 'sort-latest-ping', label: 'Sort by latest ping', shortcut: 'L', description: 'Recent response time', keywords: ['sort', 'latest', 'ping'] },
153
+ { id: 'sort-avg-ping', label: 'Sort by avg ping', shortcut: 'A', description: 'Average response time', keywords: ['sort', 'avg', 'average', 'ping'] },
154
+ { id: 'sort-swe', label: 'Sort by SWE score', shortcut: 'S', description: 'Coding ability score', keywords: ['sort', 'swe', 'score'] },
155
+ { id: 'sort-ctx', label: 'Sort by context', shortcut: 'C', description: 'Context window size', keywords: ['sort', 'context', 'ctx'] },
156
+ { id: 'sort-health', label: 'Sort by health', shortcut: 'H', description: 'Current model status', keywords: ['sort', 'health', 'condition'] },
157
+ { id: 'sort-verdict', label: 'Sort by verdict', shortcut: 'V', description: 'Overall assessment', keywords: ['sort', 'verdict'] },
158
+ { id: 'sort-stability', label: 'Sort by stability', shortcut: 'B', description: 'Reliability score', keywords: ['sort', 'stability'] },
159
+ { id: 'sort-uptime', label: 'Sort by uptime', shortcut: 'U', description: 'Success rate', keywords: ['sort', 'uptime'] },
160
+ ]
161
+ },
162
+ {
163
+ id: 'actions',
164
+ label: 'Actions',
165
+ icon: '⚙️',
166
+ children: [
167
+ {
168
+ id: 'action-target-tool',
169
+ label: 'Target tool',
170
+ icon: '🧰',
171
+ children: [
172
+ { id: 'action-cycle-tool-mode', label: 'Cycle target tool', shortcut: 'Z', icon: '🔄', description: 'Rotate through every launcher mode.', keywords: ['tool', 'mode', 'cycle', 'target'] },
173
+ ...TOOL_MODE_COMMANDS,
174
+ ],
175
+ },
176
+ {
177
+ id: 'action-ping-mode',
178
+ label: 'Ping mode',
179
+ icon: '📶',
180
+ children: PING_MODE_COMMANDS,
181
+ },
182
+ {
183
+ id: 'action-favorites-mode',
184
+ label: 'Favorites mode',
185
+ icon: '⭐',
186
+ children: [
187
+ { id: 'action-toggle-favorite-mode', label: 'Toggle favorites mode', shortcut: 'Y', icon: '⭐', description: 'Switch pinned+sticky ↔ normal list behavior.', keywords: ['favorite', 'favorites', 'mode', 'toggle', 'y'] },
188
+ { id: 'action-favorites-mode-pinned', label: 'Pinned + always visible', favoritesPinned: true, description: 'Favorites stay on top and bypass current filters.', keywords: ['favorite', 'favorites', 'pinned', 'sticky', 'always visible'] },
189
+ { id: 'action-favorites-mode-normal', label: 'Normal rows (starred only)', favoritesPinned: false, description: 'Favorites keep ⭐ but follow active filters and sort.', keywords: ['favorite', 'favorites', 'normal', 'sort', 'filter'] },
190
+ { id: 'action-toggle-favorite', label: 'Toggle favorite on selected row', shortcut: 'F', icon: '⭐', description: 'Star/unstar the highlighted model.', keywords: ['favorite', 'star', 'toggle'] },
191
+ ],
192
+ },
193
+ { id: 'action-cycle-theme', label: 'Cycle theme', shortcut: 'G', icon: '🌗', description: 'Switch dark/light/auto', keywords: ['theme', 'dark', 'light', 'auto'] },
194
+ { id: 'action-reset-view', label: 'Reset view', shortcut: 'Shift+R', icon: '🔄', description: 'Reset filters and sort', keywords: ['reset', 'view', 'sort', 'filters'] },
195
+ ],
196
+ },
197
+ // 📖 Pages - directly at root level, not in submenu
198
+ { id: 'open-settings', label: 'Settings', shortcut: 'P', icon: '⚙️', type: 'page', description: 'API keys and preferences', keywords: ['settings', 'config', 'api key'] },
199
+ { id: 'open-help', label: 'Help', shortcut: 'K', icon: '❓', type: 'page', description: 'Show all shortcuts', keywords: ['help', 'shortcuts', 'hotkeys'] },
200
+ { id: 'open-changelog', label: 'Changelog', shortcut: 'N', icon: '📋', type: 'page', description: 'Version history', keywords: ['changelog', 'release'] },
201
+ { id: 'open-feedback', label: 'Feedback', shortcut: 'I', icon: '📝', type: 'page', description: 'Report bugs or requests', keywords: ['feedback', 'bug', 'request'] },
202
+ { id: 'open-recommend', label: 'Smart recommend', shortcut: 'Q', icon: '🎯', type: 'page', description: 'Find best model for task', keywords: ['recommend', 'best model'] },
203
+ { id: 'open-install-endpoints', label: 'Install endpoints', icon: '🔌', type: 'page', description: 'Install provider catalogs', keywords: ['install', 'endpoints', 'providers'] },
204
+ ]
205
+
206
+ /**
207
+ * 📖 Build the command palette tree with dynamic model filters.
208
+ * @param {Array} visibleModels - Optional list of visible models to create model filter entries
209
+ * @returns {Array} The command tree with model filters added
210
+ */
211
+ export function buildCommandPaletteTree(visibleModels = []) {
212
+ // 📖 Clone the base tree
213
+ const tree = JSON.parse(JSON.stringify(BASE_COMMAND_TREE))
214
+
215
+ // 📖 Find the filter-model category and add dynamic model entries
216
+ const filterModelCategory = tree.find(cat => cat.id === 'filters')
217
+ ?.children.find(sub => sub.id === 'filter-model')
218
+
219
+ if (filterModelCategory && Array.isArray(visibleModels) && visibleModels.length > 0) {
220
+ // 📖 Add top 20 most-used or most relevant models
221
+ const topModels = visibleModels
222
+ .filter(m => !m.hidden && m.status !== 'noauth')
223
+ .slice(0, 20)
224
+
225
+ for (const model of topModels) {
226
+ filterModelCategory.children.push({
227
+ id: `filter-model-${model.providerKey}-${model.modelId}`,
228
+ label: model.label,
229
+ modelId: model.modelId,
230
+ providerKey: model.providerKey,
231
+ keywords: ['filter', 'model', model.label.toLowerCase(), model.modelId.toLowerCase()],
232
+ })
233
+ }
234
+ }
235
+
236
+ return tree
237
+ }
238
+
239
+ /**
240
+ * 📖 Flatten the command tree into a list, respecting which nodes are expanded.
241
+ * @param {Array} tree - The command tree
242
+ * @param {Set} expandedIds - Set of IDs that are expanded
243
+ * @returns {Array} Flat list with type markers ('category' | 'subcategory' | 'command' | 'page' | 'action')
244
+ */
245
+ export function flattenCommandTree(tree, expandedIds = new Set()) {
246
+ const result = []
247
+
248
+ function traverse(nodes, depth = 0) {
249
+ for (const node of nodes) {
250
+ // 📖 Check if this is a direct page/action (not in a submenu)
251
+ if (node.type === 'page' || node.type === 'action') {
252
+ result.push({
253
+ ...node,
254
+ type: node.type,
255
+ depth: 0,
256
+ hasChildren: false,
257
+ isExpanded: false,
258
+ })
259
+ continue
260
+ }
261
+
262
+ const isExpanded = expandedIds.has(node.id)
263
+ const hasChildren = Array.isArray(node.children) && node.children.length > 0
264
+
265
+ if (hasChildren) {
266
+ result.push({
267
+ ...node,
268
+ type: depth === 0 ? 'category' : 'subcategory',
269
+ depth,
270
+ hasChildren,
271
+ isExpanded,
272
+ })
273
+
274
+ if (isExpanded) {
275
+ traverse(node.children, depth + 1)
276
+ }
277
+ } else {
278
+ result.push({
279
+ ...node,
280
+ type: 'command',
281
+ depth,
282
+ hasChildren: false,
283
+ isExpanded: false,
284
+ })
285
+ }
286
+ }
287
+ }
288
+
289
+ traverse(tree)
290
+ return result
291
+ }
292
+
62
293
  const ID_TO_TIER = {
63
294
  'filter-tier-all': null,
64
295
  'filter-tier-splus': 'S+',
@@ -71,11 +302,31 @@ const ID_TO_TIER = {
71
302
  'filter-tier-c': 'C',
72
303
  }
73
304
 
74
- export function buildCommandPaletteEntries() {
75
- return COMMANDS.map((entry) => ({
76
- ...entry,
77
- tierValue: Object.prototype.hasOwnProperty.call(ID_TO_TIER, entry.id) ? ID_TO_TIER[entry.id] : undefined,
78
- }))
305
+ /**
306
+ * 📖 Legacy function for backward compatibility - builds flat list from tree.
307
+ * 📖 Expands all categories so every command is searchable by fuzzyMatchCommand.
308
+ * @param {Array} visibleModels - Optional list of visible models for model filter entries
309
+ */
310
+ export function buildCommandPaletteEntries(visibleModels = []) {
311
+ const tree = buildCommandPaletteTree(visibleModels)
312
+ // 📖 Collect every node id that has children so flattenCommandTree traverses into them.
313
+ const allIds = new Set()
314
+ function collectIds(nodes) {
315
+ for (const n of nodes) {
316
+ allIds.add(n.id)
317
+ if (Array.isArray(n.children)) collectIds(n.children)
318
+ }
319
+ }
320
+ collectIds(tree)
321
+ const flat = flattenCommandTree(tree, allIds)
322
+ return flat.map((entry) => {
323
+ // 📖 Copy tier and providerKey properties to tierValue for backward compatibility
324
+ const result = { ...entry }
325
+ if (entry.tier !== undefined) {
326
+ result.tierValue = entry.tier
327
+ }
328
+ return result
329
+ })
79
330
  }
80
331
 
81
332
  /**
@@ -126,15 +377,20 @@ export function fuzzyMatchCommand(query, text) {
126
377
 
127
378
  /**
128
379
  * 📖 Filter and rank command palette entries by fuzzy score.
129
- * @param {Array<{ id: string, label: string, category: string, keywords?: string[] }>} entries
380
+ * Now handles hierarchical structure with expandable categories.
381
+ * @param {Array} flatEntries - Flattened command tree entries
130
382
  * @param {string} query
131
- * @returns {Array<{ id: string, label: string, category: string, shortcut?: string|null, keywords?: string[], score: number, matchPositions: number[] }>}
383
+ * @returns {Array} Sorted and filtered entries with match scores
132
384
  */
133
- export function filterCommandPaletteEntries(entries, query) {
385
+ export function filterCommandPaletteEntries(flatEntries, query) {
134
386
  const normalizedQuery = (query || '').trim()
387
+
388
+ if (!normalizedQuery) {
389
+ return flatEntries
390
+ }
135
391
 
136
392
  const ranked = []
137
- for (const entry of entries) {
393
+ for (const entry of flatEntries) {
138
394
  const labelMatch = fuzzyMatchCommand(normalizedQuery, entry.label)
139
395
  let bestScore = labelMatch.score
140
396
  let matchPositions = labelMatch.positions
@@ -145,7 +401,6 @@ export function filterCommandPaletteEntries(entries, query) {
145
401
  const keywordMatch = fuzzyMatchCommand(normalizedQuery, keyword)
146
402
  if (!keywordMatch.matched) continue
147
403
  matched = true
148
- // 📖 Keyword matches should rank below direct label matches.
149
404
  const keywordScore = Math.max(1, keywordMatch.score - 7)
150
405
  if (keywordScore > bestScore) {
151
406
  bestScore = keywordScore
@@ -158,11 +413,24 @@ export function filterCommandPaletteEntries(entries, query) {
158
413
  ranked.push({ ...entry, score: bestScore, matchPositions })
159
414
  }
160
415
 
416
+ // Auto-expand categories that contain matches
417
+ const result = []
418
+ const idsToExpand = new Set()
419
+
420
+ // First pass: mark all categories containing matched items
421
+ for (const entry of ranked) {
422
+ if (entry.type === 'command' && entry.matchPositions) {
423
+ // Find parent categories
424
+ let current = result.find(r => r.id === entry.id)
425
+ if (current) {
426
+ idsToExpand.add(entry.parentId)
427
+ }
428
+ }
429
+ }
430
+
161
431
  ranked.sort((a, b) => {
162
432
  if (b.score !== a.score) return b.score - a.score
163
- const aCat = COMMAND_CATEGORY_ORDER.indexOf(a.category)
164
- const bCat = COMMAND_CATEGORY_ORDER.indexOf(b.category)
165
- if (aCat !== bCat) return aCat - bCat
433
+ if (a.depth !== b.depth) return a.depth - b.depth
166
434
  return a.label.localeCompare(b.label)
167
435
  })
168
436
 
package/src/config.js CHANGED
@@ -209,6 +209,7 @@ function normalizeSettingsSection(settings) {
209
209
  return {
210
210
  ...safeSettings,
211
211
  hideUnconfiguredModels: typeof safeSettings.hideUnconfiguredModels === 'boolean' ? safeSettings.hideUnconfiguredModels : true,
212
+ favoritesPinnedAndSticky: typeof safeSettings.favoritesPinnedAndSticky === 'boolean' ? safeSettings.favoritesPinnedAndSticky : false,
212
213
  theme: ['dark', 'light', 'auto'].includes(safeSettings.theme) ? safeSettings.theme : 'auto',
213
214
  }
214
215
  }
@@ -831,7 +832,7 @@ export function isProviderEnabled(config, providerKey) {
831
832
  /**
832
833
  * 📖 _emptyProfileSettings: Default TUI settings.
833
834
  *
834
- * @returns {{ tierFilter: string|null, sortColumn: string, sortAsc: boolean, pingInterval: number, hideUnconfiguredModels: boolean, preferredToolMode: string }}
835
+ * @returns {{ tierFilter: string|null, sortColumn: string, sortAsc: boolean, pingInterval: number, hideUnconfiguredModels: boolean, favoritesPinnedAndSticky: boolean, preferredToolMode: string }}
835
836
  */
836
837
  export function _emptyProfileSettings() {
837
838
  return {
@@ -840,6 +841,7 @@ export function _emptyProfileSettings() {
840
841
  sortAsc: true, // 📖 true = ascending (fastest first for latency)
841
842
  pingInterval: 10000, // 📖 default ms between pings in the steady "normal" mode
842
843
  hideUnconfiguredModels: true, // 📖 true = default to providers that are actually configured
844
+ favoritesPinnedAndSticky: false, // 📖 default mode keeps favorites as normal starred rows; press Y to pin+stick them.
843
845
  preferredToolMode: 'opencode', // 📖 remember the last Z-selected launcher across app restarts
844
846
  theme: 'auto', // 📖 'auto' follows the terminal/OS theme, override with 'dark' or 'light' if needed
845
847
  }
@@ -848,7 +850,7 @@ export function _emptyProfileSettings() {
848
850
  /**
849
851
  * 📖 normalizeEndpointInstalls keeps the endpoint-install tracking list safe to replay.
850
852
  *
851
- * 📖 Each entry represents one managed catalog install performed through the `Y` flow:
853
+ * 📖 Each entry represents one managed catalog install performed through Install Endpoints:
852
854
  * - `providerKey`: FCM provider identifier (`nvidia`, `groq`, ...)
853
855
  * - `toolMode`: canonical tool id (`opencode`, `openclaw`, `crush`, `goose`)
854
856
  * - `scope`: `all` or `selected`
@@ -3,7 +3,7 @@
3
3
  * @description Install and refresh FCM-managed provider catalogs inside external tool configs.
4
4
  *
5
5
  * @details
6
- * 📖 This module powers the `Y` hotkey flow in the TUI.
6
+ * 📖 This module powers the Install Endpoints flow in the TUI.
7
7
  * It lets users pick one configured provider, choose a target tool, then install either:
8
8
  * - the full provider catalog (`all` models), or
9
9
  * - a curated subset of specific models (`selected`)