free-coding-models 0.3.22 → 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 +13 -0
- package/README.md +4 -2
- package/package.json +1 -1
- package/src/app.js +22 -13
- package/src/command-palette.js +101 -6
- package/src/config.js +4 -2
- package/src/endpoint-installer.js +1 -1
- package/src/key-handler.js +115 -25
- package/src/overlays.js +21 -7
- package/src/render-helpers.js +15 -2
- package/src/render-table.js +32 -4
- package/src/utils.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
---
|
|
3
3
|
|
|
4
|
+
## 0.3.23
|
|
5
|
+
|
|
6
|
+
### Added
|
|
7
|
+
- **Favorites display mode (`Y`)**: Added a global toggle to switch favorites between `pinned + always visible` and `normal rows` (starred only, fully obeying active filters/sort).
|
|
8
|
+
- **Favorites mode row in Settings**: Added a dedicated Settings row to inspect/toggle favorites display behavior without leaving the maintenance screen.
|
|
9
|
+
- **Expanded command palette action menus**: Added nested menus for Ping Mode (speed/normal/slow/forced with explanations), Target Tool (all supported tools with explanations), and Favorites Mode (including the new `Y` flow).
|
|
10
|
+
- **Active text-filter footer alert**: When a custom search filter is active (for example `deep`), the last TUI footer line now shows a high-visibility inline badge (between `N Changelog` and `Ctrl+C Exit`) with the exact query and an `X` shortcut to clear it instantly.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- **Favorites sorting/filtering behavior**: Sort/filter logic now respects the selected favorites mode across the table refresh loop, hotkeys, and renderer so non-pinned mode behaves consistently everywhere.
|
|
14
|
+
- **Favorites default mode**: New/legacy configs now default to `Normal filter/sort` favorites mode (not pinned) until users press `Y` to opt into pinned+sticky behavior.
|
|
15
|
+
- **Footer/help/docs shortcut hints**: Surfaced `Y` in the TUI footer, Help overlay, and README so the new favorites mode is discoverable.
|
|
16
|
+
|
|
4
17
|
## 0.3.22
|
|
5
18
|
|
|
6
19
|
### Added
|
package/README.md
CHANGED
|
@@ -185,6 +185,8 @@ Press **`Z`** in the TUI to cycle between tools without restarting.
|
|
|
185
185
|
| `D` | Cycle provider filter |
|
|
186
186
|
| `E` | Toggle configured-only mode |
|
|
187
187
|
| `F` | Favorite / unfavorite model |
|
|
188
|
+
| `Y` | Toggle favorites mode (`Normal filter/sort` default ↔ `Pinned + always visible`) |
|
|
189
|
+
| `X` | Clear active custom text filter |
|
|
188
190
|
| `G` | Cycle global theme (`Auto → Dark → Light`) |
|
|
189
191
|
| `Ctrl+P` | Open ⚡️ command palette (search + run actions) |
|
|
190
192
|
| `R/S/C/M/O/L/A/H/V/B/U` | Sort columns |
|
|
@@ -206,12 +208,12 @@ Press **`Z`** in the TUI to cycle between tools without restarting.
|
|
|
206
208
|
- **Adaptive monitoring** — 2s burst for 60s → 10s normal → 30s idle
|
|
207
209
|
- **Stability score** — composite 0–100 (p95 latency, jitter, spike rate, uptime)
|
|
208
210
|
- **Smart ranking** — top 3 highlighted 🥇🥈🥉
|
|
209
|
-
- **Favorites** —
|
|
211
|
+
- **Favorites** — star models with `F`, persisted across sessions, default to normal rows, and switch display mode with `Y` (pinned+sticky vs normal rows)
|
|
210
212
|
- **Configured-only default** — only shows providers you have keys for
|
|
211
213
|
- **Keyless latency** — models ping even without an API key (show 🔑 NO KEY)
|
|
212
214
|
- **Smart Recommend** — questionnaire picks the best model for your task type
|
|
213
215
|
- **⚡️ Command Palette** — `Ctrl+P` opens a searchable action launcher for filters, sorting, overlays, and quick toggles
|
|
214
|
-
- **Install Endpoints** — push a full provider catalog into any tool's config (`
|
|
216
|
+
- **Install Endpoints** — push a full provider catalog into any tool's config (from Settings `P` or ⚡️ Command Palette)
|
|
215
217
|
- **Missing tool bootstrap** — detect absent CLIs, offer one-click install, then continue the selected launch automatically
|
|
216
218
|
- **Width guardrail** — shows a warning instead of a broken table in narrow terminals
|
|
217
219
|
- **Readable everywhere** — semantic theme palette keeps table rows, overlays, badges, and help screens legible in dark and light terminals
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "free-coding-models",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.23",
|
|
4
4
|
"description": "Find the fastest coding LLM models in seconds — ping free models from multiple providers, pick the best one for OpenCode, Cursor, or any AI coding assistant.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"nvidia",
|
package/src/app.js
CHANGED
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
* - JSON config stored in ~/.free-coding-models.json (auto-migrates from old plain-text)
|
|
22
22
|
* - Multi-provider support via sources.js (NIM/Groq/Cerebras/OpenRouter/Hugging Face/Replicate/DeepInfra/... — extensible)
|
|
23
23
|
* - Settings screen (P key) to manage API keys, provider toggles, manual updates, and provider-key diagnostics
|
|
24
|
-
* - Install Endpoints flow (
|
|
25
|
-
* - Favorites system: toggle with F,
|
|
24
|
+
* - Install Endpoints flow (Settings / Command Palette) to push provider catalogs into OpenCode, OpenClaw, Crush, and Goose
|
|
25
|
+
* - Favorites system: toggle with F, switch pinning mode with Y, persist between sessions
|
|
26
26
|
* - Uptime percentage tracking (successful pings / total pings)
|
|
27
27
|
* - Sortable columns (R/O/M/L/A/S/C/H/V/B/U/G keys)
|
|
28
28
|
* - Tier filtering via T key (cycles S+→S→A+→A→A-→B+→B→C→All)
|
|
@@ -307,7 +307,7 @@ export async function runApp(cliArgs, config) {
|
|
|
307
307
|
}
|
|
308
308
|
|
|
309
309
|
// 📖 Re-sync tracked external-tool catalogs after the live provider catalog has settled.
|
|
310
|
-
// 📖 This keeps prior
|
|
310
|
+
// 📖 This keeps prior endpoint installs aligned with the current FCM model list.
|
|
311
311
|
refreshInstalledEndpoints(config)
|
|
312
312
|
|
|
313
313
|
// 📖 Build results from MODELS — only include enabled providers
|
|
@@ -383,6 +383,7 @@ export async function runApp(cliArgs, config) {
|
|
|
383
383
|
tierFilterMode: 0, // 📖 Index into TIER_CYCLE (0=All, 1=S+, 2=S, ...)
|
|
384
384
|
originFilterMode: 0, // 📖 Index into ORIGIN_CYCLE (0=All, then providers)
|
|
385
385
|
hideUnconfiguredModels: config.settings?.hideUnconfiguredModels === true, // 📖 Hide providers with no configured API key when true.
|
|
386
|
+
favoritesPinnedAndSticky: config.settings?.favoritesPinnedAndSticky === true, // 📖 false by default: favorites follow normal sort/filter rules until Y enables pinned+sticky mode.
|
|
386
387
|
scrollOffset: 0, // 📖 First visible model index in viewport
|
|
387
388
|
terminalRows: process.stdout.rows || 24, // 📖 Current terminal height
|
|
388
389
|
terminalCols: process.stdout.columns || 80, // 📖 Current terminal width
|
|
@@ -409,11 +410,11 @@ export async function runApp(cliArgs, config) {
|
|
|
409
410
|
commandPaletteScrollOffset: 0, // 📖 Vertical scroll offset for the command palette result viewport.
|
|
410
411
|
commandPaletteResults: [], // 📖 Cached fuzzy-filtered command entries for the command palette.
|
|
411
412
|
commandPaletteFrozenTable: null, // 📖 Frozen table snapshot rendered behind the command palette overlay.
|
|
412
|
-
commandPaletteExpandedIds: new Set(['filters']), // 📖
|
|
413
|
+
commandPaletteExpandedIds: new Set(['filters', 'actions']), // 📖 Expanded category IDs (filters + actions open by default for quick access).
|
|
413
414
|
helpVisible: false, // 📖 Whether the help overlay (K key) is active
|
|
414
415
|
settingsScrollOffset: 0, // 📖 Vertical scroll offset for Settings overlay viewport
|
|
415
416
|
helpScrollOffset: 0, // 📖 Vertical scroll offset for Help overlay viewport
|
|
416
|
-
// 📖 Install Endpoints overlay state (
|
|
417
|
+
// 📖 Install Endpoints overlay state (opened from Settings or Command Palette)
|
|
417
418
|
installEndpointsOpen: false, // 📖 Whether the install-endpoints overlay is active
|
|
418
419
|
installEndpointsPhase: 'providers', // 📖 providers | tools | scope | models | result
|
|
419
420
|
installEndpointsCursor: 0, // 📖 Selected row within the current install phase
|
|
@@ -678,8 +679,8 @@ export async function runApp(cliArgs, config) {
|
|
|
678
679
|
const activeTier = TIER_CYCLE[state.tierFilterMode]
|
|
679
680
|
const activeOrigin = ORIGIN_CYCLE[state.originFilterMode]
|
|
680
681
|
state.results.forEach(r => {
|
|
681
|
-
// 📖
|
|
682
|
-
if (r.isFavorite) {
|
|
682
|
+
// 📖 Sticky-favorites mode keeps favorites visible regardless of configured-only, tier, or provider filters.
|
|
683
|
+
if (state.favoritesPinnedAndSticky && r.isFavorite) {
|
|
683
684
|
r.hidden = false
|
|
684
685
|
return
|
|
685
686
|
}
|
|
@@ -825,7 +826,7 @@ export async function runApp(cliArgs, config) {
|
|
|
825
826
|
if (cliArgs.tierFilter) {
|
|
826
827
|
const allowed = TIER_LETTER_MAP[cliArgs.tierFilter]
|
|
827
828
|
state.results.forEach(r => {
|
|
828
|
-
r.hidden = r.isFavorite ? false : !allowed.includes(r.tier)
|
|
829
|
+
r.hidden = (state.favoritesPinnedAndSticky && r.isFavorite) ? false : !allowed.includes(r.tier)
|
|
829
830
|
})
|
|
830
831
|
}
|
|
831
832
|
|
|
@@ -863,7 +864,9 @@ export async function runApp(cliArgs, config) {
|
|
|
863
864
|
// 📖 Cache visible+sorted models each frame so Enter handler always matches the display
|
|
864
865
|
if (!state.settingsOpen && !state.installEndpointsOpen && !state.toolInstallPromptOpen && !state.recommendOpen && !state.feedbackOpen && !state.changelogOpen && !state.commandPaletteOpen) {
|
|
865
866
|
const visible = state.results.filter(r => !r.hidden)
|
|
866
|
-
state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection
|
|
867
|
+
state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection, {
|
|
868
|
+
pinFavorites: state.favoritesPinnedAndSticky,
|
|
869
|
+
})
|
|
867
870
|
}
|
|
868
871
|
let tableContent = null
|
|
869
872
|
if (state.commandPaletteOpen) {
|
|
@@ -896,7 +899,9 @@ export async function runApp(cliArgs, config) {
|
|
|
896
899
|
state.settingsUpdateLatestVersion,
|
|
897
900
|
false,
|
|
898
901
|
state.startupLatestVersion,
|
|
899
|
-
state.versionAlertsEnabled
|
|
902
|
+
state.versionAlertsEnabled,
|
|
903
|
+
state.favoritesPinnedAndSticky,
|
|
904
|
+
state.customTextFilter
|
|
900
905
|
)
|
|
901
906
|
}
|
|
902
907
|
tableContent = state.commandPaletteFrozenTable
|
|
@@ -928,7 +933,9 @@ export async function runApp(cliArgs, config) {
|
|
|
928
933
|
state.settingsUpdateLatestVersion,
|
|
929
934
|
false,
|
|
930
935
|
state.startupLatestVersion,
|
|
931
|
-
state.versionAlertsEnabled
|
|
936
|
+
state.versionAlertsEnabled,
|
|
937
|
+
state.favoritesPinnedAndSticky,
|
|
938
|
+
state.customTextFilter
|
|
932
939
|
)
|
|
933
940
|
}
|
|
934
941
|
|
|
@@ -964,9 +971,11 @@ export async function runApp(cliArgs, config) {
|
|
|
964
971
|
|
|
965
972
|
// 📖 Populate visibleSorted before the first frame so Enter works immediately
|
|
966
973
|
const initialVisible = state.results.filter(r => !r.hidden)
|
|
967
|
-
state.visibleSorted = sortResultsWithPinnedFavorites(initialVisible, state.sortColumn, state.sortDirection
|
|
974
|
+
state.visibleSorted = sortResultsWithPinnedFavorites(initialVisible, state.sortColumn, state.sortDirection, {
|
|
975
|
+
pinFavorites: state.favoritesPinnedAndSticky,
|
|
976
|
+
})
|
|
968
977
|
|
|
969
|
-
process.stdout.write(ALT_HOME + renderTable(state.results, state.pendingPings, state.frame, state.cursor, state.sortColumn, state.sortDirection, state.pingInterval, state.lastPingTime, state.mode, state.tierFilterMode, state.scrollOffset, state.terminalRows, state.terminalCols, state.originFilterMode, null, state.pingMode, state.pingModeSource, state.hideUnconfiguredModels, state.widthWarningStartedAt, state.widthWarningDismissed, state.widthWarningShowCount, state.settingsUpdateState, state.settingsUpdateLatestVersion, false, state.startupLatestVersion, state.versionAlertsEnabled))
|
|
978
|
+
process.stdout.write(ALT_HOME + renderTable(state.results, state.pendingPings, state.frame, state.cursor, state.sortColumn, state.sortDirection, state.pingInterval, state.lastPingTime, state.mode, state.tierFilterMode, state.scrollOffset, state.terminalRows, state.terminalCols, state.originFilterMode, null, state.pingMode, state.pingModeSource, state.hideUnconfiguredModels, state.widthWarningStartedAt, state.widthWarningDismissed, state.widthWarningShowCount, state.settingsUpdateState, state.settingsUpdateLatestVersion, false, state.startupLatestVersion, state.versionAlertsEnabled, state.favoritesPinnedAndSticky, state.customTextFilter))
|
|
970
979
|
if (process.stdout.isTTY) {
|
|
971
980
|
process.stdout.flush && process.stdout.flush()
|
|
972
981
|
}
|
package/src/command-palette.js
CHANGED
|
@@ -15,6 +15,72 @@
|
|
|
15
15
|
* @see src/overlays.js
|
|
16
16
|
*/
|
|
17
17
|
|
|
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
|
+
},
|
|
82
|
+
]
|
|
83
|
+
|
|
18
84
|
// 📖 Base command tree template (will be enhanced with dynamic model list)
|
|
19
85
|
const BASE_COMMAND_TREE = [
|
|
20
86
|
{
|
|
@@ -93,6 +159,41 @@ const BASE_COMMAND_TREE = [
|
|
|
93
159
|
{ id: 'sort-uptime', label: 'Sort by uptime', shortcut: 'U', description: 'Success rate', keywords: ['sort', 'uptime'] },
|
|
94
160
|
]
|
|
95
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
|
+
},
|
|
96
197
|
// 📖 Pages - directly at root level, not in submenu
|
|
97
198
|
{ id: 'open-settings', label: 'Settings', shortcut: 'P', icon: '⚙️', type: 'page', description: 'API keys and preferences', keywords: ['settings', 'config', 'api key'] },
|
|
98
199
|
{ id: 'open-help', label: 'Help', shortcut: 'K', icon: '❓', type: 'page', description: 'Show all shortcuts', keywords: ['help', 'shortcuts', 'hotkeys'] },
|
|
@@ -100,12 +201,6 @@ const BASE_COMMAND_TREE = [
|
|
|
100
201
|
{ id: 'open-feedback', label: 'Feedback', shortcut: 'I', icon: '📝', type: 'page', description: 'Report bugs or requests', keywords: ['feedback', 'bug', 'request'] },
|
|
101
202
|
{ id: 'open-recommend', label: 'Smart recommend', shortcut: 'Q', icon: '🎯', type: 'page', description: 'Find best model for task', keywords: ['recommend', 'best model'] },
|
|
102
203
|
{ id: 'open-install-endpoints', label: 'Install endpoints', icon: '🔌', type: 'page', description: 'Install provider catalogs', keywords: ['install', 'endpoints', 'providers'] },
|
|
103
|
-
// 📖 Actions - directly at root level, not in submenu
|
|
104
|
-
{ id: 'action-cycle-theme', label: 'Cycle theme', shortcut: 'G', icon: '🌗', type: 'action', description: 'Switch dark/light/auto', keywords: ['theme', 'dark', 'light', 'auto'] },
|
|
105
|
-
{ id: 'action-cycle-tool-mode', label: 'Target Tool', shortcut: 'Z', icon: '🔄', type: 'action', description: 'Change target AI Coding CLI Tool.', keywords: ['tool', 'mode', 'launcher', 'target'] },
|
|
106
|
-
{ id: 'action-cycle-ping-mode', label: 'Cycle ping mode', shortcut: 'W', icon: '⚡', type: 'action', description: 'Adjust ping speed', keywords: ['ping', 'cadence', 'speed', 'slow'] },
|
|
107
|
-
{ id: 'action-toggle-favorite', label: 'Toggle favorite', shortcut: 'F', icon: '⭐', type: 'action', description: 'Pin to favorites', keywords: ['favorite', 'star'] },
|
|
108
|
-
{ id: 'action-reset-view', label: 'Reset view', shortcut: 'Shift+R', icon: '🔄', type: 'action', description: 'Reset filters and sort', keywords: ['reset', 'view', 'sort', 'filters'] },
|
|
109
204
|
]
|
|
110
205
|
|
|
111
206
|
/**
|
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`)
|
package/src/key-handler.js
CHANGED
|
@@ -519,7 +519,9 @@ export function createKeyHandler(ctx) {
|
|
|
519
519
|
// 📖 Shared table refresh helper so command-palette and hotkeys keep identical behavior.
|
|
520
520
|
function refreshVisibleSorted({ resetCursor = true } = {}) {
|
|
521
521
|
const visible = state.results.filter(r => !r.hidden)
|
|
522
|
-
state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection
|
|
522
|
+
state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection, {
|
|
523
|
+
pinFavorites: state.favoritesPinnedAndSticky,
|
|
524
|
+
})
|
|
523
525
|
if (resetCursor) {
|
|
524
526
|
state.cursor = 0
|
|
525
527
|
state.scrollOffset = 0
|
|
@@ -600,12 +602,48 @@ export function createKeyHandler(ctx) {
|
|
|
600
602
|
const modeOrder = getToolModeOrder()
|
|
601
603
|
const currentIndex = modeOrder.indexOf(state.mode)
|
|
602
604
|
const nextIndex = (currentIndex + 1) % modeOrder.length
|
|
603
|
-
|
|
605
|
+
setToolMode(modeOrder[nextIndex])
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// 📖 Keep tool-mode changes centralized so keyboard shortcuts and command palette
|
|
609
|
+
// 📖 both persist to config exactly the same way.
|
|
610
|
+
function setToolMode(nextMode) {
|
|
611
|
+
if (!getToolModeOrder().includes(nextMode)) return
|
|
612
|
+
state.mode = nextMode
|
|
604
613
|
if (!state.config.settings || typeof state.config.settings !== 'object') state.config.settings = {}
|
|
605
614
|
state.config.settings.preferredToolMode = state.mode
|
|
606
615
|
saveConfig(state.config)
|
|
607
616
|
}
|
|
608
617
|
|
|
618
|
+
// 📖 Favorites display mode:
|
|
619
|
+
// 📖 - true => favorites stay pinned + always visible (legacy behavior)
|
|
620
|
+
// 📖 - false => favorites are just starred rows and obey normal sort/filter rules
|
|
621
|
+
function setFavoritesDisplayMode(nextPinned, { preserveSelection = true } = {}) {
|
|
622
|
+
const normalizedNextPinned = nextPinned !== false
|
|
623
|
+
if (state.favoritesPinnedAndSticky === normalizedNextPinned) return
|
|
624
|
+
|
|
625
|
+
const selected = preserveSelection ? state.visibleSorted[state.cursor] : null
|
|
626
|
+
const selectedKey = selected ? toFavoriteKey(selected.providerKey, selected.modelId) : null
|
|
627
|
+
|
|
628
|
+
state.favoritesPinnedAndSticky = normalizedNextPinned
|
|
629
|
+
if (!state.config.settings || typeof state.config.settings !== 'object') state.config.settings = {}
|
|
630
|
+
state.config.settings.favoritesPinnedAndSticky = state.favoritesPinnedAndSticky
|
|
631
|
+
saveConfig(state.config)
|
|
632
|
+
|
|
633
|
+
applyTierFilter()
|
|
634
|
+
refreshVisibleSorted({ resetCursor: false })
|
|
635
|
+
|
|
636
|
+
if (selectedKey) {
|
|
637
|
+
const selectedIdx = state.visibleSorted.findIndex((row) => toFavoriteKey(row.providerKey, row.modelId) === selectedKey)
|
|
638
|
+
if (selectedIdx >= 0) state.cursor = selectedIdx
|
|
639
|
+
adjustScrollOffset(state)
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function toggleFavoritesDisplayMode() {
|
|
644
|
+
setFavoritesDisplayMode(!state.favoritesPinnedAndSticky)
|
|
645
|
+
}
|
|
646
|
+
|
|
609
647
|
function resetViewSettings() {
|
|
610
648
|
state.tierFilterMode = 0
|
|
611
649
|
state.originFilterMode = 0
|
|
@@ -631,7 +669,7 @@ export function createKeyHandler(ctx) {
|
|
|
631
669
|
applyTierFilter()
|
|
632
670
|
refreshVisibleSorted({ resetCursor: false })
|
|
633
671
|
|
|
634
|
-
if (wasFavorite) {
|
|
672
|
+
if (wasFavorite && state.favoritesPinnedAndSticky) {
|
|
635
673
|
state.cursor = 0
|
|
636
674
|
state.scrollOffset = 0
|
|
637
675
|
return
|
|
@@ -654,11 +692,28 @@ export function createKeyHandler(ctx) {
|
|
|
654
692
|
}
|
|
655
693
|
|
|
656
694
|
function refreshCommandPaletteResults() {
|
|
695
|
+
const query = (state.commandPaletteQuery || '').trim()
|
|
657
696
|
const tree = buildCommandPaletteTree(state.results || [])
|
|
658
|
-
|
|
659
|
-
|
|
697
|
+
// 📖 Keep collapsed view clean when query is empty, but search across the
|
|
698
|
+
// 📖 full tree when users type so hidden submenu commands still appear.
|
|
699
|
+
let flat
|
|
700
|
+
if (query.length > 0) {
|
|
701
|
+
const expandedIds = new Set()
|
|
702
|
+
const collectExpandedIds = (nodes) => {
|
|
703
|
+
for (const node of nodes || []) {
|
|
704
|
+
if (Array.isArray(node.children) && node.children.length > 0) {
|
|
705
|
+
expandedIds.add(node.id)
|
|
706
|
+
collectExpandedIds(node.children)
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
collectExpandedIds(tree)
|
|
711
|
+
flat = flattenCommandTree(tree, expandedIds)
|
|
712
|
+
} else {
|
|
713
|
+
flat = flattenCommandTree(tree, state.commandPaletteExpandedIds)
|
|
714
|
+
}
|
|
715
|
+
state.commandPaletteResults = filterCommandPaletteEntries(flat, query)
|
|
660
716
|
|
|
661
|
-
const query = (state.commandPaletteQuery || '').trim()
|
|
662
717
|
if (query.length > 0) {
|
|
663
718
|
state.commandPaletteResults.unshift({
|
|
664
719
|
id: 'filter-custom-text-apply',
|
|
@@ -707,6 +762,21 @@ export function createKeyHandler(ctx) {
|
|
|
707
762
|
function executeCommandPaletteEntry(entry) {
|
|
708
763
|
if (!entry?.id) return
|
|
709
764
|
|
|
765
|
+
if (entry.id.startsWith('action-set-ping-') && entry.pingMode) {
|
|
766
|
+
setPingMode(entry.pingMode, 'manual')
|
|
767
|
+
return
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
if (entry.id.startsWith('action-set-tool-') && entry.toolMode) {
|
|
771
|
+
setToolMode(entry.toolMode)
|
|
772
|
+
return
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
if (entry.id.startsWith('action-favorites-mode-') && typeof entry.favoritesPinned === 'boolean') {
|
|
776
|
+
setFavoritesDisplayMode(entry.favoritesPinned)
|
|
777
|
+
return
|
|
778
|
+
}
|
|
779
|
+
|
|
710
780
|
if (entry.id.startsWith('filter-tier-')) {
|
|
711
781
|
setTierFilterFromCommand(entry.tier ?? null)
|
|
712
782
|
return
|
|
@@ -793,6 +863,7 @@ export function createKeyHandler(ctx) {
|
|
|
793
863
|
return
|
|
794
864
|
}
|
|
795
865
|
case 'action-toggle-favorite': return toggleFavoriteOnSelectedRow()
|
|
866
|
+
case 'action-toggle-favorite-mode': return toggleFavoritesDisplayMode()
|
|
796
867
|
case 'action-reset-view': return resetViewSettings()
|
|
797
868
|
default:
|
|
798
869
|
return
|
|
@@ -1442,7 +1513,8 @@ export function createKeyHandler(ctx) {
|
|
|
1442
1513
|
const providerKeys = Object.keys(sources)
|
|
1443
1514
|
const updateRowIdx = providerKeys.length
|
|
1444
1515
|
const themeRowIdx = updateRowIdx + 1
|
|
1445
|
-
const
|
|
1516
|
+
const favoritesModeRowIdx = themeRowIdx + 1
|
|
1517
|
+
const cleanupLegacyProxyRowIdx = favoritesModeRowIdx + 1
|
|
1446
1518
|
const changelogViewRowIdx = cleanupLegacyProxyRowIdx + 1
|
|
1447
1519
|
// 📖 Profile system removed - API keys now persist permanently across all sessions
|
|
1448
1520
|
const maxRowIdx = changelogViewRowIdx
|
|
@@ -1523,10 +1595,7 @@ export function createKeyHandler(ctx) {
|
|
|
1523
1595
|
setResults(nextResults)
|
|
1524
1596
|
syncFavoriteFlags(state.results, state.config)
|
|
1525
1597
|
applyTierFilter()
|
|
1526
|
-
|
|
1527
|
-
state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection)
|
|
1528
|
-
if (state.cursor >= state.visibleSorted.length) state.cursor = Math.max(0, state.visibleSorted.length - 1)
|
|
1529
|
-
adjustScrollOffset(state)
|
|
1598
|
+
refreshVisibleSorted({ resetCursor: false })
|
|
1530
1599
|
// 📖 Re-ping all models that were 'noauth' (got 401 without key) but now have a key
|
|
1531
1600
|
// 📖 This makes the TUI react immediately when a user adds an API key in settings
|
|
1532
1601
|
const pingModel = getPingModel?.()
|
|
@@ -1591,6 +1660,11 @@ export function createKeyHandler(ctx) {
|
|
|
1591
1660
|
return
|
|
1592
1661
|
}
|
|
1593
1662
|
|
|
1663
|
+
if (state.settingsCursor === favoritesModeRowIdx) {
|
|
1664
|
+
toggleFavoritesDisplayMode()
|
|
1665
|
+
return
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1594
1668
|
if (state.settingsCursor === cleanupLegacyProxyRowIdx) {
|
|
1595
1669
|
runLegacyProxyCleanup()
|
|
1596
1670
|
return
|
|
@@ -1629,6 +1703,10 @@ export function createKeyHandler(ctx) {
|
|
|
1629
1703
|
cycleGlobalTheme()
|
|
1630
1704
|
return
|
|
1631
1705
|
}
|
|
1706
|
+
if (state.settingsCursor === favoritesModeRowIdx) {
|
|
1707
|
+
toggleFavoritesDisplayMode()
|
|
1708
|
+
return
|
|
1709
|
+
}
|
|
1632
1710
|
// 📖 Profile system removed - API keys now persist permanently across all sessions
|
|
1633
1711
|
|
|
1634
1712
|
// 📖 Toggle enabled/disabled for selected provider
|
|
@@ -1644,6 +1722,7 @@ export function createKeyHandler(ctx) {
|
|
|
1644
1722
|
if (
|
|
1645
1723
|
state.settingsCursor === updateRowIdx
|
|
1646
1724
|
|| state.settingsCursor === themeRowIdx
|
|
1725
|
+
|| state.settingsCursor === favoritesModeRowIdx
|
|
1647
1726
|
|| state.settingsCursor === cleanupLegacyProxyRowIdx
|
|
1648
1727
|
|| state.settingsCursor === changelogViewRowIdx
|
|
1649
1728
|
) return
|
|
@@ -1661,6 +1740,12 @@ export function createKeyHandler(ctx) {
|
|
|
1661
1740
|
return
|
|
1662
1741
|
}
|
|
1663
1742
|
|
|
1743
|
+
// 📖 Y toggles favorites display mode directly from Settings.
|
|
1744
|
+
if (key.name === 'y') {
|
|
1745
|
+
toggleFavoritesDisplayMode()
|
|
1746
|
+
return
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1664
1749
|
// 📖 Profile system removed - API keys now persist permanently across all sessions
|
|
1665
1750
|
|
|
1666
1751
|
if (key.ctrl && key.name === 'c') { exit(0); return }
|
|
@@ -1707,7 +1792,20 @@ export function createKeyHandler(ctx) {
|
|
|
1707
1792
|
return
|
|
1708
1793
|
}
|
|
1709
1794
|
|
|
1710
|
-
// 📖 Y key
|
|
1795
|
+
// 📖 Y key toggles favorites display mode (pinned+sticky vs normal rows).
|
|
1796
|
+
if (key.name === 'y' && !key.ctrl && !key.meta) {
|
|
1797
|
+
toggleFavoritesDisplayMode()
|
|
1798
|
+
return
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
// 📖 X clears the active free-text filter set from the command palette.
|
|
1802
|
+
if (key.name === 'x' && !key.ctrl && !key.meta) {
|
|
1803
|
+
if (!state.customTextFilter) return
|
|
1804
|
+
state.customTextFilter = null
|
|
1805
|
+
applyTierFilter()
|
|
1806
|
+
refreshVisibleSorted({ resetCursor: true })
|
|
1807
|
+
return
|
|
1808
|
+
}
|
|
1711
1809
|
|
|
1712
1810
|
// 📖 Profile system removed - API keys now persist permanently across all sessions
|
|
1713
1811
|
|
|
@@ -1720,7 +1818,8 @@ export function createKeyHandler(ctx) {
|
|
|
1720
1818
|
}
|
|
1721
1819
|
|
|
1722
1820
|
// 📖 Sorting keys: R=rank, O=origin, M=model, L=latest ping, A=avg ping, S=SWE-bench, C=context, H=health, V=verdict, B=stability, U=uptime, G=usage
|
|
1723
|
-
// 📖 T is reserved for tier filter cycling. Y
|
|
1821
|
+
// 📖 T is reserved for tier filter cycling. Y toggles favorites display mode.
|
|
1822
|
+
// 📖 X clears the active custom text filter.
|
|
1724
1823
|
// 📖 D is now reserved for provider filter cycling
|
|
1725
1824
|
// 📖 Shift+R is reserved for reset view settings
|
|
1726
1825
|
const sortKeys = {
|
|
@@ -1763,10 +1862,7 @@ export function createKeyHandler(ctx) {
|
|
|
1763
1862
|
state.config.settings.hideUnconfiguredModels = state.hideUnconfiguredModels
|
|
1764
1863
|
saveConfig(state.config)
|
|
1765
1864
|
applyTierFilter()
|
|
1766
|
-
|
|
1767
|
-
state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection)
|
|
1768
|
-
state.cursor = 0
|
|
1769
|
-
state.scrollOffset = 0
|
|
1865
|
+
refreshVisibleSorted({ resetCursor: true })
|
|
1770
1866
|
return
|
|
1771
1867
|
}
|
|
1772
1868
|
|
|
@@ -1775,10 +1871,7 @@ export function createKeyHandler(ctx) {
|
|
|
1775
1871
|
state.tierFilterMode = (state.tierFilterMode + 1) % TIER_CYCLE.length
|
|
1776
1872
|
applyTierFilter()
|
|
1777
1873
|
// 📖 Recompute visible sorted list and reset cursor to avoid stale index into new filtered set
|
|
1778
|
-
|
|
1779
|
-
state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection)
|
|
1780
|
-
state.cursor = 0
|
|
1781
|
-
state.scrollOffset = 0
|
|
1874
|
+
refreshVisibleSorted({ resetCursor: true })
|
|
1782
1875
|
persistUiSettings()
|
|
1783
1876
|
return
|
|
1784
1877
|
}
|
|
@@ -1788,10 +1881,7 @@ export function createKeyHandler(ctx) {
|
|
|
1788
1881
|
state.originFilterMode = (state.originFilterMode + 1) % ORIGIN_CYCLE.length
|
|
1789
1882
|
applyTierFilter()
|
|
1790
1883
|
// 📖 Recompute visible sorted list and reset cursor to avoid stale index into new filtered set
|
|
1791
|
-
|
|
1792
|
-
state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection)
|
|
1793
|
-
state.cursor = 0
|
|
1794
|
-
state.scrollOffset = 0
|
|
1884
|
+
refreshVisibleSorted({ resetCursor: true })
|
|
1795
1885
|
persistUiSettings()
|
|
1796
1886
|
return
|
|
1797
1887
|
}
|
package/src/overlays.js
CHANGED
|
@@ -94,7 +94,8 @@ export function createOverlayRenderers(state, deps) {
|
|
|
94
94
|
const providerKeys = Object.keys(sources)
|
|
95
95
|
const updateRowIdx = providerKeys.length
|
|
96
96
|
const themeRowIdx = updateRowIdx + 1
|
|
97
|
-
const
|
|
97
|
+
const favoritesModeRowIdx = themeRowIdx + 1
|
|
98
|
+
const cleanupLegacyProxyRowIdx = favoritesModeRowIdx + 1
|
|
98
99
|
const changelogViewRowIdx = cleanupLegacyProxyRowIdx + 1
|
|
99
100
|
const EL = '\x1b[K'
|
|
100
101
|
const lines = []
|
|
@@ -224,6 +225,16 @@ export function createOverlayRenderers(state, deps) {
|
|
|
224
225
|
const themeRow = `${bullet(state.settingsCursor === themeRowIdx)}${themeColors.textBold('Global Theme').padEnd(44)} ${themeStatusColor(themeStatus)}`
|
|
225
226
|
cursorLineByRow[themeRowIdx] = lines.length
|
|
226
227
|
lines.push(state.settingsCursor === themeRowIdx ? themeColors.bgCursor(themeRow) : themeRow)
|
|
228
|
+
|
|
229
|
+
// 📖 Favorites mode row mirrors Y-key behavior from the main table.
|
|
230
|
+
const favoritesModeEnabled = state.favoritesPinnedAndSticky === true
|
|
231
|
+
const favoritesModeStatus = favoritesModeEnabled
|
|
232
|
+
? themeColors.warningBold('Pinned + always visible')
|
|
233
|
+
: themeColors.info('Normal rows (filter/sort)')
|
|
234
|
+
const favoritesModeRow = `${bullet(state.settingsCursor === favoritesModeRowIdx)}${themeColors.textBold('Favorites Display Mode').padEnd(44)} ${favoritesModeStatus}`
|
|
235
|
+
cursorLineByRow[favoritesModeRowIdx] = lines.length
|
|
236
|
+
lines.push(state.settingsCursor === favoritesModeRowIdx ? themeColors.bgCursorSettingsList(favoritesModeRow) : favoritesModeRow)
|
|
237
|
+
|
|
227
238
|
if (updateState === 'error' && state.settingsUpdateError) {
|
|
228
239
|
lines.push(themeColors.error(` ${state.settingsUpdateError}`))
|
|
229
240
|
}
|
|
@@ -244,7 +255,7 @@ export function createOverlayRenderers(state, deps) {
|
|
|
244
255
|
if (state.settingsEditMode) {
|
|
245
256
|
lines.push(themeColors.dim(' Type API key • Enter Save • Esc Cancel'))
|
|
246
257
|
} else {
|
|
247
|
-
lines.push(themeColors.dim(' ↑↓ Navigate • Enter Edit/Run/Cycle • + Add key • - Remove key • Space Toggle/Cycle • T Test key • U Updates • G Global theme • Esc Close'))
|
|
258
|
+
lines.push(themeColors.dim(' ↑↓ Navigate • Enter Edit/Run/Cycle • + Add key • - Remove key • Space Toggle/Cycle • T Test key • U Updates • G Global theme • Y Favorites mode • Esc Close'))
|
|
248
259
|
}
|
|
249
260
|
// 📖 Show sync/restore status message if set
|
|
250
261
|
if (state.settingsSyncStatus) {
|
|
@@ -283,7 +294,7 @@ export function createOverlayRenderers(state, deps) {
|
|
|
283
294
|
|
|
284
295
|
// ─── Install Endpoints overlay renderer ───────────────────────────────────
|
|
285
296
|
// 📖 renderInstallEndpoints drives the provider → tool → scope → model flow
|
|
286
|
-
// 📖
|
|
297
|
+
// 📖 opened from Settings/Command Palette. It deliberately reuses the same overlay viewport
|
|
287
298
|
// 📖 helpers as Settings so long provider/model lists stay navigable.
|
|
288
299
|
function renderInstallEndpoints() {
|
|
289
300
|
const EL = '\x1b[K'
|
|
@@ -713,8 +724,8 @@ export function createOverlayRenderers(state, deps) {
|
|
|
713
724
|
lines.push(` ${label('CTX')} Context window size (128k, 200k, 256k, 1m, etc.) ${hint('Sort:')} ${key('C')}`)
|
|
714
725
|
lines.push(` ${hint('Bigger context = the model can read more of your codebase at once without forgetting.')}`)
|
|
715
726
|
lines.push('')
|
|
716
|
-
lines.push(` ${label('Model')} Model name (⭐ = favorited
|
|
717
|
-
lines.push(` ${hint('Star the ones you like
|
|
727
|
+
lines.push(` ${label('Model')} Model name (⭐ = favorited) ${hint('Sort:')} ${key('M')} ${hint('Favorite:')} ${key('F')}`)
|
|
728
|
+
lines.push(` ${hint('Star the ones you like. Press Y to switch between pinned mode and normal filter/sort mode.')}`)
|
|
718
729
|
lines.push('')
|
|
719
730
|
lines.push(` ${label('Provider')} Provider source (NIM, Groq, Cerebras, etc.) ${hint('Sort:')} ${key('O')} ${hint('Cycle:')} ${key('D')}`)
|
|
720
731
|
lines.push(` ${hint('Same model on different providers can have very different speed and uptime.')}`)
|
|
@@ -754,7 +765,9 @@ export function createOverlayRenderers(state, deps) {
|
|
|
754
765
|
lines.push(` ${key('Ctrl+P')} Open ⚡️ command palette ${hint('(search and run actions quickly)')}`)
|
|
755
766
|
lines.push(` ${key('E')} Toggle configured models only ${hint('(enabled by default)')}`)
|
|
756
767
|
lines.push(` ${key('Z')} Cycle tool mode ${hint('(OpenCode → Desktop → OpenClaw → Crush → Goose → Pi → Aider → Qwen → OpenHands → Amp)')}`)
|
|
757
|
-
lines.push(` ${key('F')} Toggle favorite on selected row ${hint('(⭐
|
|
768
|
+
lines.push(` ${key('F')} Toggle favorite on selected row ${hint('(⭐ persisted across sessions)')}`)
|
|
769
|
+
lines.push(` ${key('Y')} Toggle favorites mode ${hint('(Pinned + always visible ↔ Normal filter/sort behavior)')}`)
|
|
770
|
+
lines.push(` ${key('X')} Clear active text filter ${hint('(remove custom query applied from ⚡️ Command Palette)')}`)
|
|
758
771
|
lines.push(` ${key('Q')} Smart Recommend ${hint('(🎯 find the best model for your task — questionnaire + live analysis)')}`)
|
|
759
772
|
lines.push(` ${key('G')} Cycle theme ${hint('(auto → dark → light)')}`)
|
|
760
773
|
lines.push(` ${themeColors.errorBold('I')} Feedback, bugs & requests ${hint('(📝 send anonymous feedback, bug reports, or feature requests)')}`)
|
|
@@ -770,7 +783,8 @@ export function createOverlayRenderers(state, deps) {
|
|
|
770
783
|
lines.push(` ${key('PgUp/PgDn')} Jump by page`)
|
|
771
784
|
lines.push(` ${key('Home/End')} Jump first/last row`)
|
|
772
785
|
lines.push(` ${key('Enter')} Edit key / run selected maintenance action`)
|
|
773
|
-
lines.push(` ${key('Space')} Toggle provider
|
|
786
|
+
lines.push(` ${key('Space')} Toggle selected row option (provider/theme/favorites)`)
|
|
787
|
+
lines.push(` ${key('Y')} Toggle favorites mode (global)`)
|
|
774
788
|
lines.push(` ${key('T')} Test selected provider key`)
|
|
775
789
|
lines.push(` ${key('U')} Check updates manually`)
|
|
776
790
|
lines.push(` ${key('G')} Cycle theme globally`)
|
package/src/render-helpers.js
CHANGED
|
@@ -168,10 +168,23 @@ export function calculateViewport(terminalRows, scrollOffset, totalModels, extra
|
|
|
168
168
|
|
|
169
169
|
// 📖 sortResultsWithPinnedFavorites: Recommended models are pinned above favorites, favorites above non-favorites.
|
|
170
170
|
// 📖 Recommended: sorted by recommendation score (highest first).
|
|
171
|
-
// 📖 Favorites: keep insertion order (favoriteRank).
|
|
171
|
+
// 📖 Favorites: keep insertion order (favoriteRank) when pinFavorites=true.
|
|
172
172
|
// 📖 Non-favorites: active sort column/direction.
|
|
173
173
|
// 📖 Models that are both recommended AND favorite — show in recommended section.
|
|
174
|
-
|
|
174
|
+
// 📖 pinFavorites=false keeps favorites highlighted but lets normal sort/filter order apply.
|
|
175
|
+
export function sortResultsWithPinnedFavorites(results, sortColumn, sortDirection, { pinFavorites = true } = {}) {
|
|
176
|
+
if (!pinFavorites) {
|
|
177
|
+
const recommendedRows = results
|
|
178
|
+
.filter((r) => r.isRecommended)
|
|
179
|
+
.sort((a, b) => (b.recommendScore || 0) - (a.recommendScore || 0))
|
|
180
|
+
const nonRecommendedRows = sortResults(
|
|
181
|
+
results.filter((r) => !r.isRecommended),
|
|
182
|
+
sortColumn,
|
|
183
|
+
sortDirection
|
|
184
|
+
)
|
|
185
|
+
return [...recommendedRows, ...nonRecommendedRows]
|
|
186
|
+
}
|
|
187
|
+
|
|
175
188
|
const recommendedRows = results
|
|
176
189
|
.filter((r) => r.isRecommended && !r.isFavorite)
|
|
177
190
|
.sort((a, b) => (b.recommendScore || 0) - (a.recommendScore || 0))
|
package/src/render-table.js
CHANGED
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
* - Emoji-aware padding via padEndDisplay for aligned verdict/status cells
|
|
14
14
|
* - Viewport clipping with above/below indicators
|
|
15
15
|
* - Smart badges (mode, tier filter, origin filter)
|
|
16
|
-
* -
|
|
16
|
+
* - Favorites mode hint surfaced directly in footer hints (`Y`)
|
|
17
|
+
* - High-visibility active text-filter banner with one-key clear action (`X`)
|
|
17
18
|
* - Full-width red outdated-version banner when a newer npm release is known
|
|
18
19
|
* - Distinct auth-failure vs missing-key health labels so configured providers stay honest
|
|
19
20
|
*
|
|
@@ -66,7 +67,7 @@ export const PROVIDER_COLOR = new Proxy({}, {
|
|
|
66
67
|
})
|
|
67
68
|
|
|
68
69
|
// ─── renderTable: mode param controls footer hint text (opencode vs openclaw) ─────────
|
|
69
|
-
export function renderTable(results, pendingPings, frame, cursor = null, sortColumn = 'avg', sortDirection = 'asc', pingInterval = PING_INTERVAL, lastPingTime = Date.now(), mode = 'opencode', tierFilterMode = 0, scrollOffset = 0, terminalRows = 0, terminalCols = 0, originFilterMode = 0, legacyStatus = null, pingMode = 'normal', pingModeSource = 'auto', hideUnconfiguredModels = false, widthWarningStartedAt = null, widthWarningDismissed = false, widthWarningShowCount = 0, settingsUpdateState = 'idle', settingsUpdateLatestVersion = null, legacyFlag = false, startupLatestVersion = null, versionAlertsEnabled = true) {
|
|
70
|
+
export function renderTable(results, pendingPings, frame, cursor = null, sortColumn = 'avg', sortDirection = 'asc', pingInterval = PING_INTERVAL, lastPingTime = Date.now(), mode = 'opencode', tierFilterMode = 0, scrollOffset = 0, terminalRows = 0, terminalCols = 0, originFilterMode = 0, legacyStatus = null, pingMode = 'normal', pingModeSource = 'auto', hideUnconfiguredModels = false, widthWarningStartedAt = null, widthWarningDismissed = false, widthWarningShowCount = 0, settingsUpdateState = 'idle', settingsUpdateLatestVersion = null, legacyFlag = false, startupLatestVersion = null, versionAlertsEnabled = true, favoritesPinnedAndSticky = false, customTextFilter = null) {
|
|
70
71
|
// 📖 Filter out hidden models for display
|
|
71
72
|
const visibleResults = results.filter(r => !r.hidden)
|
|
72
73
|
|
|
@@ -237,7 +238,9 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
237
238
|
}
|
|
238
239
|
|
|
239
240
|
// 📖 Sort models using the shared helper
|
|
240
|
-
const sorted = sortResultsWithPinnedFavorites(visibleResults, sortColumn, sortDirection
|
|
241
|
+
const sorted = sortResultsWithPinnedFavorites(visibleResults, sortColumn, sortDirection, {
|
|
242
|
+
pinFavorites: favoritesPinnedAndSticky,
|
|
243
|
+
})
|
|
241
244
|
|
|
242
245
|
const lines = [
|
|
243
246
|
` ${themeColors.accentBold(`🚀 free-coding-models v${LOCAL_VERSION}`)}${modeBadge}${pingControlBadge}${tierBadge}${originBadge}${chalk.reset('')} ` +
|
|
@@ -339,6 +342,7 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
339
342
|
}
|
|
340
343
|
|
|
341
344
|
// 📖 Viewport clipping: only render models that fit on screen
|
|
345
|
+
const hasCustomFilter = typeof customTextFilter === 'string' && customTextFilter.trim().length > 0
|
|
342
346
|
const extraFooterLines = versionStatus.isOutdated ? 1 : 0
|
|
343
347
|
const vp = calculateViewport(terminalRows, scrollOffset, sorted.length, extraFooterLines)
|
|
344
348
|
const paintSweScore = (score, paddedText) => {
|
|
@@ -618,10 +622,14 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
618
622
|
// 📖 states are obvious even when the user misses the smaller header badges.
|
|
619
623
|
const configuredBadgeBg = getTheme() === 'dark' ? [52, 120, 88] : [195, 234, 206]
|
|
620
624
|
const activeHotkey = (keyLabel, text, bg) => themeColors.badge(`${keyLabel}${text}`, bg, getReadableTextRgb(bg))
|
|
625
|
+
const favoritesModeBg = favoritesPinnedAndSticky ? [157, 122, 48] : [95, 95, 95]
|
|
626
|
+
const favoritesModeLabel = favoritesPinnedAndSticky ? ' Favorites Pinned' : ' Favorites Normal'
|
|
621
627
|
// 📖 Line 1: core navigation + filtering shortcuts
|
|
622
628
|
lines.push(
|
|
623
629
|
' ' + hotkey('F', ' Toggle Favorite') +
|
|
624
630
|
themeColors.dim(` • `) +
|
|
631
|
+
activeHotkey('Y', favoritesModeLabel, favoritesModeBg) +
|
|
632
|
+
themeColors.dim(` • `) +
|
|
625
633
|
(tierFilterMode > 0
|
|
626
634
|
? activeHotkey('T', ` Tier (${activeTierLabel})`, getTierRgb(activeTierLabel))
|
|
627
635
|
: hotkey('T', ' Tier')) +
|
|
@@ -673,9 +681,29 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
673
681
|
lines.push(chalk.bgRed.white.bold(paddedBanner))
|
|
674
682
|
}
|
|
675
683
|
|
|
676
|
-
// 📖 Final footer line: changelog
|
|
684
|
+
// 📖 Final footer line: changelog + optional active text-filter badge + exit hint.
|
|
685
|
+
let filterBadge = ''
|
|
686
|
+
if (hasCustomFilter) {
|
|
687
|
+
const normalizedFilter = customTextFilter.trim().replace(/\s+/g, ' ')
|
|
688
|
+
const filterPrefix = 'X Disable filter: "'
|
|
689
|
+
const filterSuffix = '"'
|
|
690
|
+
const separatorPlain = ' • '
|
|
691
|
+
const baseFooterPlain = ' N Changelog' + separatorPlain + 'Ctrl+C Exit'
|
|
692
|
+
const baseBadgeWidth = displayWidth(` ${filterPrefix}${filterSuffix} `)
|
|
693
|
+
const availableFilterWidth = terminalCols > 0
|
|
694
|
+
? Math.max(8, terminalCols - displayWidth(baseFooterPlain) - displayWidth(separatorPlain) - baseBadgeWidth)
|
|
695
|
+
: normalizedFilter.length
|
|
696
|
+
const visibleFilter = normalizedFilter.length > availableFilterWidth
|
|
697
|
+
? `${normalizedFilter.slice(0, Math.max(3, availableFilterWidth - 3))}...`
|
|
698
|
+
: normalizedFilter
|
|
699
|
+
filterBadge = chalk.bgYellow.black.bold(` ${filterPrefix}${visibleFilter}${filterSuffix} `)
|
|
700
|
+
}
|
|
701
|
+
|
|
677
702
|
lines.push(
|
|
678
703
|
' ' + themeColors.hotkey('N') + themeColors.dim(' Changelog') +
|
|
704
|
+
(filterBadge
|
|
705
|
+
? themeColors.dim(' • ') + filterBadge
|
|
706
|
+
: '') +
|
|
679
707
|
themeColors.dim(' • ') +
|
|
680
708
|
themeColors.dim('Ctrl+C Exit')
|
|
681
709
|
)
|
package/src/utils.js
CHANGED
|
@@ -220,7 +220,7 @@ export const getStabilityScore = (r) => {
|
|
|
220
220
|
//
|
|
221
221
|
// 📖 Supported columns in the sorter.
|
|
222
222
|
// 📖 Most map directly to visible TUI sort hotkeys; `tier` remains available internally
|
|
223
|
-
// 📖
|
|
223
|
+
// 📖 while `Y` is used by the live UI for favorites display mode.
|
|
224
224
|
// - 'rank' (R key) — original index from sources.js
|
|
225
225
|
// - 'tier' (internal) — tier hierarchy (S+ first, C last)
|
|
226
226
|
// - 'origin' (O key) — provider name (all NIM for now, future-proofed)
|