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 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.17",
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 || 'dark')
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
- : 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.config.settings?.disableWidthsWarning ?? false)
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 : 'dark',
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 : 'dark',
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: 'dark', // 📖 'dark', 'light', or 'auto'
847
+ theme: 'auto', // 📖 'auto' follows the terminal/OS theme, override with 'dark' or 'light' if needed
848
848
  }
849
849
  }
850
850