free-coding-models 0.3.21 → 0.3.22

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
@@ -1,7 +1,36 @@
1
1
  # Changelog
2
-
3
2
  ---
4
3
 
4
+ ## 0.3.22
5
+
6
+ ### Added
7
+ - **Hierarchical command palette navigation**: Ctrl+P now opens an improved command palette with expandable/collapsible categories and subcategories for better organization.
8
+ - **Arrow key navigation in command palette**: Use left/right arrows to expand and collapse categories, up/down to navigate, Enter to execute or toggle.
9
+ - **Rich color scheme for command palette**: Categories use bold blue headers, subcategories use bold text, commands show keyboard shortcuts.
10
+ - **Visual expand/collapse indicators**: ▼ shows expanded categories, ▶ shows collapsed, • marks individual commands.
11
+ - **Concise default view**: Categories are collapsed by default (except Filters) showing less detail on first open.
12
+ - **Colored tier filters**: Tier filters (S+, S, A+, A, A-, B+, B, C) now display with their corresponding TUI colors for quick identification.
13
+ - **Specific provider filters**: Added 13 individual provider filters (NVIDIA NIM, Groq, Cerebras, SambaNova, OpenRouter, Together AI, DeepInfra, Fireworks, Hyperbolic, Google AI, Hugging Face) with their signature provider colors.
14
+ - **Filter by model category**: New "Filter by model" category with autocomplete that dynamically shows top 20 visible models, complete with provider icons and colored model names.
15
+ - **Improved search cursor**: Cursor placement fixed to appear right after the `>` prompt instead of after placeholder text for natural typing flow.
16
+ - **Lightning bolt in title**: Command Palette title now shows ⚡ emoji for better visibility and visual appeal.
17
+
18
+ ### Changed
19
+ - **Footer layout cleanup**: Added consistent spacing before `F Toggle Favorite`, moved `N Changelog • Ctrl+C Exit` to a dedicated final footer line for better readability.
20
+ - **Removed Y key binding**: Install Endpoints is no longer a direct hotkey — accessible only via Settings (`P`) or Command Palette (`Ctrl+P`). The `Y` key is now free/unbound.
21
+ - **Removed proxy notice from footer**: The "Proxy is temporarily disabled" banner has been fully retired from the TUI footer and its backing constant removed.
22
+ - **Command palette fuzzy search fix**: `buildCommandPaletteEntries()` now expands all categories so child commands are visible to fuzzy search.
23
+ - **Flat Pages + Actions in ⚡️ Command Palette**: page entries and action entries are now listed directly at root level (no extra submenu depth) for faster access.
24
+ - **Command explanations after shortcuts**: command palette rows now show concise English hints after hotkeys (example: `(Z) — Change target AI Coding CLI Tool.`).
25
+ - **⚡️ emoji consistency**: user-facing Command Palette mentions now consistently render as `⚡️ Command Palette` in TUI and docs.
26
+ - **README Quick Start flow**: install section now explicitly adds “create a free account on one of the providers” with a direct anchor link.
27
+ - **Provider section renamed for clarity**: Quick Start provider table now lives under `List of Free AI Providers`.
28
+ - **Quick Start CTA highlight**: added a large green `USE ⚡️ COMMAND PALETTE / CTRL+P` badge right after the command palette instruction.
29
+
30
+ ### Fixed
31
+ - **Double emoji display bug**: Fixed duplicate emoji icons in command palette entries (was showing `▶ 🔍 🔍 Filters` instead of `▶ 🔍 Filters`).
32
+ - **Provider cycle command behavior**: `Cycle provider` in ⚡️ Command Palette now correctly cycles providers again instead of resetting to `All`.
33
+
5
34
  ## 0.3.21
6
35
 
7
36
  ### Changed
package/README.md CHANGED
@@ -22,12 +22,14 @@ npm install -g free-coding-models
22
22
  free-coding-models
23
23
  ```
24
24
 
25
+ create a free account on one of the [providers](#-list-of-free-ai-providers)
26
+
25
27
  </p>
26
28
 
27
29
  <p align="center">
28
30
  <a href="#-why-this-tool">Why</a> •
29
31
  <a href="#-quick-start">Quick Start</a> •
30
- <a href="#-providers">Providers</a> •
32
+ <a href="#-list-of-free-ai-providers">Providers</a> •
31
33
  <a href="#-usage">Usage</a> •
32
34
  <a href="#-tui-keys">TUI Keys</a> •
33
35
  <a href="#-contributing">Contributing</a>
@@ -55,7 +57,9 @@ It then writes the model you pick directly into your coding tool's config — so
55
57
 
56
58
  ## ⚡ Quick Start
57
59
 
58
- **① Get a free API key** — you only need one to get started:
60
+ ### 🟢 List of Free AI Providers
61
+
62
+ Create a free account on one provider below to get started:
59
63
 
60
64
  **160 coding models** across 20 providers, ranked by [SWE-bench Verified](https://www.swebench.com).
61
65
 
@@ -94,7 +98,7 @@ It then writes the model you pick directly into your coding tool's config — so
94
98
  | **A-/B+** | 30–40% | Smaller tasks, constrained infra |
95
99
  | **B/C** | < 30% | Code completion, edge/minimal setups |
96
100
 
97
- **② Install and run:**
101
+ **① Install and run:**
98
102
 
99
103
  ```bash
100
104
  npm install -g free-coding-models
@@ -103,9 +107,15 @@ free-coding-models
103
107
 
104
108
  On first run, you'll be prompted to enter your API key(s). You can skip providers and add more later with **`P`**.
105
109
 
110
+ Use ⚡️ Command Palette! with **Ctrl+P**.
111
+
112
+ <p align="center">
113
+ <img src="https://img.shields.io/badge/USE_%E2%9A%A1%EF%B8%8F%20COMMAND%20PALETTE-CTRL%2BP-22c55e?style=for-the-badge" alt="Use ⚡️ Command Palette with Ctrl+P">
114
+ </p>
115
+
106
116
  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
117
 
108
- **③ Pick a model and launch your tool:**
118
+ **② Pick a model and launch your tool:**
109
119
 
