free-coding-models 0.3.18 → 0.3.21

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/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, Smart Recommend, Feedback, Changelog
7
+ * - Settings, Install Endpoints, Command Palette, Help, Smart Recommend, Feedback, 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
  *
@@ -56,6 +56,7 @@ export function createOverlayRenderers(state, deps) {
56
56
  getToolMeta,
57
57
  getToolInstallPlan,
58
58
  padEndDisplay,
59
+ displayWidth,
59
60
  } = deps
60
61
 
61
62
  const bullet = (isCursor) => (isCursor ? themeColors.accentBold(' ❯ ') : themeColors.dim(' '))
@@ -92,8 +93,7 @@ export function createOverlayRenderers(state, deps) {
92
93
  function renderSettings() {
93
94
  const providerKeys = Object.keys(sources)
94
95
  const updateRowIdx = providerKeys.length
95
- const widthWarningRowIdx = updateRowIdx + 1
96
- const themeRowIdx = widthWarningRowIdx + 1
96
+ const themeRowIdx = updateRowIdx + 1
97
97
  const cleanupLegacyProxyRowIdx = themeRowIdx + 1
98
98
  const changelogViewRowIdx = cleanupLegacyProxyRowIdx + 1
99
99
  const EL = '\x1b[K'
@@ -219,14 +219,6 @@ export function createOverlayRenderers(state, deps) {
219
219
  const updateRow = `${bullet(updateCursor)}${themeColors.textBold(updateActionLabel).padEnd(44)} ${updateStatus}`
220
220
  cursorLineByRow[updateRowIdx] = lines.length
221
221
  lines.push(updateCursor ? themeColors.bgCursor(updateRow) : updateRow)
222
- // 📖 Width warning visibility row for the startup narrow-terminal overlay.
223
- const disableWidthsWarning = Boolean(state.config.settings?.disableWidthsWarning)
224
- const widthWarningStatus = disableWidthsWarning
225
- ? themeColors.errorBold('🙈 Disabled')
226
- : themeColors.successBold('👁 Enabled')
227
- const widthWarningRow = `${bullet(state.settingsCursor === widthWarningRowIdx)}${themeColors.textBold('Small Width Warnings').padEnd(44)} ${widthWarningStatus}`
228
- cursorLineByRow[widthWarningRowIdx] = lines.length
229
- lines.push(state.settingsCursor === widthWarningRowIdx ? themeColors.bgCursor(widthWarningRow) : widthWarningRow)
230
222
  const themeStatus = getThemeStatusLabel(activeThemeSetting())
231
223
  const themeStatusColor = themeStatus.includes('Dark') ? themeColors.warningBold : themeColors.info
232
224
  const themeRow = `${bullet(state.settingsCursor === themeRowIdx)}${themeColors.textBold('Global Theme').padEnd(44)} ${themeStatusColor(themeStatus)}`
@@ -534,6 +526,128 @@ export function createOverlayRenderers(state, deps) {
534
526
  return cleared.join('\n')
535
527
  }
536
528
 
529
+ // ─── Command palette renderer ──────────────────────────────────────────────
530
+ // 📖 renderCommandPalette draws a centered floating modal over the live table.
531
+ // 📖 It returns cursor-positioned ANSI rows instead of replacing the full screen,
532
+ // 📖 so ping updates continue to animate in the background behind the palette.
533
+ function renderCommandPalette() {
534
+ const terminalRows = state.terminalRows || 24
535
+ const terminalCols = state.terminalCols || 80
536
+ const panelWidth = Math.max(44, Math.min(96, terminalCols - 8))
537
+ const panelInnerWidth = Math.max(28, panelWidth - 4)
538
+ const panelPad = 2
539
+ const panelOuterWidth = panelWidth + (panelPad * 2)
540
+ const footerRowCount = 2
541
+ const headerRowCount = 3
542
+ const bodyRows = Math.max(6, Math.min(16, terminalRows - 12))
543
+
544
+ const truncatePlain = (text, width) => {
545
+ if (width <= 1) return ''
546
+ if (displayWidth(text) <= width) return text
547
+ if (width <= 2) return text.slice(0, width)
548
+ return text.slice(0, width - 1) + '…'
549
+ }
550
+
551
+ const highlightMatch = (label, positions = []) => {
552
+ if (!Array.isArray(positions) || positions.length === 0) return label
553
+ const posSet = new Set(positions)
554
+ let out = ''
555
+ for (let i = 0; i < label.length; i++) {
556
+ out += posSet.has(i) ? themeColors.accentBold(label[i]) : label[i]
557
+ }
558
+ return out
559
+ }
560
+
561
+ const allResults = Array.isArray(state.commandPaletteResults) ? state.commandPaletteResults.slice(0, 80) : []
562
+ const groupedLines = []
563
+ const cursorLineByRow = {}
564
+ let category = null
565
+
566
+ if (allResults.length === 0) {
567
+ groupedLines.push(themeColors.dim(' No command found. Try a broader query.'))
568
+ } else {
569
+ for (let idx = 0; idx < allResults.length; idx++) {
570
+ const entry = allResults[idx]
571
+ if (entry.category !== category) {
572
+ category = entry.category
573
+ groupedLines.push(themeColors.textBold(` ${category}`))
574
+ }
575
+
576
+ const isCursor = idx === state.commandPaletteCursor
577
+ const pointer = isCursor ? themeColors.accentBold(' ❯ ') : themeColors.dim(' ')
578
+ const shortcutText = entry.shortcut ? themeColors.dim(entry.shortcut) : ''
579
+ const shortcutWidth = entry.shortcut ? Math.min(16, displayWidth(entry.shortcut)) : 0
580
+ const labelMax = Math.max(12, panelInnerWidth - 8 - shortcutWidth)
581
+ const plainLabel = truncatePlain(entry.label, labelMax)
582
+ const label = highlightMatch(plainLabel, entry.matchPositions)
583
+ const row = `${pointer}${padEndDisplay(label, labelMax)}${entry.shortcut ? ` ${shortcutText}` : ''}`
584
+ cursorLineByRow[idx] = groupedLines.length
585
+ groupedLines.push(isCursor ? themeColors.bgCursor(row) : row)
586
+ }
587
+ }
588
+
589
+ const targetLine = cursorLineByRow[state.commandPaletteCursor] ?? 0
590
+ state.commandPaletteScrollOffset = keepOverlayTargetVisible(
591
+ state.commandPaletteScrollOffset,
592
+ targetLine,
593
+ groupedLines.length,
594
+ bodyRows
595
+ )
596
+ const { visible, offset } = sliceOverlayLines(groupedLines, state.commandPaletteScrollOffset, bodyRows)
597
+ state.commandPaletteScrollOffset = offset
598
+
599
+ const query = state.commandPaletteQuery || ''
600
+ const queryWithCursor = query.length > 0
601
+ ? themeColors.textBold(`${query}▏`)
602
+ : themeColors.dim('type a command…') + themeColors.accentBold('▏')
603
+
604
+ const panelLines = []
605
+ const title = themeColors.textBold('Command Palette')
606
+ const titleLeft = ` ${title}`
607
+ const titleRight = themeColors.dim('Esc close')
608
+ const titleWidth = Math.max(1, panelInnerWidth - 1 - displayWidth('Esc close'))
609
+ panelLines.push(`${padEndDisplay(titleLeft, titleWidth)} ${titleRight}`)
610
+ panelLines.push(` ${padEndDisplay(`> ${queryWithCursor}`, panelInnerWidth)}`)
611
+ panelLines.push(themeColors.dim(` ${'-'.repeat(Math.max(1, panelInnerWidth))}`))
612
+
613
+ for (const line of visible) {
614
+ panelLines.push(` ${padEndDisplay(line, panelInnerWidth)}`)
615
+ }
616
+
617
+ // 📖 Keep panel body stable by filling with blank rows when result list is short.
618
+ while (panelLines.length < bodyRows + headerRowCount) {
619
+ panelLines.push(` ${' '.repeat(panelInnerWidth)}`)
620
+ }
621
+
622
+ panelLines.push(themeColors.dim(` ${'-'.repeat(Math.max(1, panelInnerWidth))}`))
623
+ panelLines.push(` ${padEndDisplay(themeColors.dim('↑↓ navigate • Enter run • Type to search'), panelInnerWidth)}`)
624
+ panelLines.push(` ${padEndDisplay(themeColors.dim('PgUp/PgDn • Home/End'), panelInnerWidth)}`)
625
+
626
+ const blankPaddedLine = ' '.repeat(panelOuterWidth)
627
+ const paddedPanelLines = [
628
+ blankPaddedLine,
629
+ blankPaddedLine,
630
+ ...panelLines.map((line) => `${' '.repeat(panelPad)}${padEndDisplay(line, panelWidth)}${' '.repeat(panelPad)}`),
631
+ blankPaddedLine,
632
+ blankPaddedLine,
633
+ ]
634
+
635
+ const panelHeight = paddedPanelLines.length
636
+ const top = Math.max(1, Math.floor((terminalRows - panelHeight) / 2) + 1)
637
+ const left = Math.max(1, Math.floor((terminalCols - panelOuterWidth) / 2) + 1)
638
+
639
+ const tintedLines = paddedPanelLines.map((line) => {
640
+ const padded = padEndDisplay(line, panelOuterWidth)
641
+ return themeColors.overlayBgCommandPalette(padded)
642
+ })
643
+
644
+ // 📖 Absolute cursor positioning overlays the palette on top of the existing table.
645
+ // 📖 The next frame starts with ALT_HOME, so this remains stable without manual cleanup.
646
+ return tintedLines
647
+ .map((line, idx) => `\x1b[${top + idx};${left}H${line}`)
648
+ .join('')
649
+ }
650
+
537
651
  // ─── Help overlay renderer ────────────────────────────────────────────────
538
652
  // 📖 renderHelp: Draw the help overlay listing all key bindings.
539
653
  // 📖 Toggled with K key. Gives users a quick reference without leaving the TUI.
@@ -602,6 +716,7 @@ export function createOverlayRenderers(state, deps) {
602
716
  lines.push('')
603
717
  lines.push(` ${heading('Controls')}`)
604
718
  lines.push(` ${key('W')} Toggle ping mode ${hint('(speed 2s → normal 10s → slow 30s → forced 4s)')}`)
719
+ lines.push(` ${key('Ctrl+P')} Open command palette ${hint('(search and run actions quickly)')}`)
605
720
  lines.push(` ${key('E')} Toggle configured models only ${hint('(enabled by default)')}`)
606
721
  lines.push(` ${key('Z')} Cycle tool mode ${hint('(OpenCode → Desktop → OpenClaw → Crush → Goose → Pi → Aider → Qwen → OpenHands → Amp)')}`)
607
722
  lines.push(` ${key('F')} Toggle favorite on selected row ${hint('(⭐ pinned at top, persisted)')}`)
@@ -1058,6 +1173,7 @@ export function createOverlayRenderers(state, deps) {
1058
1173
  renderSettings,
1059
1174
  renderInstallEndpoints,
1060
1175
  renderToolInstallPrompt,
1176
+ renderCommandPalette,
1061
1177
  renderHelp,
1062
1178
  renderRecommend,
1063
1179
  renderFeedback,
@@ -41,13 +41,13 @@ 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
53
  import { PROXY_DISABLED_NOTICE } from './product-flags.js'
@@ -67,7 +67,7 @@ export const PROVIDER_COLOR = new Proxy({}, {
67
67
  })
68
68
 
69
69
  // ─── 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) {
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) {
71
71
  // 📖 Filter out hidden models for display
72
72
  const visibleResults = results.filter(r => !r.hidden)
73
73
 
@@ -146,25 +146,68 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
146
146
 
147
147
  // 📖 Column widths (generous spacing with margins)
148
148
  const COL_SEP = getColumnSpacing()
149
+ const SEP_W = 3 // ' │ ' display width
150
+ const ROW_MARGIN = 2 // left margin ' '
149
151
  const W_RANK = 6
150
152
  const W_TIER = 6
151
153
  const W_CTX = 6
152
154
  const W_SOURCE = 14
153
155
  const W_MODEL = 26
154
- const W_SWE = 9
155
- const W_PING = 14
156
- const W_AVG = 11
156
+ const W_SWE = 6
157
157
  const W_STATUS = 18
158
158
  const W_VERDICT = 14
159
- const W_STAB = 11
160
159
  const W_UPTIME = 6
161
- const W_TOKENS = 7
160
+ // const W_TOKENS = 7 // Used column removed
162
161
  // const W_USAGE = 7 // Usage column removed
163
- const MIN_TABLE_WIDTH = 166
162
+ const MIN_TABLE_WIDTH = WIDTH_WARNING_MIN_COLS
163
+
164
+ // 📖 Responsive column visibility: progressively hide least-useful columns
165
+ // 📖 and shorten header labels when terminal width is insufficient.
166
+ // 📖 Hiding order (least useful first): Rank → Up% → Tier → Stability
167
+ // 📖 Compact mode shrinks: Latest Ping→Lat. P (10), Avg Ping→Avg. P (8),
168
+ // 📖 Stability→StaB. (8), Provider→4chars+… (10), Health→6chars+… (13)
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 = 10 // 'Lat. P' instead of 'Latest Ping'
196
+ wAvg = 8 // 'Avg. P' instead of 'Avg Ping'
197
+ wStab = 8 // 'StaB.' instead of 'Stability'
198
+ wSource = 10 // Provider truncated to 4 chars + '…'
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))
@@ -570,6 +639,7 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
570
639
  // 📖 Line 2: install flow, recommend, feedback, and extended hints.
571
640
  lines.push(
572
641
  themeColors.dim(` `) +
642
+ hotkey('Ctrl+P', ' Command palette') + themeColors.dim(` • `) +
573
643
  hotkey('Y', ' Install endpoints') + themeColors.dim(` • `) +
574
644
  hotkey('Q', ' Smart Recommend') + themeColors.dim(` • `) +
575
645
  hotkey('G', ' Theme') + themeColors.dim(` • `) +
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
@@ -52,6 +52,7 @@ const PALETTES = {
52
52
  recommend: [8, 21, 20],
53
53
  feedback: [31, 13, 20],
54
54
  changelog: [12, 24, 44],
55
+ commandPalette: [14, 20, 36],
55
56
  },
56
57
  cursor: {
57
58
  defaultBg: [39, 55, 90],
@@ -97,6 +98,7 @@ const PALETTES = {
97
98
  recommend: [246, 252, 248],
98
99
  feedback: [255, 247, 248],
99
100
  changelog: [244, 248, 255],
101
+ commandPalette: [242, 247, 255],
100
102
  },
101
103
  cursor: {
102
104
  defaultBg: [217, 231, 255],
@@ -312,4 +314,5 @@ export const themeColors = {
312
314
  overlayBgRecommend: (text) => paintBg(currentPalette().overlayBg.recommend, text, currentPalette().overlayFg),
313
315
  overlayBgFeedback: (text) => paintBg(currentPalette().overlayBg.feedback, text, currentPalette().overlayFg),
314
316
  overlayBgChangelog: (text) => paintBg(currentPalette().overlayBg.changelog, text, currentPalette().overlayFg),
317
+ overlayBgCommandPalette: (text) => paintBg(currentPalette().overlayBg.commandPalette, text, currentPalette().overlayFg),
315
318
  }
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,