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 +30 -1
- package/README.md +16 -7
- package/package.json +1 -1
- package/src/app.js +19 -2
- package/src/command-palette.js +232 -59
- package/src/key-handler.js +97 -13
- package/src/overlays.js +84 -50
- package/src/product-flags.js +4 -9
- package/src/render-table.js +20 -22
- package/src/theme.js +2 -0
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
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.
|
|
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
|
-
|
|
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
|
}
|
package/src/command-palette.js
CHANGED
|
@@ -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
|
-
* → `
|
|
7
|
+
* → `buildCommandPaletteTree` — builds the hierarchical command tree with categories and subcategories
|
|
8
|
+
* → `flattenCommandTree` — converts tree to flat list for filtering (respects expansion state)
|
|
7
9
|
* → `fuzzyMatchCommand` — scores a query against one string and returns match positions
|
|
8
10
|
* → `filterCommandPaletteEntries` — returns sorted command matches for a query
|
|
9
11
|
*
|
|
10
|
-
* @exports {
|
|
12
|
+
* @exports { buildCommandPaletteTree, flattenCommandTree, fuzzyMatchCommand, filterCommandPaletteEntries }
|
|
11
13
|
*
|
|
12
14
|
* @see src/key-handler.js
|
|
13
15
|
* @see src/overlays.js
|
|
14
16
|
*/
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
18
|
+
// 📖 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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
*
|
|
285
|
+
* Now handles hierarchical structure with expandable categories.
|
|
286
|
+
* @param {Array} flatEntries - Flattened command tree entries
|
|
130
287
|
* @param {string} query
|
|
131
|
-
* @returns {Array
|
|
288
|
+
* @returns {Array} Sorted and filtered entries with match scores
|
|
132
289
|
*/
|
|
133
|
-
export function filterCommandPaletteEntries(
|
|
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
|
|
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
|
-
|
|
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
|
|
package/src/key-handler.js
CHANGED
|
@@ -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 {
|
|
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
|
|
657
|
-
|
|
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.
|
|
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
|
-
|
|
804
|
-
|
|
805
|
-
|
|
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
|
|
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
|
|
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
|
-
// 📖
|
|
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(
|
|
537
|
-
const panelInnerWidth = Math.max(
|
|
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
|
|
541
|
-
const
|
|
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
|
|
560
|
+
const panelLines = []
|
|
563
561
|
const cursorLineByRow = {}
|
|
564
|
-
let category = null
|
|
565
562
|
|
|
566
563
|
if (allResults.length === 0) {
|
|
567
|
-
|
|
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
|
-
|
|
572
|
-
|
|
573
|
-
|
|
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
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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
|
-
|
|
631
|
+
panelLines.length,
|
|
594
632
|
bodyRows
|
|
595
633
|
)
|
|
596
|
-
const { visible, offset } = sliceOverlayLines(
|
|
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
|
-
?
|
|
602
|
-
: themeColors.
|
|
639
|
+
? `${query}${themeColors.accentBold('▏')}`
|
|
640
|
+
: themeColors.accentBold('▏') + themeColors.dim(' Search commands…')
|
|
603
641
|
|
|
604
|
-
const
|
|
605
|
-
const title = themeColors.
|
|
642
|
+
const headerLines = []
|
|
643
|
+
const title = themeColors.headerBold('⚡️ Command Palette')
|
|
606
644
|
const titleLeft = ` ${title}`
|
|
607
|
-
const titleRight = themeColors.dim('Esc
|
|
608
|
-
const titleWidth = Math.max(1, panelInnerWidth - 1 - displayWidth('Esc
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
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
|
-
|
|
618
|
-
|
|
619
|
-
|
|
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
|
-
...
|
|
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)')}`)
|
package/src/product-flags.js
CHANGED
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file src/product-flags.js
|
|
3
|
-
* @description Product-level
|
|
3
|
+
* @description Product-level flags and feature gates.
|
|
4
4
|
*
|
|
5
5
|
* @details
|
|
6
|
-
* 📖
|
|
7
|
-
* 📖
|
|
8
|
-
* 📖
|
|
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.'
|
package/src/render-table.js
CHANGED
|
@@ -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 =
|
|
153
|
-
const W_CTX =
|
|
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 =
|
|
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 (
|
|
168
|
-
// 📖 Stability→StaB. (8), Provider→4chars+… (
|
|
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 =
|
|
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 =
|
|
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:
|
|
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
|
-
// 📖
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
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),
|