free-coding-models 0.3.19 → 0.3.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -41,16 +41,15 @@ import {
41
41
  msCell,
42
42
  spinCell,
43
43
  PING_INTERVAL,
44
+ WIDTH_WARNING_MIN_COLS,
44
45
  FRAMES
45
46
  } from './constants.js'
46
47
  import { themeColors, getProviderRgb, getTierRgb, getReadableTextRgb, getTheme } from './theme.js'
47
48
  import { TIER_COLOR } from './tier-colors.js'
48
49
  import { getAvg, getVerdict, getUptime, getStabilityScore, getVersionStatusInfo } from './utils.js'
49
50
  import { usagePlaceholderForProvider } from './ping.js'
50
- import { formatTokenTotalCompact } from './token-usage-reader.js'
51
51
  import { calculateViewport, sortResultsWithPinnedFavorites, padEndDisplay, displayWidth } from './render-helpers.js'
52
52
  import { getToolMeta } from './tool-metadata.js'
53
- import { PROXY_DISABLED_NOTICE } from './product-flags.js'
54
53
  import { getColumnSpacing } from './ui-config.js'
55
54
 
56
55
  const require = createRequire(import.meta.url)
@@ -67,7 +66,7 @@ export const PROVIDER_COLOR = new Proxy({}, {
67
66
  })
68
67
 
69
68
  // ─── renderTable: mode param controls footer hint text (opencode vs openclaw) ─────────