110
120
  ```
111
121
  ↑↓ navigate → Enter to launch
@@ -176,10 +186,9 @@ Press **`Z`** in the TUI to cycle between tools without restarting.
176
186
  | `E` | Toggle configured-only mode |
177
187
  | `F` | Favorite / unfavorite model |
178
188
  | `G` | Cycle global theme (`Auto → Dark → Light`) |
179
- | `Ctrl+P` | Open command palette (search + run actions) |
189
+ | `Ctrl+P` | Open ⚡️ command palette (search + run actions) |
180
190
  | `R/S/C/M/O/L/A/H/V/B/U` | Sort columns |
181
191
  | `P` | Settings (API keys, providers, updates, theme) |
182
- | `Y` | Install Endpoints (push provider into tool config) |
183
192
  | `Q` | Smart Recommend overlay |
184
193
  | `N` | Changelog |
185
194
  | `W` | Cycle ping cadence |
@@ -201,7 +210,7 @@ Press **`Z`** in the TUI to cycle between tools without restarting.
201
210
  - **Configured-only default** — only shows providers you have keys for
202
211
  - **Keyless latency** — models ping even without an API key (show 🔑 NO KEY)
203
212
  - **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
213
+ - **⚡️ Command Palette** — `Ctrl+P` opens a searchable action launcher for filters, sorting, overlays, and quick toggles
205
214
  - **Install Endpoints** — push a full provider catalog into any tool's config (`Y`)
206
215
  - **Missing tool bootstrap** — detect absent CLIs, offer one-click install, then continue the selected launch automatically
207
216
  - **Width guardrail** — shows a warning instead of a broken table in narrow terminals
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "free-coding-models",
3
- "version": "0.3.21",
3
+ "version": "0.3.22",
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
@@ -409,6 +409,7 @@ export async function runApp(cliArgs, config) {
409
409
  commandPaletteScrollOffset: 0, // 📖 Vertical scroll offset for the command palette result viewport.
410
410
  commandPaletteResults: [], // 📖 Cached fuzzy-filtered command entries for the command palette.
411
411
  commandPaletteFrozenTable: null, // 📖 Frozen table snapshot rendered behind the command palette overlay.
412
+ commandPaletteExpandedIds: new Set(['filters']), // 📖 Set of expanded category/subcategory IDs (filters expanded by default for quick access).
412
413
  helpVisible: false, // 📖 Whether the help overlay (K key) is active
413
414
  settingsScrollOffset: 0, // 📖 Vertical scroll offset for Settings overlay viewport
414
415
  helpScrollOffset: 0, // 📖 Vertical scroll offset for Help overlay viewport
@@ -457,6 +458,8 @@ export async function runApp(cliArgs, config) {
457
458
  changelogPhase: 'index', // 📖 'index' (all versions) | 'details' (specific version)
458
459
  changelogCursor: 0, // 📖 Selected row in index phase
459
460
  changelogSelectedVersion: null, // 📖 Which version to show details for
461
+ // 📖 Custom text filter (Ctrl+P palette → type text → Enter). Ephemeral — not saved to config.
462
+ customTextFilter: null, // 📖 Active free-text filter string (null = off). Matches model name, ctx, provider key/name.
460
463
  }
461
464
 
462
465
  // 📖 Re-clamp viewport on terminal resize
@@ -690,8 +693,22 @@ export async function runApp(cliArgs, config) {
690
693
  const allowedTiers = (activeTier && TIER_LETTER_MAP[activeTier]) ? TIER_LETTER_MAP[activeTier] : [activeTier]
691
694
  const tierHide = activeTier !== null && !allowedTiers.includes(r.tier)
692
695
  const originHide = activeOrigin !== null && r.providerKey !== activeOrigin
693
- r.hidden = tierHide || originHide
694
-
696
+ if (tierHide || originHide) {
697
+ r.hidden = true
698
+ return
699
+ }
700
+ // 📖 Custom text filter — case-insensitive includes match against model name, ctx, provider key, and provider display name.
701
+ if (state.customTextFilter) {
702
+ const q = state.customTextFilter.toLowerCase()
703
+ const providerName = (sources[r.providerKey]?.name || '').toLowerCase()
704
+ const match = (r.label || '').toLowerCase().includes(q)
705
+ || (r.ctx || '').toLowerCase().includes(q)
706
+ || (r.providerKey || '').toLowerCase().includes(q)
707
+ || providerName.includes(q)
708
+ r.hidden = !match
709
+ return
710
+ }
711
+ r.hidden = false
695
712
  })
696
713
  return state.results
697
714
  }
@@ -1,64 +1,200 @@
1
1
  /**
2
2
  * @file command-palette.js
3
3
  * @description Command palette registry and fuzzy search helpers for the main TUI.
4
+ * Now supports hierarchical categories with expandable/collapsible groups.
4
5
  *
5
6
  * @functions
6
- * → `buildCommandPaletteEntries` — builds the current command list with dynamic provider/tier context
7
+ * → `buildCommandPaletteTree` — builds the hierarchical command tree with categories and subcategories
8
+ * → `flattenCommandTree` — converts tree to flat list for filtering (respects expansion state)
7
9
  * → `fuzzyMatchCommand` — scores a query against one string and returns match positions
8
10
  * → `filterCommandPaletteEntries` — returns sorted command matches for a query
9
11
  *
10
- * @exports { COMMAND_CATEGORY_ORDER, buildCommandPaletteEntries, fuzzyMatchCommand, filterCommandPaletteEntries }
12
+ * @exports { buildCommandPaletteTree, flattenCommandTree, fuzzyMatchCommand, filterCommandPaletteEntries }
11
13
  *
12
14
  * @see src/key-handler.js
13
15
  * @see src/overlays.js
14
16
  */
15
17
 
16
- export const COMMAND_CATEGORY_ORDER = ['Filters', 'Sort', 'Pages', 'Actions']
17
-
18
- const COMMANDS = [
19
- // 📖 Filters
20
- { id: 'filter-tier-all', category: 'Filters', label: 'Filter tiers: all', shortcut: 'T', keywords: ['filter', 'tier', 'all'] },
21
- { id: 'filter-tier-splus', category: 'Filters', label: 'Filter tiers: S+', shortcut: null, keywords: ['filter', 'tier', 's+'] },
22
- { id: 'filter-tier-s', category: 'Filters', label: 'Filter tiers: S', shortcut: null, keywords: ['filter', 'tier', 's'] },
23
- { id: 'filter-tier-aplus', category: 'Filters', label: 'Filter tiers: A+', shortcut: null, keywords: ['filter', 'tier', 'a+'] },
24
- { id: 'filter-tier-a', category: 'Filters', label: 'Filter tiers: A', shortcut: null, keywords: ['filter', 'tier', 'a'] },
25
- { id: 'filter-tier-aminus', category: 'Filters', label: 'Filter tiers: A-', shortcut: null, keywords: ['filter', 'tier', 'a-'] },
26
- { id: 'filter-tier-bplus', category: 'Filters', label: 'Filter tiers: B+', shortcut: null, keywords: ['filter', 'tier', 'b+'] },
27
- { id: 'filter-tier-b', category: 'Filters', label: 'Filter tiers: B', shortcut: null, keywords: ['filter', 'tier', 'b'] },
28
- { id: 'filter-tier-c', category: 'Filters', label: 'Filter tiers: C', shortcut: null, keywords: ['filter', 'tier', 'c'] },
29
- { id: 'filter-provider-cycle', category: 'Filters', label: 'Filter provider: cycle', shortcut: 'D', keywords: ['filter', 'provider', 'origin'] },
30
- { id: 'filter-configured-toggle', category: 'Filters', label: 'Toggle configured-only models', shortcut: 'E', keywords: ['filter', 'configured', 'keys'] },
31
-
32
- // 📖 Sorting
33
- { id: 'sort-rank', category: 'Sort', label: 'Sort by rank', shortcut: 'R', keywords: ['sort', 'rank'] },
34
- { id: 'sort-tier', category: 'Sort', label: 'Sort by tier', shortcut: null, keywords: ['sort', 'tier'] },
35
- { id: 'sort-provider', category: 'Sort', label: 'Sort by provider', shortcut: 'O', keywords: ['sort', 'origin', 'provider'] },
36
- { id: 'sort-model', category: 'Sort', label: 'Sort by model name', shortcut: 'M', keywords: ['sort', 'model', 'name'] },
37
- { id: 'sort-latest-ping', category: 'Sort', label: 'Sort by latest ping', shortcut: 'L', keywords: ['sort', 'latest', 'ping'] },
38
- { id: 'sort-avg-ping', category: 'Sort', label: 'Sort by average ping', shortcut: 'A', keywords: ['sort', 'avg', 'average', 'ping'] },
39
- { id: 'sort-swe', category: 'Sort', label: 'Sort by SWE score', shortcut: 'S', keywords: ['sort', 'swe', 'score'] },
40
- { id: 'sort-ctx', category: 'Sort', label: 'Sort by context window', shortcut: 'C', keywords: ['sort', 'context', 'ctx'] },
41
- { id: 'sort-health', category: 'Sort', label: 'Sort by health', shortcut: 'H', keywords: ['sort', 'health', 'condition'] },
42
- { id: 'sort-verdict', category: 'Sort', label: 'Sort by verdict', shortcut: 'V', keywords: ['sort', 'verdict'] },
43
- { id: 'sort-stability', category: 'Sort', label: 'Sort by stability', shortcut: 'B', keywords: ['sort', 'stability'] },
44
- { id: 'sort-uptime', category: 'Sort', label: 'Sort by uptime', shortcut: 'U', keywords: ['sort', 'uptime'] },
45
-
46
- // 📖 Pages / overlays
47
- { id: 'open-settings', category: 'Pages', label: 'Open settings', shortcut: 'P', keywords: ['settings', 'config', 'api key'] },
48
- { id: 'open-help', category: 'Pages', label: 'Open help', shortcut: 'K', keywords: ['help', 'shortcuts', 'hotkeys'] },
49
- { id: 'open-changelog', category: 'Pages', label: 'Open changelog', shortcut: 'N', keywords: ['changelog', 'release'] },
50
- { id: 'open-feedback', category: 'Pages', label: 'Open feedback', shortcut: 'I', keywords: ['feedback', 'bug', 'request'] },
51
- { id: 'open-recommend', category: 'Pages', label: 'Open smart recommend', shortcut: 'Q', keywords: ['recommend', 'best model'] },
52
- { id: 'open-install-endpoints', category: 'Pages', label: 'Open install endpoints', shortcut: 'Y', keywords: ['install', 'endpoints', 'providers'] },
53
-
54
- // 📖 Actions
55
- { id: 'action-cycle-theme', category: 'Actions', label: 'Cycle theme', shortcut: 'G', keywords: ['theme', 'dark', 'light', 'auto'] },
56
- { id: 'action-cycle-tool-mode', category: 'Actions', label: 'Cycle tool mode', shortcut: 'Z', keywords: ['tool', 'mode', 'launcher'] },
57
- { id: 'action-cycle-ping-mode', category: 'Actions', label: 'Cycle ping mode', shortcut: 'W', keywords: ['ping', 'cadence', 'speed', 'slow'] },
58
- { id: 'action-toggle-favorite', category: 'Actions', label: 'Toggle favorite on selected model', shortcut: 'F', keywords: ['favorite', 'star'] },
59
- { id: 'action-reset-view', category: 'Actions', label: 'Reset view settings', shortcut: 'Shift+R', keywords: ['reset', 'view', 'sort', 'filters'] },
18
+ // 📖 Base command tree template (will be enhanced with dynamic model list)
19
+ const BASE_COMMAND_TREE = [
20
+ {
21
+ id: 'filters',
22
+ label: 'Filters',
23
+ icon: '🔍',
24
+ children: [
25
+ {
26
+ id: 'filter-tier',
27
+ label: 'Filter by tier',
28
+ icon: '📊',
29
+ children: [
30
+ { id: 'filter-tier-all', label: 'All tiers', tier: null, shortcut: 'T', description: 'Show all models', keywords: ['filter', 'tier', 'all'] },
31
+ { id: 'filter-tier-splus', label: 'S+ tier', tier: 'S+', description: 'Best coding models', keywords: ['filter', 'tier', 's+'] },
32
+ { id: 'filter-tier-s', label: 'S tier', tier: 'S', description: 'Excellent models', keywords: ['filter', 'tier', 's'] },
33
+ { id: 'filter-tier-aplus', label: 'A+ tier', tier: 'A+', description: 'Very good models', keywords: ['filter', 'tier', 'a+'] },
34
+ { id: 'filter-tier-a', label: 'A tier', tier: 'A', description: 'Good models', keywords: ['filter', 'tier', 'a'] },
35
+ { id: 'filter-tier-aminus', label: 'A- tier', tier: 'A-', description: 'Solid models', keywords: ['filter', 'tier', 'a-'] },
36
+ { id: 'filter-tier-bplus', label: 'B+ tier', tier: 'B+', description: 'Fair models', keywords: ['filter', 'tier', 'b+'] },
37
+ { id: 'filter-tier-b', label: 'B tier', tier: 'B', description: 'Basic models', keywords: ['filter', 'tier', 'b'] },
38
+ { id: 'filter-tier-c', label: 'C tier', tier: 'C', description: 'Limited models', keywords: ['filter', 'tier', 'c'] },
39
+ ]
40
+ },
41
+ {
42
+ id: 'filter-provider',
43
+ label: 'Filter by provider',
44
+ icon: '🏢',
45
+ children: [
46
+ { id: 'filter-provider-cycle', label: 'Cycle provider', shortcut: 'D', description: 'Switch between providers', keywords: ['filter', 'provider', 'origin'] },
47
+ { id: 'filter-provider-all', label: 'All providers', providerKey: null, description: 'Show all providers', keywords: ['filter', 'provider', 'all'] },
48
+ { id: 'filter-provider-nvidia', label: 'NVIDIA NIM', providerKey: 'nvidiaNim', description: 'NVIDIA models', keywords: ['filter', 'provider', 'nvidia', 'nim'] },
49
+ { id: 'filter-provider-groq', label: 'Groq', providerKey: 'groq', description: 'Groq models', keywords: ['filter', 'provider', 'groq'] },
50
+ { id: 'filter-provider-cerebras', label: 'Cerebras', providerKey: 'cerebras', description: 'Cerebras models', keywords: ['filter', 'provider', 'cerebras'] },
51
+ { id: 'filter-provider-sambanova', label: 'SambaNova', providerKey: 'sambanova', description: 'SambaNova models', keywords: ['filter', 'provider', 'sambanova'] },
52
+ { id: 'filter-provider-openrouter', label: 'OpenRouter', providerKey: 'openrouter', description: 'OpenRouter models', keywords: ['filter', 'provider', 'openrouter'] },
53
+ { id: 'filter-provider-together', label: 'Together AI', providerKey: 'together', description: 'Together models', keywords: ['filter', 'provider', 'together'] },
54
+ { id: 'filter-provider-deepinfra', label: 'DeepInfra', providerKey: 'deepinfra', description: 'DeepInfra models', keywords: ['filter', 'provider', 'deepinfra'] },
55
+ { id: 'filter-provider-fireworks', label: 'Fireworks AI', providerKey: 'fireworks', description: 'Fireworks models', keywords: ['filter', 'provider', 'fireworks'] },
56
+ { id: 'filter-provider-hyperbolic', label: 'Hyperbolic', providerKey: 'hyperbolic', description: 'Hyperbolic models', keywords: ['filter', 'provider', 'hyperbolic'] },
57
+ { id: 'filter-provider-google', label: 'Google AI', providerKey: 'google', description: 'Google models', keywords: ['filter', 'provider', 'google'] },
58
+ { id: 'filter-provider-huggingface', label: 'Hugging Face', providerKey: 'huggingface', description: 'Hugging Face models', keywords: ['filter', 'provider', 'huggingface'] },
59
+ ]
60
+ },
61
+ {
62
+ id: 'filter-model',
63
+ label: 'Filter by model',
64
+ icon: '🤖',
65
+ children: []
66
+ },
67
+ {
68
+ id: 'filter-other',
69
+ label: 'Other filters',
70
+ icon: '⚙️',
71
+ children: [
72
+ { id: 'filter-configured-toggle', label: 'Toggle configured-only', shortcut: 'E', description: 'Show only configured providers', keywords: ['filter', 'configured', 'keys'] },
73
+ ]
74
+ },
75
+ ]
76
+ },
77
+ {
78
+ id: 'sort',
79
+ label: 'Sort',
80
+ icon: '📶',
81
+ children: [
82
+ { id: 'sort-rank', label: 'Sort by rank', shortcut: 'R', description: 'Rank by SWE score', keywords: ['sort', 'rank'] },
83
+ { id: 'sort-tier', label: 'Sort by tier', description: 'Group by quality tier', keywords: ['sort', 'tier'] },
84
+ { id: 'sort-provider', label: 'Sort by provider', shortcut: 'O', description: 'Group by provider', keywords: ['sort', 'origin', 'provider'] },
85
+ { id: 'sort-model', label: 'Sort by model', shortcut: 'M', description: 'Alphabetical order', keywords: ['sort', 'model', 'name'] },
86
+ { id: 'sort-latest-ping', label: 'Sort by latest ping', shortcut: 'L', description: 'Recent response time', keywords: ['sort', 'latest', 'ping'] },
87
+ { id: 'sort-avg-ping', label: 'Sort by avg ping', shortcut: 'A', description: 'Average response time', keywords: ['sort', 'avg', 'average', 'ping'] },
88
+ { id: 'sort-swe', label: 'Sort by SWE score', shortcut: 'S', description: 'Coding ability score', keywords: ['sort', 'swe', 'score'] },
89
+ { id: 'sort-ctx', label: 'Sort by context', shortcut: 'C', description: 'Context window size', keywords: ['sort', 'context', 'ctx'] },
90
+ { id: 'sort-health', label: 'Sort by health', shortcut: 'H', description: 'Current model status', keywords: ['sort', 'health', 'condition'] },
91
+ { id: 'sort-verdict', label: 'Sort by verdict', shortcut: 'V', description: 'Overall assessment', keywords: ['sort', 'verdict'] },
92
+ { id: 'sort-stability', label: 'Sort by stability', shortcut: 'B', description: 'Reliability score', keywords: ['sort', 'stability'] },
93
+ { id: 'sort-uptime', label: 'Sort by uptime', shortcut: 'U', description: 'Success rate', keywords: ['sort', 'uptime'] },
94
+ ]
95
+ },
96
+ // 📖 Pages - directly at root level, not in submenu
97
+ { id: 'open-settings', label: 'Settings', shortcut: 'P', icon: '⚙️', type: 'page', description: 'API keys and preferences', keywords: ['settings', 'config', 'api key'] },
98
+ { id: 'open-help', label: 'Help', shortcut: 'K', icon: '❓', type: 'page', description: 'Show all shortcuts', keywords: ['help', 'shortcuts', 'hotkeys'] },
99
+ { id: 'open-changelog', label: 'Changelog', shortcut: 'N', icon: '📋', type: 'page', description: 'Version history', keywords: ['changelog', 'release'] },
100
+ { id: 'open-feedback', label: 'Feedback', shortcut: 'I', icon: '📝', type: 'page', description: 'Report bugs or requests', keywords: ['feedback', 'bug', 'request'] },
101
+ { id: 'open-recommend', label: 'Smart recommend', shortcut: 'Q', icon: '🎯', type: 'page', description: 'Find best model for task', keywords: ['recommend', 'best model'] },
102
+ { 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'] },
60
109
  ]
61
110
 
111
+ /**
112
+ * 📖 Build the command palette tree with dynamic model filters.
113
+ * @param {Array} visibleModels - Optional list of visible models to create model filter entries
114
+ * @returns {Array} The command tree with model filters added
115
+ */
116
+ export function buildCommandPaletteTree(visibleModels = []) {
117
+ // 📖 Clone the base tree
118
+ const tree = JSON.parse(JSON.stringify(BASE_COMMAND_TREE))
119
+
120
+ // 📖 Find the filter-model category and add dynamic model entries
121
+ const filterModelCategory = tree.find(cat => cat.id === 'filters')
122
+ ?.children.find(sub => sub.id === 'filter-model')
123
+
124
+ if (filterModelCategory && Array.isArray(visibleModels) && visibleModels.length > 0) {
125
+ // 📖 Add top 20 most-used or most relevant models
126
+ const topModels = visibleModels
127
+ .filter(m => !m.hidden && m.status !== 'noauth')
128
+ .slice(0, 20)
129
+
130
+ for (const model of topModels) {
131
+ filterModelCategory.children.push({
132
+ id: `filter-model-${model.providerKey}-${model.modelId}`,
133
+ label: model.label,
134
+ modelId: model.modelId,
135
+ providerKey: model.providerKey,
136
+ keywords: ['filter', 'model', model.label.toLowerCase(), model.modelId.toLowerCase()],
137
+ })
138
+ }
139
+ }
140
+
141
+ return tree
142
+ }
143
+
144
+ /**
145
+ * 📖 Flatten the command tree into a list, respecting which nodes are expanded.
146
+ * @param {Array} tree - The command tree
147
+ * @param {Set} expandedIds - Set of IDs that are expanded
148
+ * @returns {Array} Flat list with type markers ('category' | 'subcategory' | 'command' | 'page' | 'action')
149
+ */
150
+ export function flattenCommandTree(tree, expandedIds = new Set()) {
151
+ const result = []
152
+
153
+ function traverse(nodes, depth = 0) {
154
+ for (const node of nodes) {
155
+ // 📖 Check if this is a direct page/action (not in a submenu)
156
+ if (node.type === 'page' || node.type === 'action') {
157
+ result.push({
158
+ ...node,
159
+ type: node.type,
160
+ depth: 0,
161
+ hasChildren: false,
162
+ isExpanded: false,
163
+ })
164
+ continue
165
+ }
166
+
167
+ const isExpanded = expandedIds.has(node.id)
168
+ const hasChildren = Array.isArray(node.children) && node.children.length > 0
169
+
170
+ if (hasChildren) {
171
+ result.push({
172
+ ...node,
173
+ type: depth === 0 ? 'category' : 'subcategory',
174
+ depth,
175
+ hasChildren,
176
+ isExpanded,
177
+ })
178
+
179
+ if (isExpanded) {
180
+ traverse(node.children, depth + 1)
181
+ }
182
+ } else {
183
+ result.push({
184
+ ...node,
185
+ type: 'command',
186
+ depth,
187
+ hasChildren: false,
188
+ isExpanded: false,
189
+ })
190
+ }
191
+ }
192
+ }
193
+
194
+ traverse(tree)
195
+ return result
196
+ }
197
+
62
198
  const ID_TO_TIER = {
63
199
  'filter-tier-all': null,
64
200
  'filter-tier-splus': 'S+',
@@ -71,11 +207,31 @@ const ID_TO_TIER = {
71
207
  'filter-tier-c': 'C',
72
208
  }
73
209
 
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
- }))
210
+ /**
211
+ * 📖 Legacy function for backward compatibility - builds flat list from tree.
212
+ * 📖 Expands all categories so every command is searchable by fuzzyMatchCommand.
213
+ * @param {Array} visibleModels - Optional list of visible models for model filter entries
214
+ */
215
+ export function buildCommandPaletteEntries(visibleModels = []) {
216
+ const tree = buildCommandPaletteTree(visibleModels)
217
+ // 📖 Collect every node id that has children so flattenCommandTree traverses into them.
218
+ const allIds = new Set()
219
+ function collectIds(nodes) {
220
+ for (const n of nodes) {
221
+ allIds.add(n.id)
222
+ if (Array.isArray(n.children)) collectIds(n.children)
223
+ }
224
+ }
225
+ collectIds(tree)
226
+ const flat = flattenCommandTree(tree, allIds)
227
+ return flat.map((entry) => {
228
+ // 📖 Copy tier and providerKey properties to tierValue for backward compatibility
229
+ const result = { ...entry }
230
+ if (entry.tier !== undefined) {
231
+ result.tierValue = entry.tier
232
+ }
233
+ return result
234
+ })
79
235
  }
80
236
 
81
237
  /**
@@ -126,15 +282,20 @@ export function fuzzyMatchCommand(query, text) {
126
282
 
127
283
  /**
128
284
  * 📖 Filter and rank command palette entries by fuzzy score.
129
- * @param {Array<{ id: string, label: string, category: string, keywords?: string[] }>} entries
285
+ * Now handles hierarchical structure with expandable categories.
286
+ * @param {Array} flatEntries - Flattened command tree entries
130
287
  * @param {string} query
131
- * @returns {Array<{ id: string, label: string, category: string, shortcut?: string|null, keywords?: string[], score: number, matchPositions: number[] }>}
288
+ * @returns {Array} Sorted and filtered entries with match scores
132
289
  */
133
- export function filterCommandPaletteEntries(entries, query) {
290
+ export function filterCommandPaletteEntries(flatEntries, query) {
134
291
  const normalizedQuery = (query || '').trim()
292
+
293
+ if (!normalizedQuery) {
294
+ return flatEntries
295
+ }
135
296
 
136
297
  const ranked = []
137
- for (const entry of entries) {
298
+ for (const entry of flatEntries) {
138
299
  const labelMatch = fuzzyMatchCommand(normalizedQuery, entry.label)
139
300
  let bestScore = labelMatch.score
140
301
  let matchPositions = labelMatch.positions
@@ -145,7 +306,6 @@ export function filterCommandPaletteEntries(entries, query) {
145
306
  const keywordMatch = fuzzyMatchCommand(normalizedQuery, keyword)
146
307
  if (!keywordMatch.matched) continue
147
308
  matched = true
148
- // 📖 Keyword matches should rank below direct label matches.
149
309
  const keywordScore = Math.max(1, keywordMatch.score - 7)
150
310
  if (keywordScore > bestScore) {
151
311
  bestScore = keywordScore
@@ -158,11 +318,24 @@ export function filterCommandPaletteEntries(entries, query) {
158
318
  ranked.push({ ...entry, score: bestScore, matchPositions })
159
319
  }
160
320
 
321
+ // Auto-expand categories that contain matches
322
+ const result = []
323
+ const idsToExpand = new Set()
324
+
325
+ // First pass: mark all categories containing matched items
326
+ for (const entry of ranked) {
327
+ if (entry.type === 'command' && entry.matchPositions) {
328
+ // Find parent categories
329
+ let current = result.find(r => r.id === entry.id)
330
+ if (current) {
331
+ idsToExpand.add(entry.parentId)
332
+ }
333
+ }
334
+ }
335
+
161
336
  ranked.sort((a, b) => {
162
337
  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
338
+ if (a.depth !== b.depth) return a.depth - b.depth
166
339
  return a.label.localeCompare(b.label)
167
340
  })
168
341
 
@@ -31,7 +31,7 @@ import { loadChangelog } from './changelog-loader.js'
31
31
  import { loadConfig, replaceConfigContents } from './config.js'
32
32
  import { cleanupLegacyProxyArtifacts } from './legacy-proxy-cleanup.js'
33
33
  import { cycleThemeSetting, detectActiveTheme } from './theme.js'
34
- import { buildCommandPaletteEntries, filterCommandPaletteEntries } from './command-palette.js'
34
+ import { buildCommandPaletteTree, flattenCommandTree, filterCommandPaletteEntries } from './command-palette.js'
35
35
  import { WIDTH_WARNING_MIN_COLS } from './constants.js'
36
36
 
37
37
  // 📖 Some providers need an explicit probe model because the first catalog entry
@@ -609,6 +609,7 @@ export function createKeyHandler(ctx) {
609
609
  function resetViewSettings() {
610
610
  state.tierFilterMode = 0
611
611
  state.originFilterMode = 0
612
+ state.customTextFilter = null // 📖 Clear ephemeral text filter on view reset
612
613
  state.sortColumn = 'avg'
613
614
  state.sortDirection = 'asc'
614
615
  if (!state.config.settings || typeof state.config.settings !== 'object') state.config.settings = {}
@@ -653,8 +654,33 @@ export function createKeyHandler(ctx) {
653
654
  }
654
655
 
655
656
  function refreshCommandPaletteResults() {
656
- const commands = buildCommandPaletteEntries()
657
- state.commandPaletteResults = filterCommandPaletteEntries(commands, state.commandPaletteQuery)
657
+ const tree = buildCommandPaletteTree(state.results || [])
658
+ const flat = flattenCommandTree(tree, state.commandPaletteExpandedIds)
659
+ state.commandPaletteResults = filterCommandPaletteEntries(flat, state.commandPaletteQuery)
660
+
661
+ const query = (state.commandPaletteQuery || '').trim()
662
+ if (query.length > 0) {
663
+ state.commandPaletteResults.unshift({
664
+ id: 'filter-custom-text-apply',
665
+ label: `🔍 Apply text filter: ${query}`,
666
+ type: 'command',
667
+ depth: 0,
668
+ hasChildren: false,
669
+ isExpanded: false,
670
+ filterQuery: query,
671
+ })
672
+ } else if (state.customTextFilter) {
673
+ state.commandPaletteResults.unshift({
674
+ id: 'filter-custom-text-remove',
675
+ label: `❌ Remove custom filter: ${state.customTextFilter}`,
676
+ type: 'command',
677
+ depth: 0,
678
+ hasChildren: false,
679
+ isExpanded: false,
680
+ filterQuery: null,
681
+ })
682
+ }
683
+
658
684
  if (state.commandPaletteCursor >= state.commandPaletteResults.length) {
659
685
  state.commandPaletteCursor = Math.max(0, state.commandPaletteResults.length - 1)
660
686
  }
@@ -682,7 +708,43 @@ export function createKeyHandler(ctx) {
682
708
  if (!entry?.id) return
683
709
 
684
710
  if (entry.id.startsWith('filter-tier-')) {
685
- setTierFilterFromCommand(entry.tierValue ?? null)
711
+ setTierFilterFromCommand(entry.tier ?? null)
712
+ return
713
+ }
714
+
715
+ if (entry.id.startsWith('filter-provider-') && entry.id !== 'filter-provider-cycle') {
716
+ if (entry.providerKey === null || entry.providerKey === undefined) {
717
+ state.originFilterMode = 0 // All
718
+ } else {
719
+ state.originFilterMode = ORIGIN_CYCLE.findIndex(key => key === entry.providerKey) + 1
720
+ if (state.originFilterMode <= 0) state.originFilterMode = 0
721
+ }
722
+ applyTierFilter()
723
+ refreshVisibleSorted({ resetCursor: true })
724
+ persistUiSettings()
725
+ return
726
+ }
727
+
728
+ if (entry.id.startsWith('filter-model-')) {
729
+ if (entry.modelId && entry.providerKey) {
730
+ state.customTextFilter = `${entry.providerKey}/${entry.modelId}`
731
+ applyTierFilter()
732
+ refreshVisibleSorted({ resetCursor: true })
733
+ }
734
+ return
735
+ }
736
+
737
+ // 📖 Custom text filter — apply or remove the free-text filter from the command palette.
738
+ if (entry.id === 'filter-custom-text-apply') {
739
+ state.customTextFilter = entry.filterQuery || null
740
+ applyTierFilter()
741
+ refreshVisibleSorted({ resetCursor: true })
742
+ return
743
+ }
744
+ if (entry.id === 'filter-custom-text-remove') {
745
+ state.customTextFilter = null
746
+ applyTierFilter()
747
+ refreshVisibleSorted({ resetCursor: true })
686
748
  return
687
749
  }
688
750
 
@@ -758,6 +820,7 @@ export function createKeyHandler(ctx) {
758
820
  if (key.ctrl && key.name === 'c') { exit(0); return }
759
821
 
760
822
  const pageStep = Math.max(1, (state.terminalRows || 1) - 10)
823
+ const selected = state.commandPaletteResults[state.commandPaletteCursor]
761
824
 
762
825
  if (key.name === 'escape') {
763
826
  closeCommandPalette()
@@ -775,6 +838,23 @@ export function createKeyHandler(ctx) {
775
838
  state.commandPaletteCursor = state.commandPaletteCursor < count - 1 ? state.commandPaletteCursor + 1 : 0
776
839
  return
777
840
  }
841
+ if (key.name === 'left') {
842
+ if (selected?.hasChildren && selected.isExpanded) {
843
+ state.commandPaletteExpandedIds.delete(selected.id)
844
+ refreshCommandPaletteResults()
845
+ }
846
+ return
847
+ }
848
+ if (key.name === 'right') {
849
+ if (selected?.hasChildren && !selected.isExpanded) {
850
+ state.commandPaletteExpandedIds.add(selected.id)
851
+ refreshCommandPaletteResults()
852
+ } else if (selected?.type === 'command') {
853
+ closeCommandPalette()
854
+ executeCommandPaletteEntry(selected)
855
+ }
856
+ return
857
+ }
778
858
  if (key.name === 'pageup') {
779
859
  state.commandPaletteCursor = Math.max(0, state.commandPaletteCursor - pageStep)
780
860
  return
@@ -800,9 +880,17 @@ export function createKeyHandler(ctx) {
800
880
  return
801
881
  }
802
882
  if (key.name === 'return') {
803
- const selectedCommand = state.commandPaletteResults[state.commandPaletteCursor]
804
- closeCommandPalette()
805
- executeCommandPaletteEntry(selectedCommand)
883
+ if (selected?.hasChildren) {
884
+ if (selected.isExpanded) {
885
+ state.commandPaletteExpandedIds.delete(selected.id)
886
+ } else {
887
+ state.commandPaletteExpandedIds.add(selected.id)
888
+ }
889
+ refreshCommandPaletteResults()
890
+ } else {
891
+ closeCommandPalette()
892
+ executeCommandPaletteEntry(selected)
893
+ }
806
894
  return
807
895
  }
808
896
  if (str && str.length === 1 && !key.ctrl && !key.meta) {
@@ -1619,11 +1707,7 @@ export function createKeyHandler(ctx) {
1619
1707
  return
1620
1708
  }
1621
1709
 
1622
- // 📖 Y key: open Install Endpoints flow for configured providers.
1623
- if (key.name === 'y') {
1624
- openInstallEndpointsOverlay()
1625
- return
1626
- }
1710
+ // 📖 Y key freed Install Endpoints is now accessible only via Settings (P) or Command Palette (Ctrl+P).
1627
1711
 
1628
1712
  // 📖 Profile system removed - API keys now persist permanently across all sessions
1629
1713
 
@@ -1636,7 +1720,7 @@ export function createKeyHandler(ctx) {
1636
1720
  }
1637
1721
 
1638
1722
  // 📖 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
1639
- // 📖 T is reserved for tier filter cycling. Y now opens the install-endpoints flow.
1723
+ // 📖 T is reserved for tier filter cycling. Y is now free (Install Endpoints moved to Settings/Palette).
1640
1724
  // 📖 D is now reserved for provider filter cycling
1641
1725
  // 📖 Shift+R is reserved for reset view settings
1642
1726
  const sortKeys = {
package/src/overlays.js CHANGED
@@ -528,18 +528,16 @@ export function createOverlayRenderers(state, deps) {
528
528
 
529
529
  // ─── Command palette renderer ──────────────────────────────────────────────
530
530
  // 📖 renderCommandPalette draws a centered floating modal over the live table.
531
- // 📖 It returns cursor-positioned ANSI rows instead of replacing the full screen,
532
- // 📖 so ping updates continue to animate in the background behind the palette.
531
+ // 📖 Supports hierarchical categories with expand/collapse and rich colors.
533
532
  function renderCommandPalette() {
534
533
  const terminalRows = state.terminalRows || 24
535
534
  const terminalCols = state.terminalCols || 80
536
- const panelWidth = Math.max(44, Math.min(96, terminalCols - 8))
537
- const panelInnerWidth = Math.max(28, panelWidth - 4)
535
+ const panelWidth = Math.max(52, Math.min(100, terminalCols - 8))
536
+ const panelInnerWidth = Math.max(32, panelWidth - 4)
538
537
  const panelPad = 2
539
538
  const panelOuterWidth = panelWidth + (panelPad * 2)
540
- const footerRowCount = 2
541
- const headerRowCount = 3
542
- const bodyRows = Math.max(6, Math.min(16, terminalRows - 12))
539
+ const headerRowCount = 4
540
+ const bodyRows = Math.max(8, Math.min(18, terminalRows - 12))
543
541
 
544
542
  const truncatePlain = (text, width) => {
545
543
  if (width <= 1) return ''
@@ -559,30 +557,70 @@ export function createOverlayRenderers(state, deps) {
559
557
  }
560
558
 
561
559
  const allResults = Array.isArray(state.commandPaletteResults) ? state.commandPaletteResults.slice(0, 80) : []
562
- const groupedLines = []
560
+ const panelLines = []
563
561
  const cursorLineByRow = {}
564
- let category = null
565
562
 
566
563
  if (allResults.length === 0) {
567
- groupedLines.push(themeColors.dim(' No command found. Try a broader query.'))
564
+ panelLines.push(themeColors.dim(' No commands found. Try a different search.'))
568
565
  } else {
569
566
  for (let idx = 0; idx < allResults.length; idx++) {
570
567
  const entry = allResults[idx]
571
- if (entry.category !== category) {
572
- category = entry.category
573
- groupedLines.push(themeColors.textBold(` ${category}`))
568
+ const isCursor = idx === state.commandPaletteCursor
569
+
570
+ const indent = ' '.repeat(entry.depth || 0)
571
+ const expandIndicator = entry.hasChildren
572
+ ? (entry.isExpanded ? themeColors.infoBold('▼') : themeColors.dim('▶'))
573
+ : themeColors.dim('•')
574
+
575
+ // 📖 Only use icon from entry, label should NOT include emoji
576
+ const iconPrefix = entry.icon ? `${entry.icon} ` : ''
577
+ const plainLabel = truncatePlain(entry.label, panelInnerWidth - indent.length - iconPrefix.length - 4)
578
+ const label = entry.matchPositions ? highlightMatch(plainLabel, entry.matchPositions) : plainLabel
579
+
580
+ let rowLine
581
+ if (entry.type === 'category') {
582
+ rowLine = `${indent}${expandIndicator} ${iconPrefix}${themeColors.headerBold(label)}`
583
+ } else if (entry.type === 'subcategory') {
584
+ rowLine = `${indent}${expandIndicator} ${iconPrefix}${themeColors.textBold(label)}`
585
+ } else if (entry.type === 'page') {
586
+ // 📖 Pages are at root level with icon + label + shortcut + description
587
+ const shortcut = entry.shortcut ? themeColors.dim(` (${entry.shortcut})`) : ''
588
+ const description = entry.description ? themeColors.dim(` — ${entry.description}`) : ''
589
+ rowLine = `${expandIndicator} ${iconPrefix}${themeColors.textBold(label)}${shortcut}${description}`
590
+ } else if (entry.type === 'action') {
591
+ // 📖 Actions are at root level with icon + label + shortcut + description
592
+ const shortcut = entry.shortcut ? themeColors.dim(` (${entry.shortcut})`) : ''
593
+ const description = entry.description ? themeColors.dim(` — ${entry.description}`) : ''
594
+ rowLine = `${expandIndicator} ${iconPrefix}${themeColors.textBold(label)}${shortcut}${description}`
595
+ } else {
596
+ // 📖 Regular commands in submenus
597
+ const shortcut = entry.shortcut ? themeColors.dim(` (${entry.shortcut})`) : ''
598
+ const description = entry.description ? themeColors.dim(` — ${entry.description}`) : ''
599
+ // 📖 Color tiers and providers
600
+ let coloredLabel = label
601
+ let prefixWithIcon = iconPrefix
602
+
603
+ if (entry.providerKey && !entry.icon) {
604
+ // 📖 Model filter: add provider icon
605
+ const providerIcon = '🏢'
606
+ prefixWithIcon = `${providerIcon} `
607
+ coloredLabel = themeColors.provider(entry.providerKey, label, { bold: false })
608
+ } else if (entry.tier) {
609
+ coloredLabel = themeColors.tier(entry.tier, label)
610
+ } else if (entry.providerKey) {
611
+ coloredLabel = themeColors.provider(entry.providerKey, label, { bold: false })
612
+ }
613
+
614
+ rowLine = `${indent} ${expandIndicator} ${prefixWithIcon}${coloredLabel}${shortcut}${description}`
574
615
  }
575
616
 
576
- const isCursor = idx === state.commandPaletteCursor
577
- const pointer = isCursor ? themeColors.accentBold(' ❯ ') : themeColors.dim(' ')
578
- const shortcutText = entry.shortcut ? themeColors.dim(entry.shortcut) : ''
579
- const shortcutWidth = entry.shortcut ? Math.min(16, displayWidth(entry.shortcut)) : 0
580
- const labelMax = Math.max(12, panelInnerWidth - 8 - shortcutWidth)
581
- const plainLabel = truncatePlain(entry.label, labelMax)
582
- const label = highlightMatch(plainLabel, entry.matchPositions)
583
- const row = `${pointer}${padEndDisplay(label, labelMax)}${entry.shortcut ? ` ${shortcutText}` : ''}`
584
- cursorLineByRow[idx] = groupedLines.length
585
- groupedLines.push(isCursor ? themeColors.bgCursor(row) : row)
617
+ cursorLineByRow[idx] = panelLines.length
618
+
619
+ if (isCursor) {
620
+ panelLines.push(themeColors.bgCursor(rowLine))
621
+ } else {
622
+ panelLines.push(rowLine)
623
+ }
586
624
  }
587
625
  }
588
626
 
@@ -590,44 +628,43 @@ export function createOverlayRenderers(state, deps) {
590
628
  state.commandPaletteScrollOffset = keepOverlayTargetVisible(
591
629
  state.commandPaletteScrollOffset,
592
630
  targetLine,
593
- groupedLines.length,
631
+ panelLines.length,
594
632
  bodyRows
595
633
  )
596
- const { visible, offset } = sliceOverlayLines(groupedLines, state.commandPaletteScrollOffset, bodyRows)
634
+ const { visible, offset } = sliceOverlayLines(panelLines, state.commandPaletteScrollOffset, bodyRows)
597
635
  state.commandPaletteScrollOffset = offset
598
636
 
599
637
  const query = state.commandPaletteQuery || ''
600
638
  const queryWithCursor = query.length > 0
601
- ? themeColors.textBold(`${query}▏`)
602
- : themeColors.dim('type a command…') + themeColors.accentBold('')
639
+ ? `${query}${themeColors.accentBold('▏')}`
640
+ : themeColors.accentBold('') + themeColors.dim(' Search commands…')
603
641
 
604
- const panelLines = []
605
- const title = themeColors.textBold('Command Palette')
642
+ const headerLines = []
643
+ const title = themeColors.headerBold('⚡️ Command Palette')
606
644
  const titleLeft = ` ${title}`
607
- const titleRight = themeColors.dim('Esc close')
608
- const titleWidth = Math.max(1, panelInnerWidth - 1 - displayWidth('Esc close'))
609
- panelLines.push(`${padEndDisplay(titleLeft, titleWidth)} ${titleRight}`)
610
- panelLines.push(` ${padEndDisplay(`> ${queryWithCursor}`, panelInnerWidth)}`)
611
- panelLines.push(themeColors.dim(` ${'-'.repeat(Math.max(1, panelInnerWidth))}`))
612
-
613
- for (const line of visible) {
614
- panelLines.push(` ${padEndDisplay(line, panelInnerWidth)}`)
615
- }
645
+ const titleRight = themeColors.dim('Esc')
646
+ const titleWidth = Math.max(1, panelInnerWidth - 1 - displayWidth('Esc'))
647
+ headerLines.push(`${padEndDisplay(titleLeft, titleWidth)} ${titleRight}`)
648
+ headerLines.push(` ${padEndDisplay(`> ${queryWithCursor}`, panelInnerWidth)}`)
649
+ headerLines.push(themeColors.dim(` ${''.repeat(Math.max(1, panelInnerWidth))}`))
650
+
651
+ const footerLines = [
652
+ themeColors.dim(` ${'─'.repeat(Math.max(1, panelInnerWidth))}`),
653
+ ` ${padEndDisplay(themeColors.dim('↵ Select • ← → Expand'), panelInnerWidth)}`,
654
+ ` ${padEndDisplay(themeColors.dim('↑↓ Navigate • Type search'), panelInnerWidth)}`,
655
+ ]
616
656
 
617
- // 📖 Keep panel body stable by filling with blank rows when result list is short.
618
- while (panelLines.length < bodyRows + headerRowCount) {
619
- panelLines.push(` ${' '.repeat(panelInnerWidth)}`)
657
+ const allPanelLines = [...headerLines, ...visible, ...footerLines]
658
+
659
+ while (allPanelLines.length < bodyRows + headerRowCount + 3) {
660
+ allPanelLines.splice(headerLines.length + visible.length, 0, ` ${' '.repeat(panelInnerWidth)}`)
620
661
  }
621
662
 
622
- panelLines.push(themeColors.dim(` ${'-'.repeat(Math.max(1, panelInnerWidth))}`))
623
- panelLines.push(` ${padEndDisplay(themeColors.dim('↑↓ navigate • Enter run • Type to search'), panelInnerWidth)}`)
624
- panelLines.push(` ${padEndDisplay(themeColors.dim('PgUp/PgDn • Home/End'), panelInnerWidth)}`)
625
-
626
663
  const blankPaddedLine = ' '.repeat(panelOuterWidth)
627
664
  const paddedPanelLines = [
628
665
  blankPaddedLine,
629
666
  blankPaddedLine,
630
- ...panelLines.map((line) => `${' '.repeat(panelPad)}${padEndDisplay(line, panelWidth)}${' '.repeat(panelPad)}`),
667
+ ...allPanelLines.map((line) => `${' '.repeat(panelPad)}${padEndDisplay(line, panelWidth)}${' '.repeat(panelPad)}`),
631
668
  blankPaddedLine,
632
669
  blankPaddedLine,
633
670
  ]
@@ -641,8 +678,6 @@ export function createOverlayRenderers(state, deps) {
641
678
  return themeColors.overlayBgCommandPalette(padded)
642
679
  })
643
680
 
644
- // 📖 Absolute cursor positioning overlays the palette on top of the existing table.
645
- // 📖 The next frame starts with ALT_HOME, so this remains stable without manual cleanup.
646
681
  return tintedLines
647
682
  .map((line, idx) => `\x1b[${top + idx};${left}H${line}`)
648
683
  .join('')
@@ -716,11 +751,10 @@ export function createOverlayRenderers(state, deps) {
716
751
  lines.push('')
717
752
  lines.push(` ${heading('Controls')}`)
718
753
  lines.push(` ${key('W')} Toggle ping mode ${hint('(speed 2s → normal 10s → slow 30s → forced 4s)')}`)
719
- lines.push(` ${key('Ctrl+P')} Open command palette ${hint('(search and run actions quickly)')}`)
754
+ lines.push(` ${key('Ctrl+P')} Open ⚡️ command palette ${hint('(search and run actions quickly)')}`)
720
755
  lines.push(` ${key('E')} Toggle configured models only ${hint('(enabled by default)')}`)
721
756
  lines.push(` ${key('Z')} Cycle tool mode ${hint('(OpenCode → Desktop → OpenClaw → Crush → Goose → Pi → Aider → Qwen → OpenHands → Amp)')}`)
722
757
  lines.push(` ${key('F')} Toggle favorite on selected row ${hint('(⭐ pinned at top, persisted)')}`)
723
- lines.push(` ${key('Y')} Install endpoints ${hint('(provider catalog → compatible tools, direct provider only)')}`)
724
758
  lines.push(` ${key('Q')} Smart Recommend ${hint('(🎯 find the best model for your task — questionnaire + live analysis)')}`)
725
759
  lines.push(` ${key('G')} Cycle theme ${hint('(auto → dark → light)')}`)
726
760
  lines.push(` ${themeColors.errorBold('I')} Feedback, bugs & requests ${hint('(📝 send anonymous feedback, bug reports, or feature requests)')}`)
@@ -1,14 +1,9 @@
1
1
  /**
2
2
  * @file src/product-flags.js
3
- * @description Product-level copy for temporarily unavailable surfaces.
3
+ * @description Product-level flags and feature gates.
4
4
  *
5
5
  * @details
6
- * 📖 The proxy bridge is being rebuilt from scratch. The main TUI still
7
- * 📖 shows a clear status line so users know the missing integration is
8
- * 📖 intentional instead of silently broken.
9
- *
10
- * @exports PROXY_DISABLED_NOTICE
6
+ * 📖 Previously held PROXY_DISABLED_NOTICE for the proxy bridge rebuild.
7
+ * 📖 That notice was removed in 0.3.22 after the proxy surface was fully
8
+ * 📖 retired. File kept as a home for future product-level flags.
11
9
  */
12
-
13
- // 📖 Public note rendered in the main TUI footer and reused in CLI/runtime guards.
14
- export const PROXY_DISABLED_NOTICE = 'ℹ️ Proxy is temporarily disabled while we rebuild it into a much more stable bridge for external tools.'
@@ -50,7 +50,6 @@ import { getAvg, getVerdict, getUptime, getStabilityScore, getVersionStatusInfo
50
50
  import { usagePlaceholderForProvider } from './ping.js'
51
51
  import { calculateViewport, sortResultsWithPinnedFavorites, padEndDisplay, displayWidth } from './render-helpers.js'
52
52
  import { getToolMeta } from './tool-metadata.js'
53
- import { PROXY_DISABLED_NOTICE } from './product-flags.js'
54
53
  import { getColumnSpacing } from './ui-config.js'
55
54
 
56
55
  const require = createRequire(import.meta.url)
@@ -149,11 +148,11 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
149
148
  const SEP_W = 3 // ' │ ' display width
150
149
  const ROW_MARGIN = 2 // left margin ' '
151
150
  const W_RANK = 6
152
- const W_TIER = 6
153
- const W_CTX = 6
151
+ const W_TIER = 5
152
+ const W_CTX = 4
154
153
  const W_SOURCE = 14
155
154
  const W_MODEL = 26
156
- const W_SWE = 6
155
+ const W_SWE = 5
157
156
  const W_STATUS = 18
158
157
  const W_VERDICT = 14
159
158
  const W_UPTIME = 6
@@ -164,8 +163,9 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
164
163
  // 📖 Responsive column visibility: progressively hide least-useful columns
165
164
  // 📖 and shorten header labels when terminal width is insufficient.
166
165
  // 📖 Hiding order (least useful first): Rank → Up% → Tier → Stability
167
- // 📖 Compact mode shrinks: Latest Ping→Lat. P (10), Avg Ping→Avg. P (8),
168
- // 📖 Stability→StaB. (8), Provider→4chars+… (10), Health→6chars+… (13)
166
+ // 📖 Compact mode shrinks: Latest Ping→Lat. P (9), Avg Ping→Avg. P (8),
167
+ // 📖 Stability→StaB. (8), Provider→4chars+… (7), Health→6chars+… (13)
168
+ // 📖 Breakpoints: full=169 | compact=146 | -Rank=137 | -Up%=128 | -Tier=120 | -Stab=109
169
169
  let wPing = 14
170
170
  let wAvg = 11
171
171
  let wStab = 11
@@ -192,10 +192,10 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
192
192
  // 📖 Step 1: Compact mode — shorten labels and reduce column widths
193
193
  if (calcWidth() > terminalCols) {
194
194
  isCompact = true
195
- wPing = 10 // 'Lat. P' instead of 'Latest Ping'
195
+ wPing = 9 // 'Lat. P' instead of 'Latest Ping'
196
196
  wAvg = 8 // 'Avg. P' instead of 'Avg Ping'
197
197
  wStab = 8 // 'StaB.' instead of 'Stability'
198
- wSource = 10 // Provider truncated to 4 chars + '…'
198
+ wSource = 7 // Provider truncated to 4 chars + '…', 7 cols total
199
199
  wStatus = 13 // Health truncated after 6 chars + '…'
200
200
  }
201
201
  // 📖 Steps 2–5: Progressive column hiding (least useful first)
@@ -620,7 +620,7 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
620
620
  const activeHotkey = (keyLabel, text, bg) => themeColors.badge(`${keyLabel}${text}`, bg, getReadableTextRgb(bg))
621
621
  // 📖 Line 1: core navigation + filtering shortcuts
622
622
  lines.push(
623
- hotkey('F', ' Toggle Favorite') +
623
+ ' ' + hotkey('F', ' Toggle Favorite') +
624
624
  themeColors.dim(` • `) +
625
625
  (tierFilterMode > 0
626
626
  ? activeHotkey('T', ` Tier (${activeTierLabel})`, getTierRgb(activeTierLabel))
@@ -636,11 +636,11 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
636
636
  themeColors.dim(` • `) +
637
637
  hotkey('K', ' Help')
638
638
  )
639
- // 📖 Line 2: install flow, recommend, feedback, and extended hints.
639
+ // 📖 Line 2: command palette (highlighted as new), recommend, feedback, and extended hints.
640
+ // 📖 CTRL+P ⚡️ Command Palette uses neon-green-on-dark-green background to highlight the feature.
641
+ const paletteLabel = chalk.bgRgb(0, 60, 0).rgb(57, 255, 20).bold(' NEW ! CTRL+P ⚡️ Command Palette ')
640
642
  lines.push(
641
- themeColors.dim(` `) +
642
- hotkey('Ctrl+P', ' Command palette') + themeColors.dim(` • `) +
643
- hotkey('Y', ' Install endpoints') + themeColors.dim(` • `) +
643
+ ' ' + paletteLabel + themeColors.dim(` `) +
644
644
  hotkey('Q', ' Smart Recommend') + themeColors.dim(` • `) +
645
645
  hotkey('G', ' Theme') + themeColors.dim(` • `) +
646
646
  hotkey('I', ' Feedback, bugs & requests')
@@ -661,11 +661,7 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
661
661
  '💬 ' +
662
662
  themeColors.footerDiscord('\x1b]8;;https://discord.gg/ZTNFHvvCkU\x1b\\Discord\x1b]8;;\x1b\\') +
663
663
  themeColors.dim(' → ') +
664
- themeColors.footerDiscord('https://discord.gg/ZTNFHvvCkU') +
665
- themeColors.dim(' • ') +
666
- themeColors.hotkey('N') + themeColors.dim(' Changelog') +
667
- themeColors.dim(' • ') +
668
- themeColors.dim('Ctrl+C Exit')
664
+ themeColors.footerDiscord('https://discord.gg/ZTNFHvvCkU')
669
665
  lines.push(footerLine)
670
666
 
671
667
  if (versionStatus.isOutdated) {
@@ -677,10 +673,12 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
677
673
  lines.push(chalk.bgRed.white.bold(paddedBanner))
678
674
  }
679
675
 
680
- // 📖 Stable release notice: keep the bridge rebuild status explicit in the main UI
681
- // 📖 so users do not go hunting for hidden controls that are disabled on purpose.
682
- const bridgeNotice = chalk.italic.rgb(...getTierRgb('A-'))(` ${PROXY_DISABLED_NOTICE}`)
683
- lines.push(bridgeNotice)
676
+ // 📖 Final footer line: changelog shortcut + exit hint (replaces the old proxy notice).
677
+ lines.push(
678
+ ' ' + themeColors.hotkey('N') + themeColors.dim(' Changelog') +
679
+ themeColors.dim(' • ') +
680
+ themeColors.dim('Ctrl+C Exit')
681
+ )
684
682
 
685
683
  // 📖 Append \x1b[K (erase to EOL) to each line so leftover chars from previous
686
684
  // 📖 frames are cleared. Then pad with blank cleared lines to fill the terminal,
package/src/theme.js CHANGED
@@ -278,11 +278,13 @@ function paintBg(bgRgb, text, fgRgb = null, options = {}) {
278
278
  export const themeColors = {
279
279
  text: (text) => paintRgb(currentPalette().text, text),
280
280
  textBold: (text) => paintRgb(currentPalette().textStrong, text, { bold: true }),
281
+ headerBold: (text) => paintRgb([142, 200, 255], text, { bold: true }),
281
282
  dim: (text) => paintRgb(currentPalette().muted, text),
282
283
  soft: (text) => paintRgb(currentPalette().soft, text),
283
284
  accent: (text) => paintRgb(currentPalette().accent, text),
284
285
  accentBold: (text) => paintRgb(currentPalette().accentStrong, text, { bold: true }),
285
286
  info: (text) => paintRgb(currentPalette().info, text),
287
+ infoBold: (text) => paintRgb([100, 180, 255], text, { bold: true }),
286
288
  success: (text) => paintRgb(currentPalette().success, text),
287
289
  successBold: (text) => paintRgb(currentPalette().successStrong, text, { bold: true }),
288
290
  warning: (text) => paintRgb(currentPalette().warning, text),