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.
- package/CHANGELOG.md +43 -1
- package/README.md +20 -9
- package/package.json +1 -1
- package/src/app.js +40 -14
- package/src/command-palette.js +327 -59
- package/src/config.js +4 -2
- package/src/endpoint-installer.js +1 -1
- package/src/key-handler.js +205 -31
- package/src/overlays.js +105 -57
- package/src/product-flags.js +4 -9
- package/src/render-helpers.js +15 -2
- package/src/render-table.js +51 -25
- package/src/theme.js +2 -0
- package/src/utils.js +1 -1
package/src/command-palette.js
CHANGED
|
@@ -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
|
-
* → `
|
|
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 {
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
{
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
{
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
{
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
{
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
*
|
|
380
|
+
* Now handles hierarchical structure with expandable categories.
|
|
381
|
+
* @param {Array} flatEntries - Flattened command tree entries
|
|
130
382
|
* @param {string} query
|
|
131
|
-
* @returns {Array
|
|
383
|
+
* @returns {Array} Sorted and filtered entries with match scores
|
|
132
384
|
*/
|
|
133
|
-
export function filterCommandPaletteEntries(
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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`)
|