70
- 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, disableWidthsWarning = false) {
69
+ 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) {
71
70
  // 📖 Filter out hidden models for display
72
71
  const visibleResults = results.filter(r => !r.hidden)
73
72
 
@@ -146,25 +145,69 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
146
145
 
147
146
  // 📖 Column widths (generous spacing with margins)
148
147
  const COL_SEP = getColumnSpacing()
148
+ const SEP_W = 3 // ' │ ' display width
149
+ const ROW_MARGIN = 2 // left margin ' '
149
150
  const W_RANK = 6
150
- const W_TIER = 6
151
- const W_CTX = 6
151
+ const W_TIER = 5
152
+ const W_CTX = 4
152
153
  const W_SOURCE = 14
153
154
  const W_MODEL = 26
154
- const W_SWE = 9
155
- const W_PING = 14
156
- const W_AVG = 11
155
+ const W_SWE = 5
157
156
  const W_STATUS = 18
158
157
  const W_VERDICT = 14
159
- const W_STAB = 11
160
158
  const W_UPTIME = 6
161
- const W_TOKENS = 7
159
+ // const W_TOKENS = 7 // Used column removed
162
160
  // const W_USAGE = 7 // Usage column removed
163
- const MIN_TABLE_WIDTH = 166
161
+ const MIN_TABLE_WIDTH = WIDTH_WARNING_MIN_COLS
162
+
163
+ // 📖 Responsive column visibility: progressively hide least-useful columns
164
+ // 📖 and shorten header labels when terminal width is insufficient.
165
+ // 📖 Hiding order (least useful first): Rank → Up% → Tier → Stability
166
+ // 📖 Compact mode shrinks: Latest Ping→Lat. P (9), Avg Ping→Avg. P (8),
167
+ // 📖 Stability→StaB. (8), Provider→4chars+… (7), Health→6chars+… (13)
168
+ // 📖 Breakpoints: full=169 | compact=146 | -Rank=137 | -Up%=128 | -Tier=120 | -Stab=109
169
+ let wPing = 14
170
+ let wAvg = 11
171
+ let wStab = 11
172
+ let wSource = W_SOURCE
173
+ let wStatus = W_STATUS
174
+ let showRank = true
175
+ let showUptime = true
176
+ let showTier = true
177
+ let showStability = true
178
+ let isCompact = false
179
+
180
+ if (terminalCols > 0) {
181
+ // 📖 Dynamically compute needed row width from visible columns
182
+ const calcWidth = () => {
183
+ const cols = []
184
+ if (showRank) cols.push(W_RANK)
185
+ if (showTier) cols.push(W_TIER)
186
+ cols.push(W_SWE, W_CTX, W_MODEL, wSource, wPing, wAvg, wStatus, W_VERDICT)
187
+ if (showStability) cols.push(wStab)
188
+ if (showUptime) cols.push(W_UPTIME)
189
+ return ROW_MARGIN + cols.reduce((a, b) => a + b, 0) + (cols.length - 1) * SEP_W
190
+ }
191
+
192
+ // 📖 Step 1: Compact mode — shorten labels and reduce column widths
193
+ if (calcWidth() > terminalCols) {
194
+ isCompact = true
195
+ wPing = 9 // 'Lat. P' instead of 'Latest Ping'
196
+ wAvg = 8 // 'Avg. P' instead of 'Avg Ping'
197
+ wStab = 8 // 'StaB.' instead of 'Stability'
198
+ wSource = 7 // Provider truncated to 4 chars + '…', 7 cols total
199
+ wStatus = 13 // Health truncated after 6 chars + '…'
200
+ }
201
+ // 📖 Steps 2–5: Progressive column hiding (least useful first)
202
+ if (calcWidth() > terminalCols) showRank = false
203
+ if (calcWidth() > terminalCols) showUptime = false
204
+ if (calcWidth() > terminalCols) showTier = false
205
+ if (calcWidth() > terminalCols) showStability = false
206
+ }
164
207
  const warningDurationMs = 2_000
165
208
  const elapsed = widthWarningStartedAt ? Math.max(0, Date.now() - widthWarningStartedAt) : warningDurationMs
166
209
  const remainingMs = Math.max(0, warningDurationMs - elapsed)
167
- const showWidthWarning = terminalCols > 0 && terminalCols < MIN_TABLE_WIDTH && !disableWidthsWarning && !widthWarningDismissed && widthWarningShowCount < 2 && remainingMs > 0
210
+ const showWidthWarning = terminalCols > 0 && terminalCols < MIN_TABLE_WIDTH && !widthWarningDismissed && widthWarningShowCount < 2 && remainingMs > 0
168
211
 
169
212
  if (showWidthWarning) {
170
213
  const lines = []
@@ -217,13 +260,16 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
217
260
  const modelH = 'Model'
218
261
  const sweH = sortColumn === 'swe' ? dir + ' SWE%' : 'SWE%'
219
262
  const ctxH = sortColumn === 'ctx' ? dir + ' CTX' : 'CTX'
220
- const pingH = sortColumn === 'ping' ? dir + ' Latest Ping' : 'Latest Ping'
221
- const avgH = sortColumn === 'avg' ? dir + ' Avg Ping' : 'Avg Ping'
263
+ // 📖 Compact labels: 'Lat. P' / 'Avg. P' / 'StaB.' to save horizontal space
264
+ const pingLabel = isCompact ? 'Lat. P' : 'Latest Ping'
265
+ const avgLabel = isCompact ? 'Avg. P' : 'Avg Ping'
266
+ const stabLabel = isCompact ? 'StaB.' : 'Stability'
267
+ const pingH = sortColumn === 'ping' ? dir + ' ' + pingLabel : pingLabel
268
+ const avgH = sortColumn === 'avg' ? dir + ' ' + avgLabel : avgLabel
222
269
  const healthH = sortColumn === 'condition' ? dir + ' Health' : 'Health'
223
270
  const verdictH = sortColumn === 'verdict' ? dir + ' Verdict' : 'Verdict'
224
- const stabH = sortColumn === 'stability' ? dir + ' Stability' : 'Stability'
271
+ const stabH = sortColumn === 'stability' ? dir + ' ' + stabLabel : stabLabel
225
272
  const uptimeH = sortColumn === 'uptime' ? dir + ' Up%' : 'Up%'
226
- const tokensH = 'Used'
227
273
 
228
274
  // 📖 Helper to colorize first letter for keyboard shortcuts
229
275
  // 📖 IMPORTANT: Pad PLAIN TEXT first, then apply colors to avoid alignment issues
@@ -238,27 +284,31 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
238
284
  // 📖 Now colorize after padding is calculated on plain text
239
285
  const rankH_c = colorFirst(rankH, W_RANK)
240
286
  const tierH_c = colorFirst('Tier', W_TIER)
241
- const originLabel = 'Provider'
287
+ const originLabel = isCompact ? 'PrOD…' : 'Provider'
242
288
  const originH_c = sortColumn === 'origin'
243
- ? themeColors.accentBold(originLabel.padEnd(W_SOURCE))
244
- : (originFilterMode > 0 ? themeColors.accentBold(originLabel.padEnd(W_SOURCE)) : (() => {
289
+ ? themeColors.accentBold(originLabel.padEnd(wSource))
290
+ : (originFilterMode > 0 ? themeColors.accentBold(originLabel.padEnd(wSource)) : (() => {
245
291
  // 📖 Provider keeps O for sorting and D for provider-filter cycling.
246
- const plain = 'PrOviDer'
247
- const padding = ' '.repeat(Math.max(0, W_SOURCE - plain.length))
292
+ // 📖 In compact mode, shorten to 'PrOD…' (4 chars + ellipsis) to save space.
293
+ const plain = isCompact ? 'PrOD…' : 'PrOviDer'
294
+ const padding = ' '.repeat(Math.max(0, wSource - plain.length))
295
+ if (isCompact) {
296
+ return themeColors.dim('Pr') + themeColors.hotkey('O') + themeColors.hotkey('D') + themeColors.dim('…' + padding)
297
+ }
248
298
  return themeColors.dim('Pr') + themeColors.hotkey('O') + themeColors.dim('vi') + themeColors.hotkey('D') + themeColors.dim('er' + padding)
249
299
  })())
250
300
  const modelH_c = colorFirst(modelH, W_MODEL)
251
301
  const sweH_c = sortColumn === 'swe' ? themeColors.accentBold(sweH.padEnd(W_SWE)) : colorFirst(sweH, W_SWE)
252
302
  const ctxH_c = sortColumn === 'ctx' ? themeColors.accentBold(ctxH.padEnd(W_CTX)) : colorFirst(ctxH, W_CTX)
253
- const pingH_c = sortColumn === 'ping' ? themeColors.accentBold(pingH.padEnd(W_PING)) : colorFirst('Latest Ping', W_PING)
254
- const avgH_c = sortColumn === 'avg' ? themeColors.accentBold(avgH.padEnd(W_AVG)) : colorFirst('Avg Ping', W_AVG)
255
- const healthH_c = sortColumn === 'condition' ? themeColors.accentBold(healthH.padEnd(W_STATUS)) : colorFirst('Health', W_STATUS)
303
+ const pingH_c = sortColumn === 'ping' ? themeColors.accentBold(pingH.padEnd(wPing)) : colorFirst(pingLabel, wPing)
304
+ const avgH_c = sortColumn === 'avg' ? themeColors.accentBold(avgH.padEnd(wAvg)) : colorFirst(avgLabel, wAvg)
305
+ const healthH_c = sortColumn === 'condition' ? themeColors.accentBold(healthH.padEnd(wStatus)) : colorFirst('Health', wStatus)
256
306
  const verdictH_c = sortColumn === 'verdict' ? themeColors.accentBold(verdictH.padEnd(W_VERDICT)) : colorFirst(verdictH, W_VERDICT)
257
307
  // 📖 Custom colorization for Stability: highlight 'B' (the sort key) since 'S' is taken by SWE
258
- const stabH_c = sortColumn === 'stability' ? themeColors.accentBold(stabH.padEnd(W_STAB)) : (() => {
259
- const plain = 'Stability'
260
- const padding = ' '.repeat(Math.max(0, W_STAB - plain.length))
261
- return themeColors.dim('Sta') + themeColors.hotkey('B') + themeColors.dim('ility' + padding)
308
+ const stabH_c = sortColumn === 'stability' ? themeColors.accentBold(stabH.padEnd(wStab)) : (() => {
309
+ const plain = stabLabel
310
+ const padding = ' '.repeat(Math.max(0, wStab - plain.length))
311
+ return themeColors.dim('Sta') + themeColors.hotkey('B') + themeColors.dim((isCompact ? '.' : 'ility') + padding)
262
312
  })()
263
313
  // 📖 Up% sorts on U, so keep the highlighted shortcut in the shared yellow sort-key color.
264
314
  const uptimeH_c = sortColumn === 'uptime' ? themeColors.accentBold(uptimeH.padEnd(W_UPTIME)) : (() => {
@@ -266,10 +316,15 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
266
316
  const padding = ' '.repeat(Math.max(0, W_UPTIME - plain.length))
267
317
  return themeColors.hotkey('U') + themeColors.dim('p%' + padding)
268
318
  })()
269
- const tokensH_c = themeColors.dim(tokensH.padEnd(W_TOKENS))
270
319
  // 📖 Usage column removed from UI – no header or separator for it.
271
- // Header without Usage column (column order: Rank, Tier, SWE%, CTX, Model, Provider, Latest Ping, Avg Ping, Health, Verdict, Stability, Up%, Used)
272
- lines.push(' ' + rankH_c + COL_SEP + tierH_c + COL_SEP + sweH_c + COL_SEP + ctxH_c + COL_SEP + modelH_c + COL_SEP + originH_c + COL_SEP + pingH_c + COL_SEP + avgH_c + COL_SEP + healthH_c + COL_SEP + verdictH_c + COL_SEP + stabH_c + COL_SEP + uptimeH_c + COL_SEP + tokensH_c)
320
+ // 📖 Header row: conditionally include columns based on responsive visibility
321
+ const headerParts = []
322
+ if (showRank) headerParts.push(rankH_c)
323
+ if (showTier) headerParts.push(tierH_c)
324
+ headerParts.push(sweH_c, ctxH_c, modelH_c, originH_c, pingH_c, avgH_c, healthH_c, verdictH_c)
325
+ if (showStability) headerParts.push(stabH_c)
326
+ if (showUptime) headerParts.push(uptimeH_c)
327
+ lines.push(' ' + headerParts.join(COL_SEP))
273
328
 
274
329
 
275
330
 
@@ -311,9 +366,13 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
311
366
  const num = themeColors.dim(String(r.idx).padEnd(W_RANK))
312
367
  const tier = tierFn(r.tier.padEnd(W_TIER))
313
368
  // 📖 Keep terminal view provider-specific so each row is monitorable per provider
369
+ // 📖 In compact mode, truncate provider name to 4 chars + '…'
314
370
  const providerNameRaw = sources[r.providerKey]?.name ?? r.providerKey ?? 'NIM'
315
371
  const providerName = normalizeOriginLabel(providerNameRaw, r.providerKey)
316
- const source = themeColors.provider(r.providerKey, providerName.padEnd(W_SOURCE))
372
+ const providerDisplay = isCompact && providerName.length > 5
373
+ ? providerName.slice(0, 4) + '…'
374
+ : providerName
375
+ const source = themeColors.provider(r.providerKey, providerDisplay.padEnd(wSource))
317
376
  // 📖 Favorites: always reserve 2 display columns at the start of Model column.
318
377
  // 📖 🎯 (2 cols) for recommended, ⭐ (2 cols) for favorites, ' ' (2 spaces) for non-favorites — keeps alignment stable.
319
378
  const favoritePrefix = r.isRecommended ? '🎯' : r.isFavorite ? '⭐' : ' '
@@ -345,7 +404,7 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
345
404
  // 📖 Keep the row-local spinner small and inline so users can still read the last measured latency.
346
405
  const buildLatestPingDisplay = (value) => {
347
406
  const spinner = r.isPinging ? ` ${FRAMES[frame % FRAMES.length]}` : ''
348
- return `${value}${spinner}`.padEnd(W_PING)
407
+ return `${value}${spinner}`.padEnd(wPing)
349
408
  }
350
409
 
351
410
  // 📖 Latest ping - pings are objects: { ms, code }
@@ -353,7 +412,7 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
353
412
  const latestPing = r.pings.length > 0 ? r.pings[r.pings.length - 1] : null
354
413
  let pingCell
355
414
  if (!latestPing) {
356
- const placeholder = r.isPinging ? buildLatestPingDisplay('———') : '———'.padEnd(W_PING)
415
+ const placeholder = r.isPinging ? buildLatestPingDisplay('———') : '———'.padEnd(wPing)
357
416
  pingCell = themeColors.dim(placeholder)
358
417
  } else if (latestPing.code === '200') {
359
418
  // 📖 Success - show response time
@@ -364,7 +423,7 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
364
423
  pingCell = themeColors.dim(buildLatestPingDisplay(String(latestPing.ms)))
365
424
  } else {
366
425
  // 📖 Error or timeout - show "———" (error code is already in Status column)
367
- const placeholder = r.isPinging ? buildLatestPingDisplay('———') : '———'.padEnd(W_PING)
426
+ const placeholder = r.isPinging ? buildLatestPingDisplay('———') : '———'.padEnd(wPing)
368
427
  pingCell = themeColors.dim(placeholder)
369
428
  }
370
429
 
@@ -372,10 +431,10 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
372
431
  const avg = getAvg(r)
373
432
  let avgCell
374
433
  if (avg !== Infinity) {
375
- const str = String(avg).padEnd(W_AVG)
434
+ const str = String(avg).padEnd(wAvg)
376
435
  avgCell = avg < 500 ? themeColors.metricGood(str) : avg < 1500 ? themeColors.metricWarn(str) : themeColors.metricBad(str)
377
436
  } else {
378
- avgCell = themeColors.dim('———'.padEnd(W_AVG))
437
+ avgCell = themeColors.dim('———'.padEnd(wAvg))
379
438
  }
380
439
 
381
440
  // 📖 Status column - build plain text with emoji, pad, then colorize
@@ -423,7 +482,18 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
423
482
  statusText = '?'
424
483
  statusColor = themeColors.dim
425
484
  }
426
- const status = statusColor(padEndDisplay(statusText, W_STATUS))
485
+ // 📖 In compact mode, truncate health text after 6 visible chars + '…' to fit wStatus
486
+ const statusDisplayText = isCompact ? (() => {
487
+ // 📖 Strip emoji prefix to measure text length, then truncate if needed
488
+ const plainText = statusText.replace(/^[\p{Emoji_Presentation}\p{Extended_Pictographic}]\s*/u, '')
489
+ if (plainText.length > 6) {
490
+ const emojiMatch = statusText.match(/^([\p{Emoji_Presentation}\p{Extended_Pictographic}]\s*)/u)
491
+ const prefix = emojiMatch ? emojiMatch[1] : ''
492
+ return prefix + plainText.slice(0, 6) + '…'
493
+ }
494
+ return statusText
495
+ })() : statusText
496
+ const status = statusColor(padEndDisplay(statusDisplayText, wStatus))
427
497
 
428
498
  // 📖 Verdict column - use getVerdict() for stability-aware verdicts, then render with emoji
429
499
  const verdict = getVerdict(r)
@@ -479,15 +549,15 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
479
549
  const stabScore = getStabilityScore(r)
480
550
  let stabCell
481
551
  if (stabScore < 0) {
482
- stabCell = themeColors.dim('———'.padEnd(W_STAB))
552
+ stabCell = themeColors.dim('———'.padEnd(wStab))
483
553
  } else if (stabScore >= 80) {
484
- stabCell = themeColors.metricGood(String(stabScore).padEnd(W_STAB))
554
+ stabCell = themeColors.metricGood(String(stabScore).padEnd(wStab))
485
555
  } else if (stabScore >= 60) {
486
- stabCell = themeColors.metricOk(String(stabScore).padEnd(W_STAB))
556
+ stabCell = themeColors.metricOk(String(stabScore).padEnd(wStab))
487
557
  } else if (stabScore >= 40) {
488
- stabCell = themeColors.metricWarn(String(stabScore).padEnd(W_STAB))
558
+ stabCell = themeColors.metricWarn(String(stabScore).padEnd(wStab))
489
559
  } else {
490
- stabCell = themeColors.metricBad(String(stabScore).padEnd(W_STAB))
560
+ stabCell = themeColors.metricBad(String(stabScore).padEnd(wStab))
491
561
  }
492
562
 
493
563
  // 📖 Uptime column - percentage of successful pings
@@ -508,22 +578,21 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
508
578
  // 📖 Model text now mirrors the provider hue so provider affinity is visible
509
579
  // 📖 even before the eye reaches the Provider column.
510
580
  const nameCell = themeColors.provider(r.providerKey, name, { bold: isCursor })
511
- const sourceCursorText = providerName.padEnd(W_SOURCE)
581
+ const sourceCursorText = providerDisplay.padEnd(wSource)
512
582
  const sourceCell = isCursor ? themeColors.provider(r.providerKey, sourceCursorText, { bold: true }) : source
513
583
 
514
584
  // 📖 Usage column removed from UI – no usage data displayed.
515
585
  // (We keep the logic but do not render it.)
516
586
  const usageCell = ''
517
587
 
518
- // 📖 Used column total historical prompt+completion tokens consumed for this
519
- // 📖 exact provider/model pair, loaded from the local usage snapshot file at startup.
520
- const tokenTotal = Number(r.totalTokens) || 0
521
- const tokensCell = tokenTotal > 0
522
- ? themeColors.metricOk(formatTokenTotalCompact(tokenTotal).padEnd(W_TOKENS))
523
- : themeColors.dim('0'.padEnd(W_TOKENS))
524
-
525
- // 📖 Build row with double space between columns (order: Rank, Tier, SWE%, CTX, Model, Provider, Latest Ping, Avg Ping, Health, Verdict, Stability, Up%, Used)
526
- const row = ' ' + num + COL_SEP + tier + COL_SEP + sweCell + COL_SEP + ctxCell + COL_SEP + nameCell + COL_SEP + sourceCell + COL_SEP + pingCell + COL_SEP + avgCell + COL_SEP + status + COL_SEP + speedCell + COL_SEP + stabCell + COL_SEP + uptimeCell + COL_SEP + tokensCell
588
+ // 📖 Build row: conditionally include columns based on responsive visibility
589
+ const rowParts = []
590
+ if (showRank) rowParts.push(num)
591
+ if (showTier) rowParts.push(tier)
592
+ rowParts.push(sweCell, ctxCell, nameCell, sourceCell, pingCell, avgCell, status, speedCell)
593
+ if (showStability) rowParts.push(stabCell)
594
+ if (showUptime) rowParts.push(uptimeCell)
595
+ const row = ' ' + rowParts.join(COL_SEP)
527
596
 
528
597
  if (isCursor) {
529
598
  lines.push(themeColors.bgModelCursor(row))
@@ -551,7 +620,7 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
551
620
  const activeHotkey = (keyLabel, text, bg) => themeColors.badge(`${keyLabel}${text}`, bg, getReadableTextRgb(bg))
552
621
  // 📖 Line 1: core navigation + filtering shortcuts
553
622
  lines.push(
554
- hotkey('F', ' Toggle Favorite') +
623
+ ' ' + hotkey('F', ' Toggle Favorite') +
555
624
  themeColors.dim(` • `) +
556
625
  (tierFilterMode > 0
557
626
  ? activeHotkey('T', ` Tier (${activeTierLabel})`, getTierRgb(activeTierLabel))
@@ -567,11 +636,11 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
567
636
  themeColors.dim(` • `) +
568
637
  hotkey('K', ' Help')
569
638
  )
570
- // 📖 Line 2: install flow, recommend, feedback, and extended hints.
639
+ // 📖 Line 2: command palette (highlighted as new), recommend, feedback, and extended hints.
640
+ // 📖 CTRL+P ⚡️ Command Palette uses neon-green-on-dark-green background to highlight the feature.
641
+ const paletteLabel = chalk.bgRgb(0, 60, 0).rgb(57, 255, 20).bold(' NEW ! CTRL+P ⚡️ Command Palette ')
571
642
  lines.push(
572
- themeColors.dim(` `) +
573
- hotkey('Ctrl+P', ' Command palette') + themeColors.dim(` • `) +
574
- hotkey('Y', ' Install endpoints') + themeColors.dim(` • `) +
643
+ ' ' + paletteLabel + themeColors.dim(` `) +
575
644
  hotkey('Q', ' Smart Recommend') + themeColors.dim(` • `) +
576
645
  hotkey('G', ' Theme') + themeColors.dim(` • `) +
577
646
  hotkey('I', ' Feedback, bugs & requests')
@@ -592,11 +661,7 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
592
661
  '💬 ' +
593
662
  themeColors.footerDiscord('\x1b]8;;https://discord.gg/ZTNFHvvCkU\x1b\\Discord\x1b]8;;\x1b\\') +
594
663
  themeColors.dim(' → ') +
595
- themeColors.footerDiscord('https://discord.gg/ZTNFHvvCkU') +
596
- themeColors.dim(' • ') +
597
- themeColors.hotkey('N') + themeColors.dim(' Changelog') +
598
- themeColors.dim(' • ') +
599
- themeColors.dim('Ctrl+C Exit')
664
+ themeColors.footerDiscord('https://discord.gg/ZTNFHvvCkU')
600
665
  lines.push(footerLine)
601
666
 
602
667
  if (versionStatus.isOutdated) {
@@ -608,10 +673,12 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
608
673
  lines.push(chalk.bgRed.white.bold(paddedBanner))
609
674
  }
610
675
 
611
- // 📖 Stable release notice: keep the bridge rebuild status explicit in the main UI
612
- // 📖 so users do not go hunting for hidden controls that are disabled on purpose.
613
- const bridgeNotice = chalk.italic.rgb(...getTierRgb('A-'))(` ${PROXY_DISABLED_NOTICE}`)
614
- lines.push(bridgeNotice)
676
+ // 📖 Final footer line: changelog shortcut + exit hint (replaces the old proxy notice).
677
+ lines.push(
678
+ ' ' + themeColors.hotkey('N') + themeColors.dim(' Changelog') +
679
+ themeColors.dim(' • ') +
680
+ themeColors.dim('Ctrl+C Exit')
681
+ )
615
682
 
616
683
  // 📖 Append \x1b[K (erase to EOL) to each line so leftover chars from previous
617
684
  // 📖 frames are cleared. Then pad with blank cleared lines to fill the terminal,
package/src/testfcm.js CHANGED
@@ -107,7 +107,7 @@ const TRANSCRIPT_FINDING_RULES = [
107
107
  title: 'PTY width warning blocked the TUI flow',
108
108
  severity: 'high',
109
109
  regex: /please maximize your terminal|terminal is too small|reduce font size or maximize width/i,
110
- task: 'Run `/testfcm` with the width warning disabled in the isolated config or force a wider PTY before sending Enter.',
110
+ task: 'Run `/testfcm` with a wider PTY (at least 80 columns) before sending Enter.',
111
111
  },
112
112
  {
113
113
  id: 'tool_missing',
package/src/theme.js CHANGED
@@ -278,11 +278,13 @@ function paintBg(bgRgb, text, fgRgb = null, options = {}) {
278
278
  export const themeColors = {
279
279
  text: (text) => paintRgb(currentPalette().text, text),
280
280
  textBold: (text) => paintRgb(currentPalette().textStrong, text, { bold: true }),
281
+ headerBold: (text) => paintRgb([142, 200, 255], text, { bold: true }),
281
282
  dim: (text) => paintRgb(currentPalette().muted, text),
282
283
  soft: (text) => paintRgb(currentPalette().soft, text),
283
284
  accent: (text) => paintRgb(currentPalette().accent, text),
284
285
  accentBold: (text) => paintRgb(currentPalette().accentStrong, text, { bold: true }),
285
286
  info: (text) => paintRgb(currentPalette().info, text),
287
+ infoBold: (text) => paintRgb([100, 180, 255], text, { bold: true }),
286
288
  success: (text) => paintRgb(currentPalette().success, text),
287
289
  successBold: (text) => paintRgb(currentPalette().successStrong, text, { bold: true }),
288
290
  warning: (text) => paintRgb(currentPalette().warning, text),
package/src/utils.js CHANGED
@@ -464,7 +464,6 @@ export function parseArgs(argv) {
464
464
  const sortAscFlag = flags.includes('--asc')
465
465
  const hideUnconfigured = flags.includes('--hide-unconfigured')
466
466
  const showUnconfigured = flags.includes('--show-unconfigured')
467
- const disableWidthsWarning = flags.includes('--disable-widths-warning')
468
467
 
469
468
  let tierFilter = tierValueIdx !== -1 ? args[tierValueIdx].toUpperCase() : null
470
469
  let sortColumn = sortValueIdx !== -1 ? args[sortValueIdx].toLowerCase() : null
@@ -501,7 +500,6 @@ export function parseArgs(argv) {
501
500
  pingInterval,
502
501
  hideUnconfigured,
503
502
  showUnconfigured,
504
- disableWidthsWarning,
505
503
  premiumMode,
506
504
  // 📖 Profile system removed - API keys now persist permanently across all sessions
507
505
  recommendMode,