free-coding-models 0.3.69 → 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.
@@ -103,11 +103,87 @@ export const PROVIDER_COLOR = new Proxy({}, {
103
103
  },
104
104
  })
105
105
 
106
- // ─── renderTable: mode param controls footer hint text (opencode vs openclaw) ─────────
107
- 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, legacyStatus = null, pingMode = 'normal', pingModeSource = 'auto', hideUnconfiguredModels = false, widthWarningStartedAt = null, widthWarningDismissed = false, widthWarningShowCount = 0, settingsUpdateState = 'idle', settingsUpdateLatestVersion = null, legacyFlag = false, startupLatestVersion = null, versionAlertsEnabled = true, favoritesPinnedAndSticky = false, customTextFilter = null, lastReleaseDate = null, legacyFooterHidden = false, verdictFilterMode = 0, healthFilterMode = 0, bestModeOnly = false, routerFooterRunning = false, routerFooterActiveSet = null, routerFooterTodayTokens = 0, routerFooterAllTimeTokens = 0, routerFooterRequests = 0) {
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
+ }
@@ -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, promptUpdateNotification
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
- }