free-coding-models 0.2.11 → 0.2.13
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 +35 -1
- package/README.md +29 -3
- package/bin/free-coding-models.js +36 -45
- package/package.json +1 -1
- package/src/changelog-loader.js +118 -0
- package/src/key-handler.js +149 -5
- package/src/overlays.js +105 -2
- package/src/render-table.js +30 -20
- package/src/tool-launchers.js +29 -13
package/CHANGELOG.md
CHANGED
|
@@ -2,10 +2,44 @@
|
|
|
2
2
|
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
+
## 0.2.13
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **Persist UI view settings**: Tier filter (T key), provider filter (D key), and sort order now persist across session restarts — settings are saved to `~/.free-coding-models.json` under `config.settings` and automatically restored on next startup. Settings also mirror into active profiles so profile switching captures live view preferences.
|
|
9
|
+
- When T cycles tier: S+ tier is now remembered for next session
|
|
10
|
+
- When D cycles provider: Filtered provider is now remembered
|
|
11
|
+
- When sort keys (R/O/M/L/A/S/C/H/V/B/U) change order: Sort column and direction are now remembered
|
|
12
|
+
- Profile loading has priority over global `config.settings` so saved profiles override global defaults
|
|
13
|
+
- **Reset view settings (Shift+R)**: New keyboard shortcut to instantly reset tier filter, provider filter, and sort order to defaults (All tier, no provider filter, avg sort ascending). Also clears persisted settings from `config.settings` so next restart returns to factory defaults.
|
|
14
|
+
- Useful when you've customized your view but want a fresh start
|
|
15
|
+
- Does not affect favorites, API keys, or other settings — only view state
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- **Help overlay (K key)**: Updated to document new Shift+R keybinding for resetting view settings
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 0.2.12
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
- **Auto-select models for all external tools**: All 10 supported tools (Aider, Crush, Goose, Claude Code, Codex, Gemini, Qwen, OpenHands, Amp, Pi) now automatically configure and pre-select the chosen model on launch — no manual model selection needed after pressing Enter.
|
|
26
|
+
- **Changelog loader utility**: New `src/changelog-loader.js` module parses CHANGELOG.md for future TUI integration to display changes directly in the app instead of opening a browser.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
- **Infinite update loop on startup**: Disabled forced auto-update that caused the app to detect the same update repeatedly after restarting. The app now checks for updates in the background without forcing installation.
|
|
30
|
+
- **Removed disruptive browser window**: The auto-update process no longer opens a browser window to show the changelog — it now shows update information in the terminal only.
|
|
31
|
+
- **Update failure tracking**: If update checks fail 3+ times, the app displays a prominent red footer warning: `⚠ OUTDATED version, please update` with manual update instructions instead of crashing.
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
- **OpenHands integration improved**: Now sets `LLM_MODEL` and `LLM_API_KEY` environment variables for proper model pre-selection on launch.
|
|
35
|
+
- **Amp integration improved**: Now writes `amp.model` to config file with the selected model ID.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
5
39
|
## 0.2.11
|
|
6
40
|
|
|
7
41
|
### Added
|
|
8
|
-
- **Pi Coding Agent support**: Enabled Pi (pi.dev) as a launchable mode in the Z key cycle. Select a model and press Enter to configure Pi's config
|
|
42
|
+
- **Pi Coding Agent support**: Enabled Pi (pi.dev) as a launchable mode in the Z key cycle. Select a model and press Enter to auto-configure Pi's model config and settings, then spawn the PI coding agent CLI with the chosen model pre-selected as the default.
|
|
9
43
|
|
|
10
44
|
---
|
|
11
45
|
|
package/README.md
CHANGED
|
@@ -80,13 +80,14 @@ By Vanessa Depraute
|
|
|
80
80
|
- **📐 Stability score** — Composite 0–100 score measuring consistency (p95, jitter, spikes, uptime)
|
|
81
81
|
- **📊 Usage tracking** — Monitor remaining quota for each exact provider/model pair when the provider exposes it; otherwise the TUI shows a green dot instead of a misleading percentage.
|
|
82
82
|
- **📜 Request Log Overlay** — Press `X` to inspect recent proxied requests and token usage for exact provider/model pairs.
|
|
83
|
+
- **📋 Changelog Overlay** — Press `N` to browse all versions in an index, then `Enter` to view details for any version with full scroll support
|
|
83
84
|
- **🛠 MODEL_NOT_FOUND Rotation** — If a specific provider returns a 404 for a model, the TUI intelligently rotates through other available providers for the same model.
|
|
84
85
|
- **🔄 Auto-retry** — Timeout models keep getting retried, nothing is ever "given up on"
|
|
85
86
|
- **🎮 Interactive selection** — Navigate with arrow keys directly in the table, press Enter to act
|
|
86
87
|
- **💻 OpenCode integration** — Auto-detects NIM setup, sets model as default, launches OpenCode
|
|
87
88
|
- **🦞 OpenClaw integration** — Sets selected model as default provider in `~/.openclaw/openclaw.json`
|
|
88
|
-
- **🧰 Public tool launchers** — `Enter`
|
|
89
|
-
- **🔌 Install Endpoints flow** — Press `Y` to install one configured provider directly into `OpenCode CLI`, `OpenCode Desktop`, `OpenClaw`, `Crush`, or `
|
|
89
|
+
- **🧰 Public tool launchers** — `Enter` auto-configures and launches 10+ tools: `OpenCode CLI`, `OpenCode Desktop`, `OpenClaw`, `Crush`, `Goose`, `Aider`, `Claude Code`, `Codex`, `Gemini`, `Qwen`, `OpenHands`, `Amp`, and `Pi`. All tools auto-select the chosen model on launch.
|
|
90
|
+
- **🔌 Install Endpoints flow** — Press `Y` to install one configured provider directly into `OpenCode CLI`, `OpenCode Desktop`, `OpenClaw`, `Crush`, `Goose`, `Aider`, or `Gemini`, either with the full provider catalog or a curated subset of models
|
|
90
91
|
- **📝 Feature Request (J key)** — Send anonymous feedback directly to the project team
|
|
91
92
|
- **🐛 Bug Report (I key)** — Send anonymous bug reports directly to the project team
|
|
92
93
|
- **🎨 Clean output** — Zero scrollback pollution, interface stays open until Ctrl+C
|
|
@@ -594,6 +595,30 @@ Use **↑↓** to scroll and **Esc** or **X** to return to the main table.
|
|
|
594
595
|
|
|
595
596
|
---
|
|
596
597
|
|
|
598
|
+
## 🧰 Supported Tool Launchers
|
|
599
|
+
|
|
600
|
+
You can use `free-coding-models` with 12+ AI coding tools. When you select a model and press Enter, the tool automatically configures and pre-selects your chosen model:
|
|
601
|
+
|
|
602
|
+
| Tool | Flag | Auto-Config |
|
|
603
|
+
|------|------|------------|
|
|
604
|
+
| OpenCode CLI | `--opencode` | ~/.config/opencode/opencode.json |
|
|
605
|
+
| OpenCode Desktop | `--opencode-desktop` | Opens Desktop app |
|
|
606
|
+
| OpenClaw | `--openclaw` | ~/.openclaw/openclaw.json |
|
|
607
|
+
| Crush | `--crush` | ~/.config/crush/crush.json |
|
|
608
|
+
| Goose | `--goose` | Environment variables |
|
|
609
|
+
| **Aider** | `--aider` | ~/.aider.conf.yml |
|
|
610
|
+
| **Claude Code** | `--claude-code` | CLI flag |
|
|
611
|
+
| **Codex** | `--codex` | CLI flag |
|
|
612
|
+
| **Gemini** | `--gemini` | ~/.gemini/settings.json |
|
|
613
|
+
| **Qwen** | `--qwen` | ~/.qwen/settings.json |
|
|
614
|
+
| **OpenHands** | `--openhands` | LLM_MODEL env var |
|
|
615
|
+
| **Amp** | `--amp` | ~/.config/amp/settings.json |
|
|
616
|
+
| **Pi** | `--pi` | ~/.pi/agent/settings.json |
|
|
617
|
+
|
|
618
|
+
Press **Z** to cycle through different tool modes in the TUI, or use flags to start in your preferred mode.
|
|
619
|
+
|
|
620
|
+
---
|
|
621
|
+
|
|
597
622
|
## 🔌 OpenCode Integration
|
|
598
623
|
|
|
599
624
|
**The easiest way** — let `free-coding-models` do everything:
|
|
@@ -604,7 +629,7 @@ Use **↑↓** to scroll and **Esc** or **X** to return to the main table.
|
|
|
604
629
|
4. **Press Enter** — tool automatically:
|
|
605
630
|
- Detects if NVIDIA NIM is configured in OpenCode
|
|
606
631
|
- Sets your selected model as default in `~/.config/opencode/opencode.json`
|
|
607
|
-
- Launches OpenCode with the model ready to use
|
|
632
|
+
- Launches OpenCode with the model pre-selected and ready to use
|
|
608
633
|
|
|
609
634
|
### tmux sub-agent panes
|
|
610
635
|
|
|
@@ -907,6 +932,7 @@ This script:
|
|
|
907
932
|
- **Shift+P** — Cycle through saved profiles (switches live TUI settings)
|
|
908
933
|
- **Shift+S** — Save current TUI settings as a named profile (inline prompt)
|
|
909
934
|
- **Q** — Open Smart Recommend overlay (find the best model for your task)
|
|
935
|
+
- **N** — Open Changelog overlay (browse index of all versions, `Enter` to view details, `B` to go back)
|
|
910
936
|
- **W** — Cycle ping mode (`FAST` 2s → `NORMAL` 10s → `SLOW` 30s → `FORCED` 4s)
|
|
911
937
|
- **J / I** — Request feature / Report bug
|
|
912
938
|
- **K / Esc** — Show help overlay / Close overlay
|
|
@@ -261,47 +261,26 @@ async function main() {
|
|
|
261
261
|
ts: new Date().toISOString(),
|
|
262
262
|
})
|
|
263
263
|
|
|
264
|
-
// 📖 Check for updates in the background
|
|
264
|
+
// 📖 Check for updates in the background (non-blocking, non-forced)
|
|
265
|
+
// 📖 The old auto-update on startup caused infinite loops, so we've moved to:
|
|
266
|
+
// 📖 1. Optional prompt when a new version is available (user chooses to update or not)
|
|
267
|
+
// 📖 2. Display "OUTDATED" in TUI footer if update check fails repeatedly
|
|
265
268
|
let latestVersion = null
|
|
269
|
+
let isOutdated = false
|
|
266
270
|
try {
|
|
267
271
|
latestVersion = await checkForUpdate()
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
// 📖 Auto-update system: force updates and handle changelog automatically
|
|
273
|
-
// 📖 Skip when running from source (dev mode) — .git means we're in a repo checkout,
|
|
274
|
-
// 📖 not a global npm install. Auto-update would overwrite the global copy but restart
|
|
275
|
-
// 📖 the local one, causing an infinite update loop since LOCAL_VERSION never changes.
|
|
276
|
-
const isDevMode = existsSync(join(dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, '$1')), '..', '.git'))
|
|
277
|
-
if (latestVersion && !isDevMode) {
|
|
278
|
-
console.log()
|
|
279
|
-
console.log(chalk.bold.red(' ⚠ AUTO-UPDATE AVAILABLE'))
|
|
280
|
-
console.log(chalk.red(` Version ${latestVersion} will be installed automatically`))
|
|
281
|
-
console.log(chalk.dim(' Opening changelog in browser...'))
|
|
282
|
-
console.log()
|
|
283
|
-
|
|
284
|
-
// 📖 Open changelog automatically
|
|
285
|
-
const { execSync } = require('child_process')
|
|
286
|
-
const changelogUrl = 'https://github.com/vava-nessa/free-coding-models/releases'
|
|
287
|
-
try {
|
|
288
|
-
if (isMac) {
|
|
289
|
-
execSync(`open "${changelogUrl}"`, { stdio: 'ignore' })
|
|
290
|
-
} else if (isWindows) {
|
|
291
|
-
execSync(`start "" "${changelogUrl}"`, { stdio: 'ignore' })
|
|
292
|
-
} else {
|
|
293
|
-
execSync(`xdg-open "${changelogUrl}"`, { stdio: 'ignore' })
|
|
294
|
-
}
|
|
295
|
-
console.log(chalk.green(' ✅ Changelog opened in browser'))
|
|
296
|
-
} catch {
|
|
297
|
-
console.log(chalk.yellow(' ⚠ Could not open browser automatically'))
|
|
298
|
-
console.log(chalk.dim(` Visit manually: ${changelogUrl}`))
|
|
272
|
+
// 📖 Track update check failures - if it fails 3+ times, mark as outdated
|
|
273
|
+
if (!latestVersion && config.settings?.updateCheckFailures >= 3) {
|
|
274
|
+
isOutdated = true
|
|
299
275
|
}
|
|
300
|
-
|
|
301
|
-
// 📖
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
276
|
+
} catch (err) {
|
|
277
|
+
// 📖 Silently fail - don't block the app if npm registry is unreachable
|
|
278
|
+
// 📖 But track the failure for outdated detection
|
|
279
|
+
const failures = (config.settings?.updateCheckFailures || 0) + 1
|
|
280
|
+
if (!config.settings) config.settings = {}
|
|
281
|
+
config.settings.updateCheckFailures = Math.min(failures, 3)
|
|
282
|
+
if (failures >= 3) isOutdated = true
|
|
283
|
+
saveConfig(config)
|
|
305
284
|
}
|
|
306
285
|
|
|
307
286
|
// 📖 Dynamic OpenRouter free model discovery — fetch live free models from API
|
|
@@ -383,8 +362,8 @@ async function main() {
|
|
|
383
362
|
frame: 0,
|
|
384
363
|
cursor: 0,
|
|
385
364
|
selectedModel: null,
|
|
386
|
-
sortColumn: startupProfileSettings?.sortColumn
|
|
387
|
-
sortDirection: startupProfileSettings?.sortAsc
|
|
365
|
+
sortColumn: startupProfileSettings?.sortColumn ?? config.settings?.sortColumn ?? 'avg',
|
|
366
|
+
sortDirection: (startupProfileSettings?.sortAsc ?? config.settings?.sortAsc ?? true) ? 'asc' : 'desc',
|
|
388
367
|
pingInterval: PING_MODE_INTERVALS.speed, // 📖 Effective live interval derived from the active ping mode.
|
|
389
368
|
pingMode: 'speed', // 📖 Current ping mode: speed | normal | slow | forced.
|
|
390
369
|
pingModeSource: 'startup', // 📖 Why this mode is active: startup | manual | auto | idle | activity.
|
|
@@ -392,6 +371,8 @@ async function main() {
|
|
|
392
371
|
lastPingTime: now, // 📖 Track when last ping cycle started
|
|
393
372
|
lastUserActivityAt: now, // 📖 Any keypress refreshes this timer; inactivity can force slow mode.
|
|
394
373
|
resumeSpeedOnActivity: false, // 📖 Set after idle slowdown so the next activity restarts a 60s speed burst.
|
|
374
|
+
latestVersion, // 📖 Latest npm version available (null if none or check failed)
|
|
375
|
+
isOutdated, // 📖 Set to true if update check failed 3+ times (show red "OUTDATED" footer)
|
|
395
376
|
mode, // 📖 'opencode' or 'openclaw' — controls Enter action
|
|
396
377
|
tierFilterMode: 0, // 📖 Index into TIER_CYCLE (0=All, 1=S+, 2=S, ...)
|
|
397
378
|
originFilterMode: 0, // 📖 Index into ORIGIN_CYCLE (0=All, then providers)
|
|
@@ -463,6 +444,12 @@ async function main() {
|
|
|
463
444
|
logVisible: false, // 📖 Whether the log page overlay is active
|
|
464
445
|
logScrollOffset: 0, // 📖 Vertical scroll offset for log overlay viewport
|
|
465
446
|
logShowAll: false, // 📖 Show all logs (true) or limited to 500 (false)
|
|
447
|
+
// 📖 Changelog overlay state (N key opens it)
|
|
448
|
+
changelogOpen: false, // 📖 Whether the changelog overlay is active
|
|
449
|
+
changelogScrollOffset: 0, // 📖 Vertical scroll offset for changelog overlay viewport
|
|
450
|
+
changelogPhase: 'index', // 📖 'index' (all versions) | 'details' (specific version)
|
|
451
|
+
changelogCursor: 0, // 📖 Selected row in index phase
|
|
452
|
+
changelogSelectedVersion: null, // 📖 Which version to show details for
|
|
466
453
|
// 📖 Proxy startup status — set by autoStartProxyIfSynced, consumed by Task 3 indicator
|
|
467
454
|
// 📖 null = not configured/not attempted
|
|
468
455
|
// 📖 { phase: 'starting' } — proxy start in progress
|
|
@@ -625,8 +612,10 @@ async function main() {
|
|
|
625
612
|
|
|
626
613
|
// 📖 originFilterMode: index into ORIGIN_CYCLE, 0=All, then each provider key in order
|
|
627
614
|
const ORIGIN_CYCLE = [null, ...Object.keys(sources)]
|
|
628
|
-
|
|
629
|
-
state.
|
|
615
|
+
const resolvedTierFilter = startupProfileSettings?.tierFilter ?? config.settings?.tierFilter
|
|
616
|
+
state.tierFilterMode = resolvedTierFilter ? Math.max(0, TIER_CYCLE.indexOf(resolvedTierFilter)) : 0
|
|
617
|
+
const resolvedOriginFilter = config.settings?.originFilter
|
|
618
|
+
state.originFilterMode = resolvedOriginFilter ? Math.max(0, ORIGIN_CYCLE.indexOf(resolvedOriginFilter)) : 0
|
|
630
619
|
|
|
631
620
|
function applyTierFilter() {
|
|
632
621
|
const activeTier = TIER_CYCLE[state.tierFilterMode]
|
|
@@ -790,12 +779,12 @@ async function main() {
|
|
|
790
779
|
process.stdin.on('keypress', onKeyPress)
|
|
791
780
|
process.on('SIGCONT', noteUserActivity)
|
|
792
781
|
|
|
793
|
-
// 📖 Animation loop: render settings overlay, recommend overlay, help overlay, feature request overlay, bug report overlay, OR main table
|
|
782
|
+
// 📖 Animation loop: render settings overlay, recommend overlay, help overlay, feature request overlay, bug report overlay, changelog overlay, OR main table
|
|
794
783
|
ticker = setInterval(() => {
|
|
795
784
|
refreshAutoPingMode()
|
|
796
785
|
state.frame++
|
|
797
786
|
// 📖 Cache visible+sorted models each frame so Enter handler always matches the display
|
|
798
|
-
if (!state.settingsOpen && !state.installEndpointsOpen && !state.recommendOpen && !state.featureRequestOpen && !state.bugReportOpen) {
|
|
787
|
+
if (!state.settingsOpen && !state.installEndpointsOpen && !state.recommendOpen && !state.featureRequestOpen && !state.bugReportOpen && !state.changelogOpen) {
|
|
799
788
|
const visible = state.results.filter(r => !r.hidden)
|
|
800
789
|
state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection)
|
|
801
790
|
}
|
|
@@ -813,7 +802,9 @@ async function main() {
|
|
|
813
802
|
? overlays.renderHelp()
|
|
814
803
|
: state.logVisible
|
|
815
804
|
? overlays.renderLog()
|
|
816
|
-
|
|
805
|
+
: state.changelogOpen
|
|
806
|
+
? overlays.renderChangelog()
|
|
807
|
+
: renderTable(state.results, state.pendingPings, state.frame, state.cursor, state.sortColumn, state.sortDirection, state.pingInterval, state.lastPingTime, state.mode, state.tierFilterMode, state.scrollOffset, state.terminalRows, state.terminalCols, state.originFilterMode, state.activeProfile, state.profileSaveMode, state.profileSaveBuffer, state.proxyStartupStatus, state.pingMode, state.pingModeSource, state.hideUnconfiguredModels, state.widthWarningStartedAt, state.widthWarningDismissed, state.settingsUpdateState, state.settingsUpdateLatestVersion, getProxySettings(state.config).enabled === true, state.isOutdated, state.latestVersion)
|
|
817
808
|
process.stdout.write(ALT_HOME + content)
|
|
818
809
|
}, Math.round(1000 / FPS))
|
|
819
810
|
|
|
@@ -821,7 +812,7 @@ async function main() {
|
|
|
821
812
|
const initialVisible = state.results.filter(r => !r.hidden)
|
|
822
813
|
state.visibleSorted = sortResultsWithPinnedFavorites(initialVisible, state.sortColumn, state.sortDirection)
|
|
823
814
|
|
|
824
|
-
process.stdout.write(ALT_HOME + renderTable(state.results, state.pendingPings, state.frame, state.cursor, state.sortColumn, state.sortDirection, state.pingInterval, state.lastPingTime, state.mode, state.tierFilterMode, state.scrollOffset, state.terminalRows, state.terminalCols, state.originFilterMode, state.activeProfile, state.profileSaveMode, state.profileSaveBuffer, state.proxyStartupStatus, state.pingMode, state.pingModeSource, state.hideUnconfiguredModels, state.widthWarningStartedAt, state.widthWarningDismissed, state.settingsUpdateState, state.settingsUpdateLatestVersion, getProxySettings(state.config).enabled === true))
|
|
815
|
+
process.stdout.write(ALT_HOME + renderTable(state.results, state.pendingPings, state.frame, state.cursor, state.sortColumn, state.sortDirection, state.pingInterval, state.lastPingTime, state.mode, state.tierFilterMode, state.scrollOffset, state.terminalRows, state.terminalCols, state.originFilterMode, state.activeProfile, state.profileSaveMode, state.profileSaveBuffer, state.proxyStartupStatus, state.pingMode, state.pingModeSource, state.hideUnconfiguredModels, state.widthWarningStartedAt, state.widthWarningDismissed, state.settingsUpdateState, state.settingsUpdateLatestVersion, getProxySettings(state.config).enabled === true, state.isOutdated, state.latestVersion))
|
|
825
816
|
|
|
826
817
|
// 📖 If --recommend was passed, auto-open the Smart Recommend overlay on start
|
|
827
818
|
if (cliArgs.recommendMode) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "free-coding-models",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.13",
|
|
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",
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file changelog-loader.js
|
|
3
|
+
* @description Load and parse CHANGELOG.md for display in the TUI
|
|
4
|
+
*
|
|
5
|
+
* @functions
|
|
6
|
+
* → loadChangelog() — Read and parse CHANGELOG.md into structured format
|
|
7
|
+
* → getLatestChanges(version) — Return changelog for a specific version
|
|
8
|
+
* → formatChangelogForDisplay(version) — Format for TUI rendering
|
|
9
|
+
*
|
|
10
|
+
* @exports loadChangelog, getLatestChanges, formatChangelogForDisplay
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { readFileSync, existsSync } from 'fs'
|
|
14
|
+
import { dirname, join } from 'path'
|
|
15
|
+
import { fileURLToPath } from 'url'
|
|
16
|
+
|
|
17
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
18
|
+
const __dirname = dirname(__filename)
|
|
19
|
+
const CHANGELOG_PATH = join(__dirname, '..', 'CHANGELOG.md')
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 📖 loadChangelog: Read and parse CHANGELOG.md
|
|
23
|
+
* @returns {Object} { versions: { '0.2.11': { added: [], fixed: [], changed: [] }, ... } }
|
|
24
|
+
*/
|
|
25
|
+
export function loadChangelog() {
|
|
26
|
+
if (!existsSync(CHANGELOG_PATH)) return { versions: {} }
|
|
27
|
+
|
|
28
|
+
const content = readFileSync(CHANGELOG_PATH, 'utf8')
|
|
29
|
+
const versions = {}
|
|
30
|
+
const lines = content.split('\n')
|
|
31
|
+
let currentVersion = null
|
|
32
|
+
let currentSection = null
|
|
33
|
+
let currentItems = []
|
|
34
|
+
|
|
35
|
+
for (const line of lines) {
|
|
36
|
+
// 📖 Match version headers: ## 0.2.11
|
|
37
|
+
const versionMatch = line.match(/^## ([\d.]+)/)
|
|
38
|
+
if (versionMatch) {
|
|
39
|
+
if (currentVersion && currentSection && currentItems.length > 0) {
|
|
40
|
+
if (!versions[currentVersion]) versions[currentVersion] = {}
|
|
41
|
+
versions[currentVersion][currentSection] = currentItems
|
|
42
|
+
}
|
|
43
|
+
currentVersion = versionMatch[1]
|
|
44
|
+
currentSection = null
|
|
45
|
+
currentItems = []
|
|
46
|
+
continue
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 📖 Match section headers: ### Added, ### Fixed, ### Changed
|
|
50
|
+
const sectionMatch = line.match(/^### (Added|Fixed|Changed|Updated)/)
|
|
51
|
+
if (sectionMatch) {
|
|
52
|
+
if (currentVersion && currentSection && currentItems.length > 0) {
|
|
53
|
+
if (!versions[currentVersion]) versions[currentVersion] = {}
|
|
54
|
+
versions[currentVersion][currentSection.toLowerCase()] = currentItems
|
|
55
|
+
}
|
|
56
|
+
currentSection = sectionMatch[1].toLowerCase()
|
|
57
|
+
currentItems = []
|
|
58
|
+
continue
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 📖 Match bullet points: - **text**: description
|
|
62
|
+
if (line.match(/^- /) && currentVersion && currentSection) {
|
|
63
|
+
currentItems.push(line.replace(/^- /, ''))
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 📖 Save the last section
|
|
68
|
+
if (currentVersion && currentSection && currentItems.length > 0) {
|
|
69
|
+
if (!versions[currentVersion]) versions[currentVersion] = {}
|
|
70
|
+
versions[currentVersion][currentSection] = currentItems
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return { versions }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 📖 getLatestChanges: Return changelog for a specific version
|
|
78
|
+
* @param {string} version (e.g. '0.2.11')
|
|
79
|
+
* @returns {Object|null}
|
|
80
|
+
*/
|
|
81
|
+
export function getLatestChanges(version) {
|
|
82
|
+
const { versions } = loadChangelog()
|
|
83
|
+
return versions[version] || null
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 📖 formatChangelogForDisplay: Format changelog section as array of strings for TUI
|
|
88
|
+
* @param {string} version
|
|
89
|
+
* @returns {string[]} formatted lines
|
|
90
|
+
*/
|
|
91
|
+
export function formatChangelogForDisplay(version) {
|
|
92
|
+
const changes = getLatestChanges(version)
|
|
93
|
+
if (!changes) return []
|
|
94
|
+
|
|
95
|
+
const lines = [
|
|
96
|
+
`📋 Changelog for v${version}`,
|
|
97
|
+
'',
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
const sections = { added: 'Added', fixed: 'Fixed', changed: 'Changed', updated: 'Updated' }
|
|
101
|
+
for (const [key, label] of Object.entries(sections)) {
|
|
102
|
+
if (changes[key] && changes[key].length > 0) {
|
|
103
|
+
lines.push(`✨ ${label}:`)
|
|
104
|
+
for (const item of changes[key]) {
|
|
105
|
+
// 📖 Wrap long lines for display
|
|
106
|
+
const maxWidth = 70
|
|
107
|
+
let item_text = item.replace(/\*\*([^*]+)\*\*/g, '$1').replace(/`([^`]+)`/g, '$1')
|
|
108
|
+
if (item_text.length > maxWidth) {
|
|
109
|
+
item_text = item_text.substring(0, maxWidth - 3) + '...'
|
|
110
|
+
}
|
|
111
|
+
lines.push(` • ${item_text}`)
|
|
112
|
+
}
|
|
113
|
+
lines.push('')
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return lines
|
|
118
|
+
}
|
package/src/key-handler.js
CHANGED
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
* @exports { buildProviderModelsUrl, parseProviderModelIds, listProviderTestModels, classifyProviderTestOutcome, buildProviderTestDetail, createKeyHandler }
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
|
+
import { loadChangelog } from './changelog-loader.js'
|
|
29
|
+
|
|
28
30
|
// 📖 Some providers need an explicit probe model because the first catalog entry
|
|
29
31
|
// 📖 is not guaranteed to be accepted by their chat endpoint.
|
|
30
32
|
const PROVIDER_TEST_MODEL_OVERRIDES = {
|
|
@@ -766,6 +768,70 @@ export function createKeyHandler(ctx) {
|
|
|
766
768
|
return
|
|
767
769
|
}
|
|
768
770
|
|
|
771
|
+
// 📖 Changelog overlay: two-phase (index + details) with keyboard navigation
|
|
772
|
+
if (state.changelogOpen) {
|
|
773
|
+
const pageStep = Math.max(1, (state.terminalRows || 1) - 2)
|
|
774
|
+
const changelogData = loadChangelog()
|
|
775
|
+
const { versions } = changelogData
|
|
776
|
+
const versionList = Object.keys(versions).sort((a, b) => {
|
|
777
|
+
const aParts = a.split('.').map(Number)
|
|
778
|
+
const bParts = b.split('.').map(Number)
|
|
779
|
+
for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
|
|
780
|
+
const aVal = aParts[i] || 0
|
|
781
|
+
const bVal = bParts[i] || 0
|
|
782
|
+
if (bVal !== aVal) return bVal - aVal
|
|
783
|
+
}
|
|
784
|
+
return 0
|
|
785
|
+
})
|
|
786
|
+
|
|
787
|
+
// 📖 Close changelog overlay
|
|
788
|
+
if (key.name === 'escape' || key.name === 'n') {
|
|
789
|
+
state.changelogOpen = false
|
|
790
|
+
state.changelogPhase = 'index'
|
|
791
|
+
state.changelogCursor = 0
|
|
792
|
+
state.changelogSelectedVersion = null
|
|
793
|
+
return
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
if (state.changelogPhase === 'index') {
|
|
797
|
+
// 📖 INDEX PHASE: Navigate through versions
|
|
798
|
+
if (key.name === 'up') {
|
|
799
|
+
state.changelogCursor = Math.max(0, state.changelogCursor - 1)
|
|
800
|
+
return
|
|
801
|
+
}
|
|
802
|
+
if (key.name === 'down') {
|
|
803
|
+
state.changelogCursor = Math.min(versionList.length - 1, state.changelogCursor + 1)
|
|
804
|
+
return
|
|
805
|
+
}
|
|
806
|
+
if (key.name === 'home') { state.changelogCursor = 0; return }
|
|
807
|
+
if (key.name === 'end') { state.changelogCursor = versionList.length - 1; return }
|
|
808
|
+
if (key.name === 'return') {
|
|
809
|
+
// 📖 Enter details phase for selected version
|
|
810
|
+
state.changelogPhase = 'details'
|
|
811
|
+
state.changelogSelectedVersion = versionList[state.changelogCursor]
|
|
812
|
+
state.changelogScrollOffset = 0
|
|
813
|
+
return
|
|
814
|
+
}
|
|
815
|
+
} else if (state.changelogPhase === 'details') {
|
|
816
|
+
// 📖 DETAILS PHASE: Scroll through selected version details
|
|
817
|
+
if (key.name === 'b') {
|
|
818
|
+
// 📖 B = back to index
|
|
819
|
+
state.changelogPhase = 'index'
|
|
820
|
+
state.changelogScrollOffset = 0
|
|
821
|
+
return
|
|
822
|
+
}
|
|
823
|
+
if (key.name === 'up') { state.changelogScrollOffset = Math.max(0, state.changelogScrollOffset - 1); return }
|
|
824
|
+
if (key.name === 'down') { state.changelogScrollOffset += 1; return }
|
|
825
|
+
if (key.name === 'pageup') { state.changelogScrollOffset = Math.max(0, state.changelogScrollOffset - pageStep); return }
|
|
826
|
+
if (key.name === 'pagedown') { state.changelogScrollOffset += pageStep; return }
|
|
827
|
+
if (key.name === 'home') { state.changelogScrollOffset = 0; return }
|
|
828
|
+
if (key.name === 'end') { state.changelogScrollOffset = Number.MAX_SAFE_INTEGER; return }
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
if (key.ctrl && key.name === 'c') { exit(0); return }
|
|
832
|
+
return
|
|
833
|
+
}
|
|
834
|
+
|
|
769
835
|
// 📖 Smart Recommend overlay: full keyboard handling while overlay is open.
|
|
770
836
|
if (state.recommendOpen) {
|
|
771
837
|
if (key.ctrl && key.name === 'c') { exit(0); return }
|
|
@@ -882,10 +948,11 @@ export function createKeyHandler(ctx) {
|
|
|
882
948
|
const proxySyncRowIdx = updateRowIdx + 2
|
|
883
949
|
const proxyPortRowIdx = updateRowIdx + 3
|
|
884
950
|
const proxyCleanupRowIdx = updateRowIdx + 4
|
|
885
|
-
|
|
951
|
+
const changelogViewRowIdx = updateRowIdx + 5
|
|
952
|
+
// 📖 Profile rows start after maintenance + proxy rows + changelog row — one row per saved profile
|
|
886
953
|
const savedProfiles = listProfiles(state.config)
|
|
887
|
-
const profileStartIdx = updateRowIdx +
|
|
888
|
-
const maxRowIdx = savedProfiles.length > 0 ? profileStartIdx + savedProfiles.length - 1 :
|
|
954
|
+
const profileStartIdx = updateRowIdx + 6
|
|
955
|
+
const maxRowIdx = savedProfiles.length > 0 ? profileStartIdx + savedProfiles.length - 1 : changelogViewRowIdx
|
|
889
956
|
|
|
890
957
|
// 📖 Edit/Add-key mode: capture typed characters for the API key
|
|
891
958
|
if (state.settingsEditMode || state.settingsAddKeyMode) {
|
|
@@ -1059,6 +1126,17 @@ export function createKeyHandler(ctx) {
|
|
|
1059
1126
|
return
|
|
1060
1127
|
}
|
|
1061
1128
|
|
|
1129
|
+
// 📖 Changelog row: Enter → open changelog overlay
|
|
1130
|
+
if (state.settingsCursor === changelogViewRowIdx) {
|
|
1131
|
+
state.settingsOpen = false
|
|
1132
|
+
state.changelogOpen = true
|
|
1133
|
+
state.changelogPhase = 'index'
|
|
1134
|
+
state.changelogCursor = 0
|
|
1135
|
+
state.changelogSelectedVersion = null
|
|
1136
|
+
state.changelogScrollOffset = 0
|
|
1137
|
+
return
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1062
1140
|
// 📖 Profile row: Enter → load the selected profile (apply its settings live)
|
|
1063
1141
|
if (state.settingsCursor >= profileStartIdx && savedProfiles.length > 0) {
|
|
1064
1142
|
const profileIdx = state.settingsCursor - profileStartIdx
|
|
@@ -1094,7 +1172,7 @@ export function createKeyHandler(ctx) {
|
|
|
1094
1172
|
}
|
|
1095
1173
|
|
|
1096
1174
|
if (key.name === 'space') {
|
|
1097
|
-
if (state.settingsCursor === updateRowIdx || state.settingsCursor === proxyPortRowIdx || state.settingsCursor === proxyCleanupRowIdx) return
|
|
1175
|
+
if (state.settingsCursor === updateRowIdx || state.settingsCursor === proxyPortRowIdx || state.settingsCursor === proxyCleanupRowIdx || state.settingsCursor === changelogViewRowIdx) return
|
|
1098
1176
|
// 📖 Profile rows don't respond to Space
|
|
1099
1177
|
if (state.settingsCursor >= profileStartIdx) return
|
|
1100
1178
|
|
|
@@ -1125,7 +1203,7 @@ export function createKeyHandler(ctx) {
|
|
|
1125
1203
|
}
|
|
1126
1204
|
|
|
1127
1205
|
if (key.name === 't') {
|
|
1128
|
-
if (state.settingsCursor === updateRowIdx) return
|
|
1206
|
+
if (state.settingsCursor === updateRowIdx || state.settingsCursor === proxyPortRowIdx || state.settingsCursor === proxyCleanupRowIdx || state.settingsCursor === changelogViewRowIdx) return
|
|
1129
1207
|
// 📖 Profile rows don't respond to T (test key)
|
|
1130
1208
|
if (state.settingsCursor >= profileStartIdx) return
|
|
1131
1209
|
|
|
@@ -1333,9 +1411,60 @@ export function createKeyHandler(ctx) {
|
|
|
1333
1411
|
return
|
|
1334
1412
|
}
|
|
1335
1413
|
|
|
1414
|
+
// 📖 Helper: persist current UI view settings (tier, provider, sort) to config.settings
|
|
1415
|
+
// 📖 Called after every T / D / sort key so preferences survive session restarts.
|
|
1416
|
+
function persistUiSettings() {
|
|
1417
|
+
if (!state.config.settings || typeof state.config.settings !== 'object') state.config.settings = {}
|
|
1418
|
+
state.config.settings.tierFilter = TIER_CYCLE[state.tierFilterMode]
|
|
1419
|
+
state.config.settings.originFilter = ORIGIN_CYCLE[state.originFilterMode] ?? null
|
|
1420
|
+
state.config.settings.sortColumn = state.sortColumn
|
|
1421
|
+
state.config.settings.sortAsc = state.sortDirection === 'asc'
|
|
1422
|
+
// 📖 Mirror into active profile too so profile captures live preferences
|
|
1423
|
+
if (state.activeProfile && state.config.profiles?.[state.activeProfile]) {
|
|
1424
|
+
const profile = state.config.profiles[state.activeProfile]
|
|
1425
|
+
if (!profile.settings || typeof profile.settings !== 'object') profile.settings = {}
|
|
1426
|
+
profile.settings.tierFilter = state.config.settings.tierFilter
|
|
1427
|
+
profile.settings.originFilter = state.config.settings.originFilter
|
|
1428
|
+
profile.settings.sortColumn = state.config.settings.sortColumn
|
|
1429
|
+
profile.settings.sortAsc = state.config.settings.sortAsc
|
|
1430
|
+
}
|
|
1431
|
+
saveConfig(state.config)
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
// 📖 Shift+R: reset all UI view settings to defaults (tier, sort, provider) and clear persisted config
|
|
1435
|
+
if (key.name === 'r' && key.shift) {
|
|
1436
|
+
state.tierFilterMode = 0
|
|
1437
|
+
state.originFilterMode = 0
|
|
1438
|
+
state.sortColumn = 'avg'
|
|
1439
|
+
state.sortDirection = 'asc'
|
|
1440
|
+
if (!state.config.settings || typeof state.config.settings !== 'object') state.config.settings = {}
|
|
1441
|
+
delete state.config.settings.tierFilter
|
|
1442
|
+
delete state.config.settings.originFilter
|
|
1443
|
+
delete state.config.settings.sortColumn
|
|
1444
|
+
delete state.config.settings.sortAsc
|
|
1445
|
+
// 📖 Also clear in active profile if loaded
|
|
1446
|
+
if (state.activeProfile && state.config.profiles?.[state.activeProfile]) {
|
|
1447
|
+
const profile = state.config.profiles[state.activeProfile]
|
|
1448
|
+
if (profile.settings) {
|
|
1449
|
+
delete profile.settings.tierFilter
|
|
1450
|
+
delete profile.settings.originFilter
|
|
1451
|
+
delete profile.settings.sortColumn
|
|
1452
|
+
delete profile.settings.sortAsc
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
saveConfig(state.config)
|
|
1456
|
+
applyTierFilter()
|
|
1457
|
+
const visible = state.results.filter(r => !r.hidden)
|
|
1458
|
+
state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection)
|
|
1459
|
+
state.cursor = 0
|
|
1460
|
+
state.scrollOffset = 0
|
|
1461
|
+
return
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1336
1464
|
// 📖 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
|
|
1337
1465
|
// 📖 T is reserved for tier filter cycling. Y now opens the install-endpoints flow.
|
|
1338
1466
|
// 📖 D is now reserved for provider filter cycling
|
|
1467
|
+
// 📖 Shift+R is reserved for reset view settings
|
|
1339
1468
|
const sortKeys = {
|
|
1340
1469
|
'r': 'rank', 'o': 'origin', 'm': 'model',
|
|
1341
1470
|
'l': 'ping', 'a': 'avg', 's': 'swe', 'c': 'ctx', 'h': 'condition', 'v': 'verdict', 'b': 'stability', 'u': 'uptime', 'g': 'usage'
|
|
@@ -1355,6 +1484,7 @@ export function createKeyHandler(ctx) {
|
|
|
1355
1484
|
state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection)
|
|
1356
1485
|
state.cursor = 0
|
|
1357
1486
|
state.scrollOffset = 0
|
|
1487
|
+
persistUiSettings()
|
|
1358
1488
|
return
|
|
1359
1489
|
}
|
|
1360
1490
|
|
|
@@ -1440,6 +1570,7 @@ export function createKeyHandler(ctx) {
|
|
|
1440
1570
|
state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection)
|
|
1441
1571
|
state.cursor = 0
|
|
1442
1572
|
state.scrollOffset = 0
|
|
1573
|
+
persistUiSettings()
|
|
1443
1574
|
return
|
|
1444
1575
|
}
|
|
1445
1576
|
|
|
@@ -1452,6 +1583,7 @@ export function createKeyHandler(ctx) {
|
|
|
1452
1583
|
state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection)
|
|
1453
1584
|
state.cursor = 0
|
|
1454
1585
|
state.scrollOffset = 0
|
|
1586
|
+
persistUiSettings()
|
|
1455
1587
|
return
|
|
1456
1588
|
}
|
|
1457
1589
|
|
|
@@ -1462,6 +1594,18 @@ export function createKeyHandler(ctx) {
|
|
|
1462
1594
|
return
|
|
1463
1595
|
}
|
|
1464
1596
|
|
|
1597
|
+
// 📖 Changelog overlay key: N = toggle changelog overlay
|
|
1598
|
+
if (key.name === 'n') {
|
|
1599
|
+
state.changelogOpen = !state.changelogOpen
|
|
1600
|
+
if (state.changelogOpen) {
|
|
1601
|
+
state.changelogScrollOffset = 0
|
|
1602
|
+
state.changelogPhase = 'index'
|
|
1603
|
+
state.changelogCursor = 0
|
|
1604
|
+
state.changelogSelectedVersion = null
|
|
1605
|
+
}
|
|
1606
|
+
return
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1465
1609
|
// 📖 Mode toggle key: Z cycles through the supported tool targets.
|
|
1466
1610
|
if (key.name === 'z') {
|
|
1467
1611
|
const modeOrder = getToolModeOrder()
|
package/src/overlays.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* @details
|
|
6
6
|
* This module centralizes all overlay rendering in one place:
|
|
7
|
-
* - Settings, Install Endpoints, Help, Log, Smart Recommend, Feature Request, Bug Report
|
|
7
|
+
* - Settings, Install Endpoints, Help, Log, Smart Recommend, Feature Request, Bug Report, Changelog
|
|
8
8
|
* - Settings diagnostics for provider key tests, including wrapped retry/error details
|
|
9
9
|
* - Recommend analysis timer orchestration and progress updates
|
|
10
10
|
*
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
* @exports { createOverlayRenderers }
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
+
import { loadChangelog } from './changelog-loader.js'
|
|
21
|
+
|
|
20
22
|
export function createOverlayRenderers(state, deps) {
|
|
21
23
|
const {
|
|
22
24
|
chalk,
|
|
@@ -138,6 +140,7 @@ export function createOverlayRenderers(state, deps) {
|
|
|
138
140
|
const proxySyncRowIdx = updateRowIdx + 2
|
|
139
141
|
const proxyPortRowIdx = updateRowIdx + 3
|
|
140
142
|
const proxyCleanupRowIdx = updateRowIdx + 4
|
|
143
|
+
const changelogViewRowIdx = updateRowIdx + 5
|
|
141
144
|
const proxySettings = getProxySettings(state.config)
|
|
142
145
|
const EL = '\x1b[K'
|
|
143
146
|
const lines = []
|
|
@@ -296,9 +299,15 @@ export function createOverlayRenderers(state, deps) {
|
|
|
296
299
|
cursorLineByRow[proxyCleanupRowIdx] = lines.length
|
|
297
300
|
lines.push(state.settingsCursor === proxyCleanupRowIdx ? chalk.bgRgb(45, 30, 30)(proxyCleanupRow) : proxyCleanupRow)
|
|
298
301
|
|
|
302
|
+
// 📖 Changelog viewer row
|
|
303
|
+
const changelogViewBullet = state.settingsCursor === changelogViewRowIdx ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
304
|
+
const changelogViewRow = `${changelogViewBullet}${chalk.bold('View Changelog').padEnd(44)} ${chalk.dim('Enter browse version history')}`
|
|
305
|
+
cursorLineByRow[changelogViewRowIdx] = lines.length
|
|
306
|
+
lines.push(state.settingsCursor === changelogViewRowIdx ? chalk.bgRgb(30, 45, 30)(changelogViewRow) : changelogViewRow)
|
|
307
|
+
|
|
299
308
|
// 📖 Profiles section — list saved profiles with active indicator + delete support
|
|
300
309
|
const savedProfiles = listProfiles(state.config)
|
|
301
|
-
const profileStartIdx = updateRowIdx +
|
|
310
|
+
const profileStartIdx = updateRowIdx + 6
|
|
302
311
|
const maxRowIdx = savedProfiles.length > 0 ? profileStartIdx + savedProfiles.length - 1 : updateRowIdx
|
|
303
312
|
|
|
304
313
|
lines.push('')
|
|
@@ -610,6 +619,8 @@ export function createOverlayRenderers(state, deps) {
|
|
|
610
619
|
lines.push(` ${chalk.yellow('Shift+S')} Save current config as a named profile ${chalk.dim('(inline prompt — type name + Enter)')}`)
|
|
611
620
|
lines.push(` ${chalk.dim('Profiles store: favorites, sort, tier filter, ping interval, configured-only filter, API keys.')}`)
|
|
612
621
|
lines.push(` ${chalk.dim('Use --profile <name> to load a profile on startup.')}`)
|
|
622
|
+
lines.push(` ${chalk.yellow('Shift+R')} Reset view settings ${chalk.dim('(tier filter, sort, provider filter → defaults)')}`)
|
|
623
|
+
lines.push(` ${chalk.yellow('N')} Changelog ${chalk.dim('(📋 browse all versions, Enter to view details)')}`)
|
|
613
624
|
lines.push(` ${chalk.yellow('K')} / ${chalk.yellow('Esc')} Show/hide this help`)
|
|
614
625
|
lines.push(` ${chalk.yellow('Ctrl+C')} Exit`)
|
|
615
626
|
lines.push('')
|
|
@@ -1239,6 +1250,97 @@ export function createOverlayRenderers(state, deps) {
|
|
|
1239
1250
|
return cleared.join('\n')
|
|
1240
1251
|
}
|
|
1241
1252
|
|
|
1253
|
+
// ─── Changelog overlay renderer ───────────────────────────────────────────
|
|
1254
|
+
// 📖 renderChangelog: Two-phase overlay — index of all versions or details of one version
|
|
1255
|
+
function renderChangelog() {
|
|
1256
|
+
const EL = '\x1b[K'
|
|
1257
|
+
const lines = []
|
|
1258
|
+
const changelogData = loadChangelog()
|
|
1259
|
+
const { versions } = changelogData
|
|
1260
|
+
const versionList = Object.keys(versions).sort((a, b) => {
|
|
1261
|
+
const aParts = a.split('.').map(Number)
|
|
1262
|
+
const bParts = b.split('.').map(Number)
|
|
1263
|
+
for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
|
|
1264
|
+
const aVal = aParts[i] || 0
|
|
1265
|
+
const bVal = bParts[i] || 0
|
|
1266
|
+
if (bVal !== aVal) return bVal - aVal
|
|
1267
|
+
}
|
|
1268
|
+
return 0
|
|
1269
|
+
})
|
|
1270
|
+
|
|
1271
|
+
// 📖 Branding header
|
|
1272
|
+
lines.push(` ${chalk.cyanBright('🚀')} ${chalk.bold.cyanBright('free-coding-models')} ${chalk.dim(`v${LOCAL_VERSION}`)}`)
|
|
1273
|
+
|
|
1274
|
+
if (state.changelogPhase === 'index') {
|
|
1275
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1276
|
+
// 📖 INDEX PHASE: Show all versions with selection
|
|
1277
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1278
|
+
lines.push(` ${chalk.bold('📋 Changelog - All Versions')}`)
|
|
1279
|
+
lines.push(` ${chalk.dim('— ↑↓ navigate • Enter select • Esc close')}`)
|
|
1280
|
+
lines.push('')
|
|
1281
|
+
|
|
1282
|
+
for (let i = 0; i < versionList.length; i++) {
|
|
1283
|
+
const version = versionList[i]
|
|
1284
|
+
const changes = versions[version]
|
|
1285
|
+
const isSelected = i === state.changelogCursor
|
|
1286
|
+
|
|
1287
|
+
// 📖 Count items in this version
|
|
1288
|
+
let itemCount = 0
|
|
1289
|
+
for (const key of ['added', 'fixed', 'changed', 'updated']) {
|
|
1290
|
+
if (changes[key]) itemCount += changes[key].length
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
// 📖 Format version line with selection highlight
|
|
1294
|
+
const versionStr = ` v${version.padEnd(8)} — ${itemCount} ${itemCount === 1 ? 'change' : 'changes'}`
|
|
1295
|
+
if (isSelected) {
|
|
1296
|
+
lines.push(chalk.inverse(versionStr))
|
|
1297
|
+
} else {
|
|
1298
|
+
lines.push(versionStr)
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
lines.push('')
|
|
1303
|
+
lines.push(` ${chalk.dim(`Total: ${versionList.length} versions`)}`)
|
|
1304
|
+
|
|
1305
|
+
} else if (state.changelogPhase === 'details') {
|
|
1306
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1307
|
+
// 📖 DETAILS PHASE: Show detailed changes for selected version
|
|
1308
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
1309
|
+
lines.push(` ${chalk.bold(`📋 v${state.changelogSelectedVersion}`)}`)
|
|
1310
|
+
lines.push(` ${chalk.dim('— ↑↓ / PgUp / PgDn scroll • B back • Esc close')}`)
|
|
1311
|
+
lines.push('')
|
|
1312
|
+
|
|
1313
|
+
const changes = versions[state.changelogSelectedVersion]
|
|
1314
|
+
if (changes) {
|
|
1315
|
+
const sections = { added: '✨ Added', fixed: '🐛 Fixed', changed: '🔄 Changed', updated: '📝 Updated' }
|
|
1316
|
+
for (const [key, label] of Object.entries(sections)) {
|
|
1317
|
+
if (changes[key] && changes[key].length > 0) {
|
|
1318
|
+
lines.push(` ${chalk.yellow(label)}`)
|
|
1319
|
+
for (const item of changes[key]) {
|
|
1320
|
+
// 📖 Unwrap markdown bold/code markers for display
|
|
1321
|
+
let displayText = item.replace(/\*\*([^*]+)\*\*/g, '$1').replace(/`([^`]+)`/g, '$1')
|
|
1322
|
+
// 📖 Wrap long lines
|
|
1323
|
+
const maxWidth = state.terminalCols - 16
|
|
1324
|
+
if (displayText.length > maxWidth) {
|
|
1325
|
+
displayText = displayText.substring(0, maxWidth - 3) + '…'
|
|
1326
|
+
}
|
|
1327
|
+
lines.push(` • ${displayText}`)
|
|
1328
|
+
}
|
|
1329
|
+
lines.push('')
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
// 📖 Use scrolling with overlay handler
|
|
1336
|
+
const CHANGELOG_OVERLAY_BG = chalk.bgRgb(10, 40, 80) // Dark blue background
|
|
1337
|
+
const { visible, offset } = sliceOverlayLines(lines, state.changelogScrollOffset, state.terminalRows)
|
|
1338
|
+
state.changelogScrollOffset = offset
|
|
1339
|
+
const tintedLines = tintOverlayLines(visible, CHANGELOG_OVERLAY_BG, state.terminalCols)
|
|
1340
|
+
const cleared = tintedLines.map(l => l + EL)
|
|
1341
|
+
return cleared.join('\n')
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1242
1344
|
// 📖 stopRecommendAnalysis: cleanup timers if user cancels during analysis
|
|
1243
1345
|
function stopRecommendAnalysis() {
|
|
1244
1346
|
if (state.recommendAnalysisTimer) { clearInterval(state.recommendAnalysisTimer); state.recommendAnalysisTimer = null }
|
|
@@ -1253,6 +1355,7 @@ export function createOverlayRenderers(state, deps) {
|
|
|
1253
1355
|
renderRecommend,
|
|
1254
1356
|
renderFeatureRequest,
|
|
1255
1357
|
renderBugReport,
|
|
1358
|
+
renderChangelog,
|
|
1256
1359
|
startRecommendAnalysis,
|
|
1257
1360
|
stopRecommendAnalysis,
|
|
1258
1361
|
}
|
package/src/render-table.js
CHANGED
|
@@ -92,7 +92,7 @@ export function setActiveProxy(proxyInstance) {
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
// ─── renderTable: mode param controls footer hint text (opencode vs openclaw) ─────────
|
|
95
|
-
export function renderTable(results, pendingPings, frame, cursor = null, sortColumn = 'avg', sortDirection = 'asc', pingInterval = PING_INTERVAL, lastPingTime = Date.now(), mode = 'opencode', tierFilterMode = 0, scrollOffset = 0, terminalRows = 0, terminalCols = 0, originFilterMode = 0, activeProfile = null, profileSaveMode = false, profileSaveBuffer = '', proxyStartupStatus = null, pingMode = 'normal', pingModeSource = 'auto', hideUnconfiguredModels = false, widthWarningStartedAt = null, widthWarningDismissed = false, settingsUpdateState = 'idle', settingsUpdateLatestVersion = null, proxyEnabled = false) {
|
|
95
|
+
export function renderTable(results, pendingPings, frame, cursor = null, sortColumn = 'avg', sortDirection = 'asc', pingInterval = PING_INTERVAL, lastPingTime = Date.now(), mode = 'opencode', tierFilterMode = 0, scrollOffset = 0, terminalRows = 0, terminalCols = 0, originFilterMode = 0, activeProfile = null, profileSaveMode = false, profileSaveBuffer = '', proxyStartupStatus = null, pingMode = 'normal', pingModeSource = 'auto', hideUnconfiguredModels = false, widthWarningStartedAt = null, widthWarningDismissed = false, settingsUpdateState = 'idle', settingsUpdateLatestVersion = null, proxyEnabled = false, isOutdated = false, latestVersion = null) {
|
|
96
96
|
// 📖 Filter out hidden models for display
|
|
97
97
|
const visibleResults = results.filter(r => !r.hidden)
|
|
98
98
|
|
|
@@ -648,25 +648,35 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
648
648
|
const latestLabel = chalk.redBright(` local v${LOCAL_VERSION} · latest v${versionStatus.latestVersion}`)
|
|
649
649
|
lines.push(` ${outdatedBadge}${latestLabel}`)
|
|
650
650
|
}
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
chalk.
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
)
|
|
651
|
+
|
|
652
|
+
// 📖 Build footer line, with OUTDATED warning if isOutdated is true
|
|
653
|
+
let footerLine = ''
|
|
654
|
+
if (isOutdated) {
|
|
655
|
+
// 📖 Show OUTDATED in red background, high contrast warning
|
|
656
|
+
footerLine = chalk.bgRed.bold.white(' ⚠ OUTDATED version, please update with "npm i -g free-coding-models@latest" ')
|
|
657
|
+
} else {
|
|
658
|
+
footerLine =
|
|
659
|
+
chalk.rgb(255, 150, 200)(' Made with 💖 & ☕ by \x1b]8;;https://github.com/vava-nessa\x1b\\vava-nessa\x1b]8;;\x1b\\') +
|
|
660
|
+
chalk.dim(' • ') +
|
|
661
|
+
'⭐ ' +
|
|
662
|
+
chalk.yellow('\x1b]8;;https://github.com/vava-nessa/free-coding-models\x1b\\Star on GitHub\x1b]8;;\x1b\\') +
|
|
663
|
+
chalk.dim(' • ') +
|
|
664
|
+
'🤝 ' +
|
|
665
|
+
chalk.rgb(255, 165, 0)('\x1b]8;;https://github.com/vava-nessa/free-coding-models/graphs/contributors\x1b\\Contributors\x1b]8;;\x1b\\') +
|
|
666
|
+
chalk.dim(' • ') +
|
|
667
|
+
'☕ ' +
|
|
668
|
+
chalk.rgb(255, 200, 100)('\x1b]8;;https://buymeacoffee.com/vavanessadev\x1b\\Buy me a coffee\x1b]8;;\x1b\\') +
|
|
669
|
+
chalk.dim(' • ') +
|
|
670
|
+
'💬 ' +
|
|
671
|
+
chalk.rgb(200, 150, 255)('\x1b]8;;https://discord.gg/ZTNFHvvCkU\x1b\\Discord\x1b]8;;\x1b\\') +
|
|
672
|
+
chalk.dim(' → ') +
|
|
673
|
+
chalk.rgb(200, 150, 255)('https://discord.gg/ZTNFHvvCkU') +
|
|
674
|
+
chalk.dim(' • ') +
|
|
675
|
+
chalk.yellow('N') + chalk.dim(' Changelog') +
|
|
676
|
+
chalk.dim(' • ') +
|
|
677
|
+
chalk.dim('Ctrl+C Exit')
|
|
678
|
+
}
|
|
679
|
+
lines.push(footerLine)
|
|
670
680
|
|
|
671
681
|
// 📖 Append \x1b[K (erase to EOL) to each line so leftover chars from previous
|
|
672
682
|
// 📖 frames are cleared. Then pad with blank cleared lines to fill the terminal,
|
package/src/tool-launchers.js
CHANGED
|
@@ -228,25 +228,36 @@ function writeQwenConfig(model, providerKey, apiKey, baseUrl) {
|
|
|
228
228
|
}
|
|
229
229
|
|
|
230
230
|
function writePiConfig(model, apiKey, baseUrl) {
|
|
231
|
-
|
|
232
|
-
const
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
231
|
+
// 📖 Write models.json with the selected provider config
|
|
232
|
+
const modelsFilePath = join(homedir(), '.pi', 'agent', 'models.json')
|
|
233
|
+
const modelsBackupPath = backupIfExists(modelsFilePath)
|
|
234
|
+
const modelsConfig = readJson(modelsFilePath, { providers: {} })
|
|
235
|
+
if (!modelsConfig.providers || typeof modelsConfig.providers !== 'object') modelsConfig.providers = {}
|
|
236
|
+
modelsConfig.providers.freeCodingModels = {
|
|
236
237
|
baseUrl,
|
|
237
238
|
api: 'openai-completions',
|
|
238
239
|
apiKey,
|
|
239
240
|
models: [{ id: model.modelId, name: model.label }],
|
|
240
241
|
}
|
|
241
|
-
writeJson(
|
|
242
|
-
|
|
242
|
+
writeJson(modelsFilePath, modelsConfig)
|
|
243
|
+
|
|
244
|
+
// 📖 Write settings.json to set the model as default on next launch
|
|
245
|
+
const settingsFilePath = join(homedir(), '.pi', 'agent', 'settings.json')
|
|
246
|
+
const settingsBackupPath = backupIfExists(settingsFilePath)
|
|
247
|
+
const settingsConfig = readJson(settingsFilePath, {})
|
|
248
|
+
settingsConfig.defaultProvider = 'freeCodingModels'
|
|
249
|
+
settingsConfig.defaultModel = model.modelId
|
|
250
|
+
writeJson(settingsFilePath, settingsConfig)
|
|
251
|
+
|
|
252
|
+
return { filePath: modelsFilePath, backupPath: modelsBackupPath, settingsFilePath, settingsBackupPath }
|
|
243
253
|
}
|
|
244
254
|
|
|
245
|
-
function writeAmpConfig(baseUrl) {
|
|
255
|
+
function writeAmpConfig(model, baseUrl) {
|
|
246
256
|
const filePath = join(homedir(), '.config', 'amp', 'settings.json')
|
|
247
257
|
const backupPath = backupIfExists(filePath)
|
|
248
258
|
const config = readJson(filePath, {})
|
|
249
259
|
config['amp.url'] = baseUrl
|
|
260
|
+
config['amp.model'] = model.modelId
|
|
250
261
|
writeJson(filePath, config)
|
|
251
262
|
return { filePath, backupPath }
|
|
252
263
|
}
|
|
@@ -346,19 +357,24 @@ export async function startExternalTool(mode, model, config) {
|
|
|
346
357
|
}
|
|
347
358
|
|
|
348
359
|
if (mode === 'openhands') {
|
|
349
|
-
|
|
360
|
+
// 📖 OpenHands supports LLM_MODEL env var to set the default model
|
|
361
|
+
env.LLM_MODEL = model.modelId
|
|
362
|
+
env.LLM_API_KEY = apiKey || env.LLM_API_KEY
|
|
363
|
+
if (baseUrl) env.LLM_BASE_URL = baseUrl
|
|
364
|
+
console.log(chalk.dim(` 📖 OpenHands launched with model: ${model.modelId}`))
|
|
350
365
|
return spawnCommand('openhands', ['--override-with-envs'], env)
|
|
351
366
|
}
|
|
352
367
|
|
|
353
368
|
if (mode === 'amp') {
|
|
354
|
-
printConfigResult(meta.label, writeAmpConfig(baseUrl))
|
|
355
|
-
console.log(chalk.
|
|
356
|
-
console.log(chalk.dim(' The proxy URL is written, then Amp is launched so you can reuse the current endpoint.'))
|
|
369
|
+
printConfigResult(meta.label, writeAmpConfig(model, baseUrl))
|
|
370
|
+
console.log(chalk.dim(` 📖 Amp config updated with model: ${model.modelId}`))
|
|
357
371
|
return spawnCommand('amp', [], env)
|
|
358
372
|
}
|
|
359
373
|
|
|
360
374
|
if (mode === 'pi') {
|
|
361
|
-
|
|
375
|
+
const piResult = writePiConfig(model, apiKey, baseUrl)
|
|
376
|
+
printConfigResult(meta.label, { filePath: piResult.filePath, backupPath: piResult.backupPath })
|
|
377
|
+
printConfigResult(meta.label, { filePath: piResult.settingsFilePath, backupPath: piResult.settingsBackupPath })
|
|
362
378
|
return spawnCommand('pi', [], env)
|
|
363
379
|
}
|
|
364
380
|
|