free-coding-models 0.3.17 → 0.3.19
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 +26 -0
- package/README.md +11 -1
- package/package.json +1 -1
- package/src/app.js +106 -3
- package/src/command-palette.js +170 -0
- package/src/config.js +3 -3
- package/src/key-handler.js +492 -142
- package/src/openclaw.js +39 -5
- package/src/opencode.js +2 -1
- package/src/overlays.js +426 -208
- package/src/render-helpers.js +1 -1
- package/src/render-table.js +141 -177
- package/src/theme.js +294 -43
- package/src/tier-colors.js +15 -17
- package/src/tool-bootstrap.js +310 -0
- package/src/tool-launchers.js +12 -7
- package/src/ui-config.js +24 -31
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
+
## 0.3.19
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **Command palette overlay (`Ctrl+P`)**: Added a searchable floating palette with fuzzy matching so users can quickly run filters, sorts, overlays, and global actions.
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- **Main footer and help discoverability**: surfaced `Ctrl+P` in table hints and Help overlay so the new command launcher is visible immediately.
|
|
12
|
+
- **Command palette spacing polish**: added two-character inner padding around the floating palette so the overlay feels less cramped and visually cleaner.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- **Command palette visual jitter**: background ping cycles now pause while the command palette is open so table rows stop reshuffling during command search.
|
|
16
|
+
- **Command palette background freeze**: while the palette is open, the full table behind it is now frozen (spinner glyphs, ping countdown, and dynamic row updates) and resumes instantly on close.
|
|
17
|
+
|
|
18
|
+
## 0.3.18
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- **Missing tool bootstrap flow**: FCM now detects when a target CLI is absent, offers a minimal in-TUI install confirmation, runs the official global install command, then resumes the selected model launch automatically.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- **TUI readability overhaul across every screen**: the main table, Settings, Help, Smart Recommend, Feedback, and Changelog overlays now share a semantic high-contrast theme system instead of a patchwork of hardcoded colors.
|
|
25
|
+
- **Global theme switching now works for real**: press `G` to cycle `auto → dark → light` live, and the Settings screen now exposes a visible `Global Theme` row for the same control.
|
|
26
|
+
- **Launcher binary resolution**: direct tool launches now search PATH plus common user bin directories so a freshly installed CLI can be reused immediately in the same FCM session.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
- **Theme repaint bugs**: provider colors, tier colors, separators, badges, cursor highlights, and overlay backgrounds now update immediately when the theme changes instead of keeping stale import-time colors.
|
|
30
|
+
|
|
5
31
|
## 0.3.17
|
|
6
32
|
|
|
7
33
|
### Added
|
package/README.md
CHANGED
|
@@ -103,6 +103,8 @@ free-coding-models
|
|
|
103
103
|
|
|
104
104
|
On first run, you'll be prompted to enter your API key(s). You can skip providers and add more later with **`P`**.
|
|
105
105
|
|
|
106
|
+
Need to fix contrast because your terminal theme is fighting the TUI? Press **`G`** at any time to cycle **Auto → Dark → Light**. The switch recolors the full interface live: table, Settings, Help, Smart Recommend, Feedback, and Changelog.
|
|
107
|
+
|
|
106
108
|
**③ Pick a model and launch your tool:**
|
|
107
109
|
|
|
108
110
|
```
|
|
@@ -111,6 +113,8 @@ On first run, you'll be prompted to enter your API key(s). You can skip provider
|
|
|
111
113
|
|
|
112
114
|
The model you select is automatically written into your tool's config (OpenCode, OpenClaw, Crush, etc.) and the tool opens immediately. Done.
|
|
113
115
|
|
|
116
|
+
If the active CLI tool is missing, FCM now catches it before launch, offers a tiny Yes/No install prompt, installs the tool with its official global command, then resumes the same model launch automatically.
|
|
117
|
+
|
|
114
118
|
> 💡 You can also run `free-coding-models --goose --tier S` to pre-filter to S-tier models for Goose before the TUI even opens.
|
|
115
119
|
|
|
116
120
|
|
|
@@ -171,8 +175,10 @@ Press **`Z`** in the TUI to cycle between tools without restarting.
|
|
|
171
175
|
| `D` | Cycle provider filter |
|
|
172
176
|
| `E` | Toggle configured-only mode |
|
|
173
177
|
| `F` | Favorite / unfavorite model |
|
|
178
|
+
| `G` | Cycle global theme (`Auto → Dark → Light`) |
|
|
179
|
+
| `Ctrl+P` | Open command palette (search + run actions) |
|
|
174
180
|
| `R/S/C/M/O/L/A/H/V/B/U` | Sort columns |
|
|
175
|
-
| `P` | Settings (API keys, providers, updates) |
|
|
181
|
+
| `P` | Settings (API keys, providers, updates, theme) |
|
|
176
182
|
| `Y` | Install Endpoints (push provider into tool config) |
|
|
177
183
|
| `Q` | Smart Recommend overlay |
|
|
178
184
|
| `N` | Changelog |
|
|
@@ -195,8 +201,12 @@ Press **`Z`** in the TUI to cycle between tools without restarting.
|
|
|
195
201
|
- **Configured-only default** — only shows providers you have keys for
|
|
196
202
|
- **Keyless latency** — models ping even without an API key (show 🔑 NO KEY)
|
|
197
203
|
- **Smart Recommend** — questionnaire picks the best model for your task type
|
|
204
|
+
- **Command Palette** — `Ctrl+P` opens a searchable action launcher for filters, sorting, overlays, and quick toggles
|
|
198
205
|
- **Install Endpoints** — push a full provider catalog into any tool's config (`Y`)
|
|
206
|
+
- **Missing tool bootstrap** — detect absent CLIs, offer one-click install, then continue the selected launch automatically
|
|
199
207
|
- **Width guardrail** — shows a warning instead of a broken table in narrow terminals
|
|
208
|
+
- **Readable everywhere** — semantic theme palette keeps table rows, overlays, badges, and help screens legible in dark and light terminals
|
|
209
|
+
- **Global theme switch** — `G` cycles `auto`, `dark`, and `light` live without restarting
|
|
200
210
|
- **Auto-retry** — timeout models keep getting retried
|
|
201
211
|
|
|
202
212
|
---
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "free-coding-models",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.19",
|
|
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
|
@@ -122,6 +122,7 @@ import { createOverlayRenderers } from '../src/overlays.js'
|
|
|
122
122
|
import { createKeyHandler } from '../src/key-handler.js'
|
|
123
123
|
import { getToolModeOrder, getToolMeta } from '../src/tool-metadata.js'
|
|
124
124
|
import { startExternalTool } from '../src/tool-launchers.js'
|
|
125
|
+
import { getToolInstallPlan, installToolWithPlan, isToolInstalled } from '../src/tool-bootstrap.js'
|
|
125
126
|
import { getConfiguredInstallableProviders, installProviderEndpoints, refreshInstalledEndpoints, getInstallTargetModes, getProviderCatalogModels } from '../src/endpoint-installer.js'
|
|
126
127
|
import { loadCache, saveCache, clearCache, getCacheAge } from '../src/cache.js'
|
|
127
128
|
import { checkConfigSecurity } from '../src/security.js'
|
|
@@ -176,7 +177,7 @@ const LOCAL_VERSION = pkg.version
|
|
|
176
177
|
export async function runApp(cliArgs, config) {
|
|
177
178
|
|
|
178
179
|
// 📖 Detect user active terminal theme
|
|
179
|
-
detectActiveTheme(config.settings?.theme || '
|
|
180
|
+
detectActiveTheme(config.settings?.theme || 'auto')
|
|
180
181
|
|
|
181
182
|
// 📖 Check config file security — warn and offer auto-fix if permissions are too open
|
|
182
183
|
const securityCheck = checkConfigSecurity()
|
|
@@ -405,6 +406,12 @@ export async function runApp(cliArgs, config) {
|
|
|
405
406
|
settingsUpdateError: null, // 📖 Last update-check error message for maintenance row
|
|
406
407
|
config, // 📖 Live reference to the config object (updated on save)
|
|
407
408
|
visibleSorted: [], // 📖 Cached visible+sorted models — shared between render loop and key handlers
|
|
409
|
+
commandPaletteOpen: false, // 📖 Whether the Ctrl+P command palette overlay is active.
|
|
410
|
+
commandPaletteQuery: '', // 📖 Current command palette search query.
|
|
411
|
+
commandPaletteCursor: 0, // 📖 Selected command index in the filtered command list.
|
|
412
|
+
commandPaletteScrollOffset: 0, // 📖 Vertical scroll offset for the command palette result viewport.
|
|
413
|
+
commandPaletteResults: [], // 📖 Cached fuzzy-filtered command entries for the command palette.
|
|
414
|
+
commandPaletteFrozenTable: null, // 📖 Frozen table snapshot rendered behind the command palette overlay.
|
|
408
415
|
helpVisible: false, // 📖 Whether the help overlay (K key) is active
|
|
409
416
|
settingsScrollOffset: 0, // 📖 Vertical scroll offset for Settings overlay viewport
|
|
410
417
|
helpScrollOffset: 0, // 📖 Vertical scroll offset for Help overlay viewport
|
|
@@ -420,6 +427,14 @@ export async function runApp(cliArgs, config) {
|
|
|
420
427
|
installEndpointsSelectedModelIds: new Set(), // 📖 Multi-select buffer for the selected-models phase
|
|
421
428
|
installEndpointsErrorMsg: null, // 📖 Temporary validation/error message inside the install flow
|
|
422
429
|
installEndpointsResult: null, // 📖 Final install result shown in the result phase
|
|
430
|
+
// 📖 Missing-tool bootstrap overlay — confirms a one-click install before retrying the launch.
|
|
431
|
+
toolInstallPromptOpen: false,
|
|
432
|
+
toolInstallPromptCursor: 0,
|
|
433
|
+
toolInstallPromptScrollOffset: 0,
|
|
434
|
+
toolInstallPromptMode: null,
|
|
435
|
+
toolInstallPromptModel: null,
|
|
436
|
+
toolInstallPromptPlan: null,
|
|
437
|
+
toolInstallPromptErrorMsg: null,
|
|
423
438
|
// 📖 Smart Recommend overlay state (Q key opens it)
|
|
424
439
|
recommendOpen: false, // 📖 Whether the recommend overlay is active
|
|
425
440
|
recommendPhase: 'questionnaire', // 📖 'questionnaire'|'analyzing'|'results' — current phase
|
|
@@ -750,6 +765,9 @@ export async function runApp(cliArgs, config) {
|
|
|
750
765
|
getInstallTargetModes,
|
|
751
766
|
getProviderCatalogModels,
|
|
752
767
|
getToolMeta,
|
|
768
|
+
getToolInstallPlan,
|
|
769
|
+
padEndDisplay,
|
|
770
|
+
displayWidth,
|
|
753
771
|
})
|
|
754
772
|
|
|
755
773
|
onKeyPress = createKeyHandler({
|
|
@@ -785,6 +803,9 @@ export async function runApp(cliArgs, config) {
|
|
|
785
803
|
startOpenCode,
|
|
786
804
|
startExternalTool,
|
|
787
805
|
getToolModeOrder,
|
|
806
|
+
getToolInstallPlan,
|
|
807
|
+
isToolInstalled,
|
|
808
|
+
installToolWithPlan,
|
|
788
809
|
startRecommendAnalysis: overlays.startRecommendAnalysis,
|
|
789
810
|
stopRecommendAnalysis: overlays.stopRecommendAnalysis,
|
|
790
811
|
sendBugReport,
|
|
@@ -844,14 +865,87 @@ export async function runApp(cliArgs, config) {
|
|
|
844
865
|
refreshAutoPingMode()
|
|
845
866
|
state.frame++
|
|
846
867
|
// 📖 Cache visible+sorted models each frame so Enter handler always matches the display
|
|
847
|
-
if (!state.settingsOpen && !state.installEndpointsOpen && !state.recommendOpen && !state.feedbackOpen && !state.changelogOpen) {
|
|
868
|
+
if (!state.settingsOpen && !state.installEndpointsOpen && !state.toolInstallPromptOpen && !state.recommendOpen && !state.feedbackOpen && !state.changelogOpen && !state.commandPaletteOpen) {
|
|
848
869
|
const visible = state.results.filter(r => !r.hidden)
|
|
849
870
|
state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection)
|
|
850
871
|
}
|
|
872
|
+
let tableContent = null
|
|
873
|
+
if (state.commandPaletteOpen) {
|
|
874
|
+
if (!state.commandPaletteFrozenTable) {
|
|
875
|
+
// 📖 Freeze the full table (including countdown and spinner glyphs) while
|
|
876
|
+
// 📖 the command palette is open so the background remains perfectly static.
|
|
877
|
+
state.commandPaletteFrozenTable = renderTable(
|
|
878
|
+
state.results,
|
|
879
|
+
state.pendingPings,
|
|
880
|
+
state.frame,
|
|
881
|
+
state.cursor,
|
|
882
|
+
state.sortColumn,
|
|
883
|
+
state.sortDirection,
|
|
884
|
+
state.pingInterval,
|
|
885
|
+
state.lastPingTime,
|
|
886
|
+
state.mode,
|
|
887
|
+
state.tierFilterMode,
|
|
888
|
+
state.scrollOffset,
|
|
889
|
+
state.terminalRows,
|
|
890
|
+
state.terminalCols,
|
|
891
|
+
state.originFilterMode,
|
|
892
|
+
null,
|
|
893
|
+
state.pingMode,
|
|
894
|
+
state.pingModeSource,
|
|
895
|
+
state.hideUnconfiguredModels,
|
|
896
|
+
state.widthWarningStartedAt,
|
|
897
|
+
state.widthWarningDismissed,
|
|
898
|
+
state.widthWarningShowCount,
|
|
899
|
+
state.settingsUpdateState,
|
|
900
|
+
state.settingsUpdateLatestVersion,
|
|
901
|
+
false,
|
|
902
|
+
state.startupLatestVersion,
|
|
903
|
+
state.versionAlertsEnabled,
|
|
904
|
+
state.config.settings?.disableWidthsWarning ?? false
|
|
905
|
+
)
|
|
906
|
+
}
|
|
907
|
+
tableContent = state.commandPaletteFrozenTable
|
|
908
|
+
} else {
|
|
909
|
+
state.commandPaletteFrozenTable = null
|
|
910
|
+
tableContent = renderTable(
|
|
911
|
+
state.results,
|
|
912
|
+
state.pendingPings,
|
|
913
|
+
state.frame,
|
|
914
|
+
state.cursor,
|
|
915
|
+
state.sortColumn,
|
|
916
|
+
state.sortDirection,
|
|
917
|
+
state.pingInterval,
|
|
918
|
+
state.lastPingTime,
|
|
919
|
+
state.mode,
|
|
920
|
+
state.tierFilterMode,
|
|
921
|
+
state.scrollOffset,
|
|
922
|
+
state.terminalRows,
|
|
923
|
+
state.terminalCols,
|
|
924
|
+
state.originFilterMode,
|
|
925
|
+
null,
|
|
926
|
+
state.pingMode,
|
|
927
|
+
state.pingModeSource,
|
|
928
|
+
state.hideUnconfiguredModels,
|
|
929
|
+
state.widthWarningStartedAt,
|
|
930
|
+
state.widthWarningDismissed,
|
|
931
|
+
state.widthWarningShowCount,
|
|
932
|
+
state.settingsUpdateState,
|
|
933
|
+
state.settingsUpdateLatestVersion,
|
|
934
|
+
false,
|
|
935
|
+
state.startupLatestVersion,
|
|
936
|
+
state.versionAlertsEnabled,
|
|
937
|
+
state.config.settings?.disableWidthsWarning ?? false
|
|
938
|
+
)
|
|
939
|
+
}
|
|
940
|
+
|
|
851
941
|
const content = state.settingsOpen
|
|
852
942
|
? overlays.renderSettings()
|
|
853
943
|
: state.installEndpointsOpen
|
|
854
944
|
? overlays.renderInstallEndpoints()
|
|
945
|
+
: state.toolInstallPromptOpen
|
|
946
|
+
? overlays.renderToolInstallPrompt()
|
|
947
|
+
: state.commandPaletteOpen
|
|
948
|
+
? tableContent + overlays.renderCommandPalette()
|
|
855
949
|
: state.recommendOpen
|
|
856
950
|
? overlays.renderRecommend()
|
|
857
951
|
: state.feedbackOpen
|
|
@@ -860,7 +954,7 @@ export async function runApp(cliArgs, config) {
|
|
|
860
954
|
? overlays.renderHelp()
|
|
861
955
|
: state.changelogOpen
|
|
862
956
|
? overlays.renderChangelog()
|
|
863
|
-
:
|
|
957
|
+
: tableContent
|
|
864
958
|
process.stdout.write(ALT_HOME + content)
|
|
865
959
|
if (process.stdout.isTTY) {
|
|
866
960
|
process.stdout.flush && process.stdout.flush()
|
|
@@ -904,6 +998,15 @@ export async function runApp(cliArgs, config) {
|
|
|
904
998
|
const runPingCycle = async () => {
|
|
905
999
|
try {
|
|
906
1000
|
refreshAutoPingMode()
|
|
1001
|
+
|
|
1002
|
+
// 📖 Command palette intentionally pauses background ping bursts to avoid
|
|
1003
|
+
// 📖 visible row jitter while users type and navigate commands.
|
|
1004
|
+
if (state.commandPaletteOpen) {
|
|
1005
|
+
state.lastPingTime = Date.now()
|
|
1006
|
+
scheduleNextPing()
|
|
1007
|
+
return
|
|
1008
|
+
}
|
|
1009
|
+
|
|
907
1010
|
state.lastPingTime = Date.now()
|
|
908
1011
|
|
|
909
1012
|
// 📖 Refresh persisted usage snapshots each cycle so background usage data appears live in table.
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file command-palette.js
|
|
3
|
+
* @description Command palette registry and fuzzy search helpers for the main TUI.
|
|
4
|
+
*
|
|
5
|
+
* @functions
|
|
6
|
+
* → `buildCommandPaletteEntries` — builds the current command list with dynamic provider/tier context
|
|
7
|
+
* → `fuzzyMatchCommand` — scores a query against one string and returns match positions
|
|
8
|
+
* → `filterCommandPaletteEntries` — returns sorted command matches for a query
|
|
9
|
+
*
|
|
10
|
+
* @exports { COMMAND_CATEGORY_ORDER, buildCommandPaletteEntries, fuzzyMatchCommand, filterCommandPaletteEntries }
|
|
11
|
+
*
|
|
12
|
+
* @see src/key-handler.js
|
|
13
|
+
* @see src/overlays.js
|
|
14
|
+
*/
|
|
15
|
+
|
|
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'] },
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
const ID_TO_TIER = {
|
|
63
|
+
'filter-tier-all': null,
|
|
64
|
+
'filter-tier-splus': 'S+',
|
|
65
|
+
'filter-tier-s': 'S',
|
|
66
|
+
'filter-tier-aplus': 'A+',
|
|
67
|
+
'filter-tier-a': 'A',
|
|
68
|
+
'filter-tier-aminus': 'A-',
|
|
69
|
+
'filter-tier-bplus': 'B+',
|
|
70
|
+
'filter-tier-b': 'B',
|
|
71
|
+
'filter-tier-c': 'C',
|
|
72
|
+
}
|
|
73
|
+
|
|
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
|
+
}))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 📖 Fuzzy matching optimized for short command labels and keyboard aliases.
|
|
83
|
+
* @param {string} query
|
|
84
|
+
* @param {string} text
|
|
85
|
+
* @returns {{ matched: boolean, score: number, positions: number[] }}
|
|
86
|
+
*/
|
|
87
|
+
export function fuzzyMatchCommand(query, text) {
|
|
88
|
+
const q = (query || '').trim().toLowerCase()
|
|
89
|
+
const t = (text || '').toLowerCase()
|
|
90
|
+
|
|
91
|
+
if (!q) return { matched: true, score: 0, positions: [] }
|
|
92
|
+
if (!t) return { matched: false, score: 0, positions: [] }
|
|
93
|
+
|
|
94
|
+
let qIdx = 0
|
|
95
|
+
const positions = []
|
|
96
|
+
for (let i = 0; i < t.length && qIdx < q.length; i++) {
|
|
97
|
+
if (q[qIdx] === t[i]) {
|
|
98
|
+
positions.push(i)
|
|
99
|
+
qIdx++
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (qIdx !== q.length) return { matched: false, score: 0, positions: [] }
|
|
104
|
+
|
|
105
|
+
let score = q.length * 10
|
|
106
|
+
|
|
107
|
+
// 📖 Bonus when matches are contiguous.
|
|
108
|
+
for (let i = 1; i < positions.length; i++) {
|
|
109
|
+
if (positions[i] === positions[i - 1] + 1) score += 5
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 📖 Bonus for word boundaries and prefix matches.
|
|
113
|
+
for (const pos of positions) {
|
|
114
|
+
if (pos === 0) score += 8
|
|
115
|
+
else {
|
|
116
|
+
const prev = t[pos - 1]
|
|
117
|
+
if (prev === ' ' || prev === ':' || prev === '-' || prev === '/') score += 6
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 📖 Small penalty for very long labels so focused labels float up.
|
|
122
|
+
score -= Math.max(0, t.length - q.length)
|
|
123
|
+
|
|
124
|
+
return { matched: true, score, positions }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 📖 Filter and rank command palette entries by fuzzy score.
|
|
129
|
+
* @param {Array<{ id: string, label: string, category: string, keywords?: string[] }>} entries
|
|
130
|
+
* @param {string} query
|
|
131
|
+
* @returns {Array<{ id: string, label: string, category: string, shortcut?: string|null, keywords?: string[], score: number, matchPositions: number[] }>}
|
|
132
|
+
*/
|
|
133
|
+
export function filterCommandPaletteEntries(entries, query) {
|
|
134
|
+
const normalizedQuery = (query || '').trim()
|
|
135
|
+
|
|
136
|
+
const ranked = []
|
|
137
|
+
for (const entry of entries) {
|
|
138
|
+
const labelMatch = fuzzyMatchCommand(normalizedQuery, entry.label)
|
|
139
|
+
let bestScore = labelMatch.score
|
|
140
|
+
let matchPositions = labelMatch.positions
|
|
141
|
+
let matched = labelMatch.matched
|
|
142
|
+
|
|
143
|
+
if (!matched && Array.isArray(entry.keywords)) {
|
|
144
|
+
for (const keyword of entry.keywords) {
|
|
145
|
+
const keywordMatch = fuzzyMatchCommand(normalizedQuery, keyword)
|
|
146
|
+
if (!keywordMatch.matched) continue
|
|
147
|
+
matched = true
|
|
148
|
+
// 📖 Keyword matches should rank below direct label matches.
|
|
149
|
+
const keywordScore = Math.max(1, keywordMatch.score - 7)
|
|
150
|
+
if (keywordScore > bestScore) {
|
|
151
|
+
bestScore = keywordScore
|
|
152
|
+
matchPositions = []
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!matched) continue
|
|
158
|
+
ranked.push({ ...entry, score: bestScore, matchPositions })
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
ranked.sort((a, b) => {
|
|
162
|
+
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
|
|
166
|
+
return a.label.localeCompare(b.label)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
return ranked
|
|
170
|
+
}
|
package/src/config.js
CHANGED
|
@@ -210,7 +210,7 @@ function normalizeSettingsSection(settings) {
|
|
|
210
210
|
...safeSettings,
|
|
211
211
|
hideUnconfiguredModels: typeof safeSettings.hideUnconfiguredModels === 'boolean' ? safeSettings.hideUnconfiguredModels : true,
|
|
212
212
|
disableWidthsWarning: safeSettings.disableWidthsWarning === true,
|
|
213
|
-
theme: ['dark', 'light', 'auto'].includes(safeSettings.theme) ? safeSettings.theme : '
|
|
213
|
+
theme: ['dark', 'light', 'auto'].includes(safeSettings.theme) ? safeSettings.theme : 'auto',
|
|
214
214
|
}
|
|
215
215
|
}
|
|
216
216
|
|
|
@@ -231,7 +231,7 @@ function normalizeProfileSettings(settings) {
|
|
|
231
231
|
..._emptyProfileSettings(),
|
|
232
232
|
...safeSettings,
|
|
233
233
|
disableWidthsWarning: safeSettings.disableWidthsWarning === true,
|
|
234
|
-
theme: ['dark', 'light', 'auto'].includes(safeSettings.theme) ? safeSettings.theme : '
|
|
234
|
+
theme: ['dark', 'light', 'auto'].includes(safeSettings.theme) ? safeSettings.theme : 'auto',
|
|
235
235
|
}
|
|
236
236
|
}
|
|
237
237
|
|
|
@@ -844,7 +844,7 @@ export function _emptyProfileSettings() {
|
|
|
844
844
|
hideUnconfiguredModels: true, // 📖 true = default to providers that are actually configured
|
|
845
845
|
preferredToolMode: 'opencode', // 📖 remember the last Z-selected launcher across app restarts
|
|
846
846
|
disableWidthsWarning: false, // 📖 Disable widths warning (default off)
|
|
847
|
-
theme: '
|
|
847
|
+
theme: 'auto', // 📖 'auto' follows the terminal/OS theme, override with 'dark' or 'light' if needed
|
|
848
848
|
}
|
|
849
849
|
}
|
|
850
850
|
|