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/CHANGELOG.md +27 -0
- package/README.md +3 -1
- package/package.json +1 -1
- package/src/app.js +96 -32
- package/src/cli-help.js +1 -2
- package/src/command-palette.js +170 -0
- package/src/config.js +0 -3
- package/src/constants.js +5 -0
- package/src/key-handler.js +318 -143
- package/src/overlays.js +127 -11
- package/src/render-table.js +120 -50
- package/src/testfcm.js +1 -1
- package/src/theme.js +3 -0
- package/src/utils.js +0 -2
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
|
|
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,
|
package/src/render-table.js
CHANGED
|
@@ -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
|
|
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 =
|
|
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 =
|
|
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 && !
|
|
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
|
-
|
|
221
|
-
const
|
|
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 + '
|
|
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(
|
|
244
|
-
: (originFilterMode > 0 ? themeColors.accentBold(originLabel.padEnd(
|
|
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
|
-
|
|
247
|
-
const
|
|
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(
|
|
254
|
-
const avgH_c = sortColumn === 'avg' ? themeColors.accentBold(avgH.padEnd(
|
|
255
|
-
const healthH_c = sortColumn === 'condition' ? themeColors.accentBold(healthH.padEnd(
|
|
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(
|
|
259
|
-
const plain =
|
|
260
|
-
const padding = ' '.repeat(Math.max(0,
|
|
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
|
|
272
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
552
|
+
stabCell = themeColors.dim('———'.padEnd(wStab))
|
|
483
553
|
} else if (stabScore >= 80) {
|
|
484
|
-
stabCell = themeColors.metricGood(String(stabScore).padEnd(
|
|
554
|
+
stabCell = themeColors.metricGood(String(stabScore).padEnd(wStab))
|
|
485
555
|
} else if (stabScore >= 60) {
|
|
486
|
-
stabCell = themeColors.metricOk(String(stabScore).padEnd(
|
|
556
|
+
stabCell = themeColors.metricOk(String(stabScore).padEnd(wStab))
|
|
487
557
|
} else if (stabScore >= 40) {
|
|
488
|
-
stabCell = themeColors.metricWarn(String(stabScore).padEnd(
|
|
558
|
+
stabCell = themeColors.metricWarn(String(stabScore).padEnd(wStab))
|
|
489
559
|
} else {
|
|
490
|
-
stabCell = themeColors.metricBad(String(stabScore).padEnd(
|
|
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 =
|
|
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
|
-
// 📖
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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
|
|
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,
|