free-coding-models 0.3.68 → 0.3.70
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/README.md +13 -13
- package/changelog/v0.3.69.md +47 -0
- package/changelog/v0.3.70.md +8 -0
- package/package.json +2 -2
- package/sources.js +92 -66
- package/src/app.js +89 -358
- package/src/ping-loop.js +105 -0
- package/src/render-table.js +79 -3
- package/src/tui-filters.js +128 -0
- package/src/tui-state.js +265 -0
- package/src/updater.js +1 -105
- package/web/dist/assets/{index-Cfy_uz7_.js → index-BTwSEyBT.js} +4 -4
- package/web/dist/assets/{index-DwztVNMT.css → index-CGN-0_A0.css} +1 -1
- package/web/dist/index.html +2 -2
- package/web/src/App.jsx +7 -0
- package/web/src/components/layout/Sidebar.jsx +1 -0
- package/web/src/components/map/MapView.jsx +17 -0
- package/web/src/components/map/MapView.module.css +25 -0
package/src/render-table.js
CHANGED
|
@@ -103,11 +103,87 @@ export const PROVIDER_COLOR = new Proxy({}, {
|
|
|
103
103
|
},
|
|
104
104
|
})
|
|
105
105
|
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
/**
|
|
107
|
+
* 📖 renderTable: Render the full TUI table as a string (no side effects).
|
|
108
|
+
* 📖 Accepts a single options object so adding/removing params never silently breaks call sites.
|
|
109
|
+
* 📖 `mode` controls footer hint text (opencode vs openclaw).
|
|
110
|
+
*
|
|
111
|
+
* @param {{
|
|
112
|
+
* results: Array,
|
|
113
|
+
* pendingPings: number,
|
|
114
|
+
* frame: number,
|
|
115
|
+
* cursor: number|null,
|
|
116
|
+
* sortColumn: string,
|
|
117
|
+
* sortDirection: string,
|
|
118
|
+
* pingInterval: number,
|
|
119
|
+
* lastPingTime: number,
|
|
120
|
+
* mode: string,
|
|
121
|
+
* tierFilterMode: number,
|
|
122
|
+
* scrollOffset: number,
|
|
123
|
+
* terminalRows: number,
|
|
124
|
+
* terminalCols: number,
|
|
125
|
+
* originFilterMode: number,
|
|
126
|
+
* pingMode: string,
|
|
127
|
+
* pingModeSource: string,
|
|
128
|
+
* hideUnconfiguredModels: boolean,
|
|
129
|
+
* widthWarningStartedAt: number|null,
|
|
130
|
+
* widthWarningDismissed: boolean,
|
|
131
|
+
* settingsUpdateState: string,
|
|
132
|
+
* settingsUpdateLatestVersion: string|null,
|
|
133
|
+
* startupLatestVersion: string|null,
|
|
134
|
+
* versionAlertsEnabled: boolean,
|
|
135
|
+
* favoritesPinnedAndSticky: boolean,
|
|
136
|
+
* customTextFilter: string|null,
|
|
137
|
+
* lastReleaseDate: string|null,
|
|
138
|
+
* verdictFilterMode: number,
|
|
139
|
+
* healthFilterMode: number,
|
|
140
|
+
* bestModeOnly: boolean,
|
|
141
|
+
* routerFooterRunning?: boolean,
|
|
142
|
+
* routerFooterActiveSet?: string|null,
|
|
143
|
+
* routerFooterTodayTokens?: number,
|
|
144
|
+
* routerFooterAllTimeTokens?: number,
|
|
145
|
+
* routerFooterRequests?: number,
|
|
146
|
+
* }} opts
|
|
147
|
+
* @returns {string}
|
|
148
|
+
*/
|
|
149
|
+
export function renderTable({
|
|
150
|
+
results = [],
|
|
151
|
+
pendingPings = 0,
|
|
152
|
+
frame = 0,
|
|
153
|
+
cursor = null,
|
|
154
|
+
sortColumn = 'avg',
|
|
155
|
+
sortDirection = 'asc',
|
|
156
|
+
pingInterval = PING_INTERVAL,
|
|
157
|
+
lastPingTime = Date.now(),
|
|
158
|
+
mode = 'opencode',
|
|
159
|
+
tierFilterMode = 0,
|
|
160
|
+
scrollOffset = 0,
|
|
161
|
+
terminalRows = 0,
|
|
162
|
+
terminalCols = 0,
|
|
163
|
+
originFilterMode = 0,
|
|
164
|
+
pingMode = 'normal',
|
|
165
|
+
pingModeSource = 'auto',
|
|
166
|
+
hideUnconfiguredModels = false,
|
|
167
|
+
widthWarningStartedAt = null,
|
|
168
|
+
widthWarningDismissed = false,
|
|
169
|
+
settingsUpdateState = 'idle',
|
|
170
|
+
settingsUpdateLatestVersion = null,
|
|
171
|
+
startupLatestVersion = null,
|
|
172
|
+
versionAlertsEnabled = true,
|
|
173
|
+
favoritesPinnedAndSticky = false,
|
|
174
|
+
customTextFilter = null,
|
|
175
|
+
lastReleaseDate = null,
|
|
176
|
+
verdictFilterMode = 0,
|
|
177
|
+
healthFilterMode = 0,
|
|
178
|
+
bestModeOnly = false,
|
|
179
|
+
routerFooterRunning = false,
|
|
180
|
+
routerFooterActiveSet = null,
|
|
181
|
+
routerFooterTodayTokens = 0,
|
|
182
|
+
routerFooterAllTimeTokens = 0,
|
|
183
|
+
routerFooterRequests = 0,
|
|
184
|
+
} = {}) {
|
|
108
185
|
// 📖 Filter out hidden models for display
|
|
109
186
|
const visibleResults = results.filter(r => !r.hidden)
|
|
110
|
-
void legacyFooterHidden
|
|
111
187
|
|
|
112
188
|
const up = visibleResults.filter(r => r.status === 'up').length
|
|
113
189
|
const down = visibleResults.filter(r => r.status === 'down').length
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file tui-filters.js
|
|
3
|
+
* @description TUI filter logic — tier, origin, verdict, health, text, and usability filters.
|
|
4
|
+
*
|
|
5
|
+
* @details
|
|
6
|
+
* All the filtering that determines which model rows are visible in the TUI table
|
|
7
|
+
* lives here. The main app.js calls applyTierFilter() on each frame and after every
|
|
8
|
+
* keypress that changes a filter mode, so the visible set is always consistent.
|
|
9
|
+
*
|
|
10
|
+
* 🎯 Filter precedence (first failing check hides the row):
|
|
11
|
+
* 1. Configure-only mode (E key) — hide models with no API key or bad health
|
|
12
|
+
* 2. Usable-only mode (E key, second cycle) — only Health UP + verdict Normal/Perfect/Slow
|
|
13
|
+
* 3. Sticky favorites (Y key) — pinned favorites bypass tier/origin/text filters
|
|
14
|
+
* 4. Tier filter (T key) — only show models in the selected tier family
|
|
15
|
+
* 5. Origin filter (O key) — only show models from the selected provider
|
|
16
|
+
* 6. Verdict filter (V key) — only show models with the selected verdict
|
|
17
|
+
* 7. Health filter (H key) — only show models with the selected health status
|
|
18
|
+
* 8. Custom text filter (Ctrl+P) — case-insensitive match on name, ctx, provider
|
|
19
|
+
*
|
|
20
|
+
* @functions
|
|
21
|
+
* → createTuiFilters(state, deps) — Returns { applyTierFilter, buildOriginCycle }
|
|
22
|
+
*
|
|
23
|
+
* @exports createTuiFilters
|
|
24
|
+
*
|
|
25
|
+
* @see src/app.js — calls applyTierFilter() on each render frame
|
|
26
|
+
* @see src/tui-state.js — state shape for filter mode indices
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { TIER_CYCLE, VERDICT_CYCLE, HEALTH_CYCLE } from './constants.js'
|
|
30
|
+
import { TIER_LETTER_MAP } from './utils.js'
|
|
31
|
+
import { getVerdict } from './utils.js'
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 📖 createTuiFilters: Build the filter functions for a given TUI state + dependencies.
|
|
35
|
+
*
|
|
36
|
+
* @param {object} state — The TUI state object (from createTuiState)
|
|
37
|
+
* @param {{
|
|
38
|
+
* sources: object,
|
|
39
|
+
* getApiKey: function,
|
|
40
|
+
* PROVIDER_METADATA: object,
|
|
41
|
+
* }} deps — External dependencies needed by filter logic
|
|
42
|
+
* @returns {{ applyTierFilter: () => Array, buildOriginCycle: () => Array }}
|
|
43
|
+
*/
|
|
44
|
+
export function createTuiFilters(state, { sources, getApiKey, PROVIDER_METADATA }) {
|
|
45
|
+
/**
|
|
46
|
+
* 📖 applyTierFilter: Apply all active filters to the model results.
|
|
47
|
+
* 📖 Mutates r.hidden in-place for each result row.
|
|
48
|
+
* 📖 Returns the results array for chaining.
|
|
49
|
+
*/
|
|
50
|
+
function applyTierFilter() {
|
|
51
|
+
const activeTier = TIER_CYCLE[state.tierFilterMode]
|
|
52
|
+
const activeOrigin = buildOriginCycle()[state.originFilterMode]
|
|
53
|
+
const activeVerdict = VERDICT_CYCLE[state.verdictFilterMode]
|
|
54
|
+
const activeHealth = HEALTH_CYCLE[state.healthFilterMode]
|
|
55
|
+
|
|
56
|
+
state.results.forEach(r => {
|
|
57
|
+
const stickyFavorite = state.favoritesPinnedAndSticky && r.isFavorite
|
|
58
|
+
// 📖 CLI-only tools (rovo, gemini) and Zen models don't need traditional API keys —
|
|
59
|
+
// 📖 they authenticate via their own CLI login flow, so "configured only" should never hide them.
|
|
60
|
+
const providerMeta = PROVIDER_METADATA[r.providerKey]
|
|
61
|
+
const noKeyNeeded = providerMeta?.cliOnly || providerMeta?.zenOnly
|
|
62
|
+
// 📖 E toggles "Show only configured & working models":
|
|
63
|
+
// 📖 hide models where provider has no key, or where the health status is noauth/auth_error (but keep timeout and 429)
|
|
64
|
+
const badHealth = r.status === 'noauth' || r.status === 'auth_error'
|
|
65
|
+
const unconfiguredHide = state.hideUnconfiguredModels && !noKeyNeeded && (!getApiKey(state.config, r.providerKey) || badHealth)
|
|
66
|
+
if (unconfiguredHide) {
|
|
67
|
+
r.hidden = true
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
// 📖 Usable only: only show models with Health UP and Verdict Perfect/Normal/Slow
|
|
71
|
+
if (state.bestModeOnly) {
|
|
72
|
+
const bmVerdict = getVerdict(r)
|
|
73
|
+
const bmVerdictOk = ['Perfect', 'Normal', 'Slow'].includes(bmVerdict)
|
|
74
|
+
const bmHealthOk = r.status === 'up'
|
|
75
|
+
if (!bmHealthOk || !bmVerdictOk) {
|
|
76
|
+
r.hidden = true
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// 📖 Sticky-favorites mode keeps usable favorites visible regardless of
|
|
81
|
+
// 📖 tier/provider/text filters, but "Usable only" health still wins above.
|
|
82
|
+
if (stickyFavorite) {
|
|
83
|
+
r.hidden = false
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
// 📖 Apply tier, origin, verdict, and health filters — model is hidden if it fails any
|
|
87
|
+
const allowedTiers = (activeTier && TIER_LETTER_MAP[activeTier]) ? TIER_LETTER_MAP[activeTier] : [activeTier]
|
|
88
|
+
const tierHide = activeTier !== null && !allowedTiers.includes(r.tier)
|
|
89
|
+
const originHide = activeOrigin !== null && r.providerKey !== activeOrigin
|
|
90
|
+
// 📖 Verdict filter: match against getVerdict(r) when active
|
|
91
|
+
const rVerdict = getVerdict(r)
|
|
92
|
+
const verdictHide = activeVerdict !== null && rVerdict !== activeVerdict
|
|
93
|
+
// 📖 Health filter: match against r.status when active
|
|
94
|
+
const healthHide = activeHealth !== null && r.status !== activeHealth
|
|
95
|
+
if (tierHide || originHide || verdictHide || healthHide) {
|
|
96
|
+
r.hidden = true
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
// 📖 Custom text filter — case-insensitive includes match against model name, ctx, provider key, and provider display name.
|
|
100
|
+
if (state.customTextFilter) {
|
|
101
|
+
const q = state.customTextFilter.toLowerCase()
|
|
102
|
+
const providerName = (sources[r.providerKey]?.name || '').toLowerCase()
|
|
103
|
+
const match = (r.label || '').toLowerCase().includes(q)
|
|
104
|
+
|| (r.ctx || '').toLowerCase().includes(q)
|
|
105
|
+
|| (r.providerKey || '').toLowerCase().includes(q)
|
|
106
|
+
|| providerName.includes(q)
|
|
107
|
+
r.hidden = !match
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
r.hidden = false
|
|
111
|
+
})
|
|
112
|
+
return state.results
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 📖 buildOriginCycle: Build the origin filter cycle array on demand.
|
|
117
|
+
* 📖 [null, ...providerKeys] — null = "All", then each provider key in sources.js order.
|
|
118
|
+
* @returns {Array<string|null>}
|
|
119
|
+
*/
|
|
120
|
+
function buildOriginCycle() {
|
|
121
|
+
return [null, ...Object.keys(sources)]
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
applyTierFilter,
|
|
126
|
+
buildOriginCycle,
|
|
127
|
+
}
|
|
128
|
+
}
|
package/src/tui-state.js
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file tui-state.js
|
|
3
|
+
* @description Factory for the TUI state object — extracted from src/app.js for maintainability.
|
|
4
|
+
*
|
|
5
|
+
* @details
|
|
6
|
+
* The state object is the single source of truth for the interactive TUI.
|
|
7
|
+
* It holds all mutable UI state: cursor position, filter modes, overlay flags,
|
|
8
|
+
* ping cadence timing, settings editing buffers, and live result data.
|
|
9
|
+
*
|
|
10
|
+
* 🎯 Why a factory instead of inline in app.js?
|
|
11
|
+
* - Reduces runApp() from 1,300+ lines — state initialization alone was 250+ lines.
|
|
12
|
+
* - Makes state shape inspectable and testable in isolation.
|
|
13
|
+
* - Future: could generate TypeScript types from the default values.
|
|
14
|
+
*
|
|
15
|
+
* 📖 Ping cadence constants (PING_MODE_INTERVALS, PING_MODE_CYCLE, etc.) live here
|
|
16
|
+
* because they are tightly coupled to the state fields they initialize.
|
|
17
|
+
*
|
|
18
|
+
* @functions
|
|
19
|
+
* → createTuiState(opts) — Build the full initial state object with computed defaults
|
|
20
|
+
*
|
|
21
|
+
* @exports createTuiState, PING_MODE_INTERVALS, PING_MODE_CYCLE, SPEED_MODE_DURATION_MS, IDLE_SLOW_AFTER_MS
|
|
22
|
+
*
|
|
23
|
+
* @see src/app.js — calls createTuiState() once at startup
|
|
24
|
+
* @see src/ping-loop.js — reads ping mode fields from the state
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { WIDTH_WARNING_MIN_COLS } from './constants.js'
|
|
28
|
+
|
|
29
|
+
// 📖 Ping cadence intervals per mode (ms). Speed = startup burst, normal = steady,
|
|
30
|
+
// 📖 slow = idle throttle, forced = user-triggered fast burst.
|
|
31
|
+
export const PING_MODE_INTERVALS = {
|
|
32
|
+
speed: 2_000,
|
|
33
|
+
normal: 10_000,
|
|
34
|
+
slow: 30_000,
|
|
35
|
+
forced: 4_000,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 📖 Ping mode cycle order — used by keyboard handler to cycle through modes.
|
|
39
|
+
export const PING_MODE_CYCLE = ['speed', 'normal', 'slow', 'forced']
|
|
40
|
+
|
|
41
|
+
// 📖 Speed mode auto-falls back to normal after this duration.
|
|
42
|
+
export const SPEED_MODE_DURATION_MS = 60_000
|
|
43
|
+
|
|
44
|
+
// 📖 After this much inactivity, the ping loop slows down to save quota.
|
|
45
|
+
export const IDLE_SLOW_AFTER_MS = 5 * 60_000
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 📖 intervalToPingMode: Map a raw interval (ms) to the closest ping mode label.
|
|
49
|
+
* @param {number} intervalMs
|
|
50
|
+
* @returns {string}
|
|
51
|
+
*/
|
|
52
|
+
export function intervalToPingMode(intervalMs) {
|
|
53
|
+
if (intervalMs <= 3000) return 'speed'
|
|
54
|
+
if (intervalMs <= 5000) return 'forced'
|
|
55
|
+
if (intervalMs >= 30000) return 'slow'
|
|
56
|
+
return 'normal'
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 📖 createTuiState: Build the full initial TUI state object.
|
|
61
|
+
*
|
|
62
|
+
* 📖 Accepts pre-computed values (results, config, mode, etc.) that come from runApp's
|
|
63
|
+
* 📖 startup logic (OpenRouter discovery, key setup, etc.) and fills in all the
|
|
64
|
+
* 📖 default overlay/ping/filter state that the TUI needs on frame 1.
|
|
65
|
+
*
|
|
66
|
+
* @param {{
|
|
67
|
+
* results: Array,
|
|
68
|
+
* config: object,
|
|
69
|
+
* mode: string,
|
|
70
|
+
* sessionId: string,
|
|
71
|
+
* latestVersion: string|null,
|
|
72
|
+
* isDevMode: boolean,
|
|
73
|
+
* }} opts
|
|
74
|
+
* @returns {object} The complete TUI state (mutable)
|
|
75
|
+
*/
|
|
76
|
+
export function createTuiState({
|
|
77
|
+
results,
|
|
78
|
+
config,
|
|
79
|
+
mode,
|
|
80
|
+
sessionId,
|
|
81
|
+
latestVersion,
|
|
82
|
+
isDevMode,
|
|
83
|
+
}) {
|
|
84
|
+
const now = Date.now()
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
// 📖 Core data: model results (mutated in-place by ping loop)
|
|
88
|
+
results,
|
|
89
|
+
pendingPings: 0,
|
|
90
|
+
|
|
91
|
+
// 📖 Animation frame counter — incremented each render tick
|
|
92
|
+
frame: 0,
|
|
93
|
+
|
|
94
|
+
// 📖 Cursor position in the visible sorted table
|
|
95
|
+
cursor: 0,
|
|
96
|
+
selectedModel: null,
|
|
97
|
+
|
|
98
|
+
// 📖 Sorting preferences
|
|
99
|
+
sortColumn: config.settings?.sortColumn ?? 'avg',
|
|
100
|
+
sortDirection: (config.settings?.sortAsc ?? true) ? 'asc' : 'desc',
|
|
101
|
+
|
|
102
|
+
// 📖 Ping cadence — drives the interval between background ping cycles
|
|
103
|
+
pingInterval: PING_MODE_INTERVALS.speed,
|
|
104
|
+
pingMode: 'speed',
|
|
105
|
+
pingModeSource: 'startup',
|
|
106
|
+
speedModeUntil: now + SPEED_MODE_DURATION_MS,
|
|
107
|
+
lastPingTime: now,
|
|
108
|
+
lastUserActivityAt: now,
|
|
109
|
+
resumeSpeedOnActivity: false,
|
|
110
|
+
|
|
111
|
+
// 📖 Version tracking — startup auto-check + periodic re-check
|
|
112
|
+
startupLatestVersion: latestVersion,
|
|
113
|
+
lastReleaseDate: null,
|
|
114
|
+
versionAlertsEnabled: !isDevMode,
|
|
115
|
+
|
|
116
|
+
// 📖 Tool mode — determines which external CLI opens on Enter (opencode, openclaw, crush, etc.)
|
|
117
|
+
mode,
|
|
118
|
+
|
|
119
|
+
// 📖 Filter mode indices — each cycles through a predefined sequence
|
|
120
|
+
tierFilterMode: 0,
|
|
121
|
+
originFilterMode: 0,
|
|
122
|
+
verdictFilterMode: 0,
|
|
123
|
+
healthFilterMode: 0,
|
|
124
|
+
|
|
125
|
+
// 📖 E-key toggle: Normal → Configured only → Usable only
|
|
126
|
+
hideUnconfiguredModels: config.settings?.hideUnconfiguredModels === true,
|
|
127
|
+
bestModeOnly: false,
|
|
128
|
+
|
|
129
|
+
// 📖 Favorites pinning — Y key toggles pinned+sticky mode
|
|
130
|
+
favoritesPinnedAndSticky: config.settings?.favoritesPinnedAndSticky === true,
|
|
131
|
+
|
|
132
|
+
// 📖 Viewport scroll + terminal dimensions
|
|
133
|
+
scrollOffset: 0,
|
|
134
|
+
terminalRows: process.stdout.rows || 24,
|
|
135
|
+
terminalCols: process.stdout.columns || 80,
|
|
136
|
+
widthWarningStartedAt: (process.stdout.columns || 80) < WIDTH_WARNING_MIN_COLS ? now : null,
|
|
137
|
+
widthWarningDismissed: false,
|
|
138
|
+
widthWarningShowCount: 0, // 📖 No longer used — kept for backward compatibility.
|
|
139
|
+
|
|
140
|
+
// 📖 Settings screen state (P key opens it)
|
|
141
|
+
settingsOpen: false,
|
|
142
|
+
settingsCursor: 0,
|
|
143
|
+
settingsEditMode: false,
|
|
144
|
+
settingsAddKeyMode: false,
|
|
145
|
+
settingsEditBuffer: '',
|
|
146
|
+
settingsErrorMsg: null,
|
|
147
|
+
settingsTestResults: {},
|
|
148
|
+
settingsTestDetails: {},
|
|
149
|
+
settingsUpdateState: 'idle',
|
|
150
|
+
settingsUpdateLatestVersion: null,
|
|
151
|
+
settingsUpdateError: null,
|
|
152
|
+
|
|
153
|
+
// 📖 Live config reference — mutated in-place by save/restore
|
|
154
|
+
config,
|
|
155
|
+
sessionId,
|
|
156
|
+
|
|
157
|
+
// 📖 Cached visible+sorted models — recomputed each frame by the render loop
|
|
158
|
+
visibleSorted: [],
|
|
159
|
+
|
|
160
|
+
// 📖 Command palette (Ctrl+P)
|
|
161
|
+
commandPaletteOpen: false,
|
|
162
|
+
commandPaletteQuery: '',
|
|
163
|
+
commandPaletteCursor: 0,
|
|
164
|
+
commandPaletteScrollOffset: 0,
|
|
165
|
+
commandPaletteResults: [],
|
|
166
|
+
commandPaletteFrozenTable: null,
|
|
167
|
+
commandPaletteExpandedIds: new Set(['filters', 'actions']),
|
|
168
|
+
|
|
169
|
+
// 📖 Help overlay (K key)
|
|
170
|
+
helpVisible: false,
|
|
171
|
+
settingsScrollOffset: 0,
|
|
172
|
+
helpScrollOffset: 0,
|
|
173
|
+
|
|
174
|
+
// 📖 Install Endpoints overlay (opened from Settings or Command Palette)
|
|
175
|
+
installEndpointsOpen: false,
|
|
176
|
+
installEndpointsPhase: 'providers',
|
|
177
|
+
installEndpointsCursor: 0,
|
|
178
|
+
installEndpointsScrollOffset: 0,
|
|
179
|
+
installEndpointsProviderKey: null,
|
|
180
|
+
installEndpointsToolMode: null,
|
|
181
|
+
installEndpointsConnectionMode: null,
|
|
182
|
+
installEndpointsScope: null,
|
|
183
|
+
installEndpointsSelectedModelIds: new Set(),
|
|
184
|
+
installEndpointsErrorMsg: null,
|
|
185
|
+
installEndpointsResult: null,
|
|
186
|
+
|
|
187
|
+
// 📖 Missing-tool bootstrap overlay
|
|
188
|
+
toolInstallPromptOpen: false,
|
|
189
|
+
toolInstallPromptCursor: 0,
|
|
190
|
+
toolInstallPromptScrollOffset: 0,
|
|
191
|
+
toolInstallPromptMode: null,
|
|
192
|
+
toolInstallPromptModel: null,
|
|
193
|
+
toolInstallPromptPlan: null,
|
|
194
|
+
toolInstallPromptErrorMsg: null,
|
|
195
|
+
|
|
196
|
+
// 📖 Incompatible model fallback overlay
|
|
197
|
+
incompatibleFallbackOpen: false,
|
|
198
|
+
incompatibleFallbackCursor: 0,
|
|
199
|
+
incompatibleFallbackScrollOffset: 0,
|
|
200
|
+
incompatibleFallbackModel: null,
|
|
201
|
+
incompatibleFallbackTools: [],
|
|
202
|
+
incompatibleFallbackSimilarModels: [],
|
|
203
|
+
incompatibleFallbackSection: 'tools',
|
|
204
|
+
|
|
205
|
+
// 📖 Smart Recommend overlay (Q key)
|
|
206
|
+
recommendOpen: false,
|
|
207
|
+
recommendPhase: 'questionnaire',
|
|
208
|
+
recommendCursor: 0,
|
|
209
|
+
recommendQuestion: 0,
|
|
210
|
+
recommendAnswers: { taskType: null, priority: null, contextBudget: null },
|
|
211
|
+
recommendProgress: 0,
|
|
212
|
+
recommendResults: [],
|
|
213
|
+
recommendScrollOffset: 0,
|
|
214
|
+
recommendAnalysisTimer: null,
|
|
215
|
+
recommendPingTimer: null,
|
|
216
|
+
recommendedKeys: new Set(),
|
|
217
|
+
|
|
218
|
+
// 📖 OpenCode sync status (S key in settings)
|
|
219
|
+
settingsSyncStatus: null,
|
|
220
|
+
|
|
221
|
+
// 📖 Changelog overlay (N key)
|
|
222
|
+
changelogOpen: false,
|
|
223
|
+
changelogScrollOffset: 0,
|
|
224
|
+
changelogPhase: 'index',
|
|
225
|
+
changelogCursor: 0,
|
|
226
|
+
changelogSelectedVersion: null,
|
|
227
|
+
|
|
228
|
+
// 📖 Installed Models overlay (Command Palette → Installed models)
|
|
229
|
+
installedModelsOpen: false,
|
|
230
|
+
installedModelsCursor: 0,
|
|
231
|
+
installedModelsScrollOffset: 0,
|
|
232
|
+
installedModelsData: [],
|
|
233
|
+
installedModelsErrorMsg: null,
|
|
234
|
+
|
|
235
|
+
// 📖 Router Dashboard overlay (Shift+R)
|
|
236
|
+
routerDashboardOpen: false,
|
|
237
|
+
routerDashboardStatus: 'idle',
|
|
238
|
+
routerDashboardBaseUrl: null,
|
|
239
|
+
routerDashboardPort: null,
|
|
240
|
+
routerDashboardHealth: null,
|
|
241
|
+
routerDashboardStats: null,
|
|
242
|
+
routerDashboardError: null,
|
|
243
|
+
routerDashboardScrollOffset: 0,
|
|
244
|
+
routerDashboardEvents: [],
|
|
245
|
+
routerDashboardLiveRequests: [],
|
|
246
|
+
routerDashboardClearedAt: 0,
|
|
247
|
+
routerDashboardLastUpdatedAt: null,
|
|
248
|
+
routerDashboardLastRefreshStartedAt: null,
|
|
249
|
+
routerDashboardPollTimer: null,
|
|
250
|
+
routerDashboardEventAbort: null,
|
|
251
|
+
routerDashboardEventStatus: 'idle',
|
|
252
|
+
routerDashboardEventError: null,
|
|
253
|
+
routerDashboardNotice: null,
|
|
254
|
+
routerDashboardNoticeTimer: null,
|
|
255
|
+
routerOnboardingScrollOffset: 0,
|
|
256
|
+
routerDashboardEverOpened: false,
|
|
257
|
+
routerDashboardCursorIndex: 0,
|
|
258
|
+
|
|
259
|
+
// 📖 Custom text filter (Ctrl+P → type text → Enter). Ephemeral — not saved to config.
|
|
260
|
+
customTextFilter: null,
|
|
261
|
+
|
|
262
|
+
// 📖 Token usage overlay scroll state (used when overlay opens from footer)
|
|
263
|
+
tokenUsageOpen: false,
|
|
264
|
+
}
|
|
265
|
+
}
|
package/src/updater.js
CHANGED
|
@@ -17,12 +17,6 @@
|
|
|
17
17
|
* On success, relaunches the process with the same argv. On failure, prints manual
|
|
18
18
|
* instructions (using the correct PM command) and exits with code 1.
|
|
19
19
|
*
|
|
20
|
-
* - `promptUpdateNotification(latestVersion)` — renders a small centered interactive menu
|
|
21
|
-
* that lets the user choose: Update Now / Read Changelogs / Continue without update.
|
|
22
|
-
* Uses raw mode readline keypress events (same pattern as the main TUI).
|
|
23
|
-
* This function is called BEFORE the alt-screen is entered, so it writes to the
|
|
24
|
-
* normal terminal buffer.
|
|
25
|
-
*
|
|
26
20
|
* ⚙️ Notes:
|
|
27
21
|
* - `LOCAL_VERSION` is resolved from package.json via `createRequire` so this module
|
|
28
22
|
* can be imported independently from the bin entry point.
|
|
@@ -39,11 +33,9 @@
|
|
|
39
33
|
* → checkForUpdateDetailed() — Fetch npm latest with explicit error info
|
|
40
34
|
* → checkForUpdate() — Startup wrapper, returns version string or null
|
|
41
35
|
* → runUpdate(latestVersion) — Install new version via detected PM + relaunch
|
|
42
|
-
* → promptUpdateNotification(version) — Interactive pre-TUI update menu
|
|
43
|
-
*
|
|
44
36
|
* @exports
|
|
45
37
|
* detectPackageManager, getInstallArgs, getManualInstallCmd,
|
|
46
|
-
* checkForUpdateDetailed, checkForUpdate, runUpdate,
|
|
38
|
+
* checkForUpdateDetailed, checkForUpdate, runUpdate, fetchLastReleaseDate
|
|
47
39
|
*
|
|
48
40
|
* @see bin/free-coding-models.js — calls checkForUpdate() at startup and runUpdate() on confirm
|
|
49
41
|
*/
|
|
@@ -329,100 +321,4 @@ export function runUpdate(latestVersion) {
|
|
|
329
321
|
process.exit(1)
|
|
330
322
|
}
|
|
331
323
|
|
|
332
|
-
/**
|
|
333
|
-
* 📖 promptUpdateNotification: Show a centered interactive menu when a new version is available.
|
|
334
|
-
* 📖 Returns 'update', 'changelogs', or null (continue without update).
|
|
335
|
-
* 📖 Called BEFORE entering the alt-screen so it renders in the normal terminal buffer.
|
|
336
|
-
* @param {string|null} latestVersion
|
|
337
|
-
* @returns {Promise<'update'|'changelogs'|null>}
|
|
338
|
-
*/
|
|
339
|
-
export async function promptUpdateNotification(latestVersion) {
|
|
340
|
-
if (!latestVersion) return null
|
|
341
|
-
|
|
342
|
-
return new Promise((resolve) => {
|
|
343
|
-
let selected = 0
|
|
344
|
-
const options = [
|
|
345
|
-
{
|
|
346
|
-
label: 'Update now',
|
|
347
|
-
icon: '⬆',
|
|
348
|
-
description: `Update free-coding-models to v${latestVersion}`,
|
|
349
|
-
},
|
|
350
|
-
{
|
|
351
|
-
label: 'Read Changelogs',
|
|
352
|
-
icon: '📋',
|
|
353
|
-
description: 'Open GitHub changelog',
|
|
354
|
-
},
|
|
355
|
-
{
|
|
356
|
-
label: 'Continue without update',
|
|
357
|
-
icon: '▶',
|
|
358
|
-
description: '⚠ You will be reminded again in the TUI',
|
|
359
|
-
},
|
|
360
|
-
]
|
|
361
|
-
|
|
362
|
-
// 📖 Centered render function
|
|
363
|
-
const render = () => {
|
|
364
|
-
process.stdout.write('\x1b[2J\x1b[H') // clear screen + cursor home
|
|
365
|
-
|
|
366
|
-
// 📖 Calculate centering
|
|
367
|
-
const terminalWidth = process.stdout.columns || 80
|
|
368
|
-
const maxWidth = Math.min(terminalWidth - 4, 70)
|
|
369
|
-
const centerPad = ' '.repeat(Math.max(0, Math.floor((terminalWidth - maxWidth) / 2)))
|
|
370
|
-
|
|
371
|
-
console.log()
|
|
372
|
-
console.log(centerPad + chalk.bold.rgb(57, 255, 20)(' 🚀⬆️ UPDATE AVAILABLE'))
|
|
373
|
-
console.log(centerPad + chalk.rgb(57, 255, 20)(` Version ${latestVersion} is ready to install`))
|
|
374
|
-
console.log()
|
|
375
|
-
console.log(centerPad + chalk.bold(' ⚡ Free Coding Models') + chalk.dim(` v${LOCAL_VERSION}`))
|
|
376
|
-
console.log()
|
|
377
|
-
|
|
378
|
-
for (let i = 0; i < options.length; i++) {
|
|
379
|
-
const isSelected = i === selected
|
|
380
|
-
const bullet = isSelected ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
|
|
381
|
-
const label = isSelected
|
|
382
|
-
? chalk.bold.white(options[i].icon + ' ' + options[i].label)
|
|
383
|
-
: chalk.dim(options[i].icon + ' ' + options[i].label)
|
|
384
|
-
|
|
385
|
-
console.log(centerPad + bullet + label)
|
|
386
|
-
console.log(centerPad + chalk.dim(' ' + options[i].description))
|
|
387
|
-
console.log()
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
console.log(centerPad + chalk.dim(' ↑↓ Navigate • Enter Select • Ctrl+C Continue'))
|
|
391
|
-
console.log()
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
render()
|
|
395
|
-
|
|
396
|
-
readline.emitKeypressEvents(process.stdin)
|
|
397
|
-
// 📖 Ensure stdin is flowing — the shell-env prompt may have paused it
|
|
398
|
-
process.stdin.resume()
|
|
399
|
-
if (process.stdin.isTTY) process.stdin.setRawMode(true)
|
|
400
|
-
|
|
401
|
-
const onKey = (_str, key) => {
|
|
402
|
-
if (!key) return
|
|
403
|
-
if (key.ctrl && key.name === 'c') {
|
|
404
|
-
if (process.stdin.isTTY) process.stdin.setRawMode(false)
|
|
405
|
-
process.stdin.removeListener('keypress', onKey)
|
|
406
|
-
resolve(null) // Continue without update
|
|
407
|
-
return
|
|
408
|
-
}
|
|
409
|
-
if (key.name === 'up' && selected > 0) {
|
|
410
|
-
selected--
|
|
411
|
-
render()
|
|
412
|
-
} else if (key.name === 'down' && selected < options.length - 1) {
|
|
413
|
-
selected++
|
|
414
|
-
render()
|
|
415
|
-
} else if (key.name === 'return') {
|
|
416
|
-
if (process.stdin.isTTY) process.stdin.setRawMode(false)
|
|
417
|
-
process.stdin.removeListener('keypress', onKey)
|
|
418
|
-
process.stdin.pause()
|
|
419
324
|
|
|
420
|
-
if (selected === 0) resolve('update')
|
|
421
|
-
else if (selected === 1) resolve('changelogs')
|
|
422
|
-
else resolve(null) // Continue without update
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
process.stdin.on('keypress', onKey)
|
|
427
|
-
})
|
|
428
|
-
}
|