free-coding-models 0.3.24 → 0.3.26
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 +51 -0
- package/README.md +20 -2
- package/package.json +1 -1
- package/sources.js +37 -19
- package/src/app.js +84 -2
- package/src/command-palette.js +1 -0
- package/src/constants.js +5 -2
- package/src/installed-models-manager.js +636 -0
- package/src/key-handler.js +548 -0
- package/src/mouse.js +186 -0
- package/src/overlays.js +170 -1
- package/src/render-table.js +169 -42
package/src/render-table.js
CHANGED
|
@@ -50,12 +50,47 @@ import { TIER_COLOR } from './tier-colors.js'
|
|
|
50
50
|
import { getAvg, getVerdict, getUptime, getStabilityScore, getVersionStatusInfo } from './utils.js'
|
|
51
51
|
import { usagePlaceholderForProvider } from './ping.js'
|
|
52
52
|
import { calculateViewport, sortResultsWithPinnedFavorites, padEndDisplay, displayWidth } from './render-helpers.js'
|
|
53
|
-
import { getToolMeta, TOOL_METADATA, TOOL_MODE_ORDER,
|
|
53
|
+
import { getToolMeta, TOOL_METADATA, TOOL_MODE_ORDER, isModelCompatibleWithTool } from './tool-metadata.js'
|
|
54
54
|
import { getColumnSpacing } from './ui-config.js'
|
|
55
55
|
|
|
56
56
|
const require = createRequire(import.meta.url)
|
|
57
57
|
const { version: LOCAL_VERSION } = require('../package.json')
|
|
58
58
|
|
|
59
|
+
// 📖 Mouse support: column boundary map updated every frame by renderTable().
|
|
60
|
+
// 📖 Each entry maps a column name to its display X-start and X-end (1-based, inclusive).
|
|
61
|
+
// 📖 headerRow is the 1-based terminal row of the column header line.
|
|
62
|
+
// 📖 firstModelRow/lastModelRow are the 1-based terminal rows of the first/last visible model row.
|
|
63
|
+
// 📖 Exported so the mouse handler can translate click coordinates into column/row targets.
|
|
64
|
+
let _lastLayout = {
|
|
65
|
+
columns: [], // 📖 Array of { name, xStart, xEnd } in display order
|
|
66
|
+
headerRow: 0, // 📖 1-based terminal row of the column headers
|
|
67
|
+
firstModelRow: 0, // 📖 1-based terminal row of the first visible model
|
|
68
|
+
lastModelRow: 0, // 📖 1-based terminal row of the last visible model
|
|
69
|
+
viewportStartIdx: 0, // 📖 index into sorted[] of the first visible model
|
|
70
|
+
viewportEndIdx: 0, // 📖 index into sorted[] past the last visible model
|
|
71
|
+
hasAboveIndicator: false, // 📖 whether "... N more above ..." is shown
|
|
72
|
+
hasBelowIndicator: false, // 📖 whether "... N more below ..." is shown
|
|
73
|
+
footerHotkeys: [], // 📖 Array of { key, row, xStart, xEnd } for footer click zones
|
|
74
|
+
}
|
|
75
|
+
export function getLastLayout() { return _lastLayout }
|
|
76
|
+
|
|
77
|
+
// 📖 Column name → sort key mapping for mouse click-to-sort on header row
|
|
78
|
+
const COLUMN_SORT_MAP = {
|
|
79
|
+
rank: 'rank',
|
|
80
|
+
tier: null, // 📖 Tier column click cycles tier filter rather than sorting
|
|
81
|
+
swe: 'swe',
|
|
82
|
+
ctx: 'ctx',
|
|
83
|
+
model: 'model',
|
|
84
|
+
source: 'origin',
|
|
85
|
+
ping: 'ping',
|
|
86
|
+
avg: 'avg',
|
|
87
|
+
health: 'condition',
|
|
88
|
+
verdict: 'verdict',
|
|
89
|
+
stability: 'stability',
|
|
90
|
+
uptime: 'uptime',
|
|
91
|
+
}
|
|
92
|
+
export { COLUMN_SORT_MAP }
|
|
93
|
+
|
|
59
94
|
// 📖 Provider column palette: soft pastel rainbow so each provider stays easy
|
|
60
95
|
// 📖 to spot without turning the table into a harsh neon wall.
|
|
61
96
|
// 📖 Exported for use in overlays (settings screen) and logs.
|
|
@@ -160,7 +195,7 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
160
195
|
const W_STATUS = 18
|
|
161
196
|
const W_VERDICT = 14
|
|
162
197
|
const W_UPTIME = 6
|
|
163
|
-
|
|
198
|
+
|
|
164
199
|
// const W_TOKENS = 7 // Used column removed
|
|
165
200
|
// const W_USAGE = 7 // Usage column removed
|
|
166
201
|
const MIN_TABLE_WIDTH = WIDTH_WARNING_MIN_COLS
|
|
@@ -180,7 +215,6 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
180
215
|
let showUptime = true
|
|
181
216
|
let showTier = true
|
|
182
217
|
let showStability = true
|
|
183
|
-
let showCompat = true // 📖 "Compatible with" column — hidden on narrow terminals
|
|
184
218
|
let isCompact = false
|
|
185
219
|
|
|
186
220
|
if (terminalCols > 0) {
|
|
@@ -192,7 +226,6 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
192
226
|
cols.push(W_SWE, W_CTX, W_MODEL, wSource, wPing, wAvg, wStatus, W_VERDICT)
|
|
193
227
|
if (showStability) cols.push(wStab)
|
|
194
228
|
if (showUptime) cols.push(W_UPTIME)
|
|
195
|
-
if (showCompat) cols.push(W_COMPAT)
|
|
196
229
|
return ROW_MARGIN + cols.reduce((a, b) => a + b, 0) + (cols.length - 1) * SEP_W
|
|
197
230
|
}
|
|
198
231
|
|
|
@@ -206,12 +239,39 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
206
239
|
wStatus = 13 // Health truncated after 6 chars + '…'
|
|
207
240
|
}
|
|
208
241
|
// 📖 Steps 2–5: Progressive column hiding (least useful first)
|
|
209
|
-
if (calcWidth() > terminalCols) showCompat = false
|
|
210
242
|
if (calcWidth() > terminalCols) showRank = false
|
|
211
243
|
if (calcWidth() > terminalCols) showUptime = false
|
|
212
244
|
if (calcWidth() > terminalCols) showTier = false
|
|
213
245
|
if (calcWidth() > terminalCols) showStability = false
|
|
214
246
|
}
|
|
247
|
+
|
|
248
|
+
// 📖 Mouse support: compute column boundaries from the resolved responsive widths.
|
|
249
|
+
// 📖 This builds an ordered array of { name, xStart, xEnd } (1-based display columns)
|
|
250
|
+
// 📖 matching exactly what renderTable paints so click-to-sort hits the right column.
|
|
251
|
+
{
|
|
252
|
+
const colDefs = []
|
|
253
|
+
if (showRank) colDefs.push({ name: 'rank', width: W_RANK })
|
|
254
|
+
if (showTier) colDefs.push({ name: 'tier', width: W_TIER })
|
|
255
|
+
colDefs.push({ name: 'swe', width: W_SWE })
|
|
256
|
+
colDefs.push({ name: 'ctx', width: W_CTX })
|
|
257
|
+
colDefs.push({ name: 'model', width: W_MODEL })
|
|
258
|
+
colDefs.push({ name: 'source', width: wSource })
|
|
259
|
+
colDefs.push({ name: 'ping', width: wPing })
|
|
260
|
+
colDefs.push({ name: 'avg', width: wAvg })
|
|
261
|
+
colDefs.push({ name: 'health', width: wStatus })
|
|
262
|
+
colDefs.push({ name: 'verdict', width: W_VERDICT })
|
|
263
|
+
if (showStability) colDefs.push({ name: 'stability', width: wStab })
|
|
264
|
+
if (showUptime) colDefs.push({ name: 'uptime', width: W_UPTIME })
|
|
265
|
+
let x = ROW_MARGIN + 1 // 📖 1-based: first column starts after the 2-char left margin
|
|
266
|
+
const columns = []
|
|
267
|
+
for (let i = 0; i < colDefs.length; i++) {
|
|
268
|
+
const { name, width } = colDefs[i]
|
|
269
|
+
const xEnd = x + width - 1
|
|
270
|
+
columns.push({ name, xStart: x, xEnd })
|
|
271
|
+
x = xEnd + 1 + SEP_W // 📖 skip past the ' │ ' separator
|
|
272
|
+
}
|
|
273
|
+
_lastLayout.columns = columns
|
|
274
|
+
}
|
|
215
275
|
const warningDurationMs = 2_000
|
|
216
276
|
const elapsed = widthWarningStartedAt ? Math.max(0, Date.now() - widthWarningStartedAt) : warningDurationMs
|
|
217
277
|
const remainingMs = Math.max(0, warningDurationMs - elapsed)
|
|
@@ -268,8 +328,8 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
268
328
|
const tierH = 'Tier'
|
|
269
329
|
const originH = 'Provider'
|
|
270
330
|
const modelH = 'Model'
|
|
271
|
-
const sweH = sortColumn === 'swe' ? dir + '
|
|
272
|
-
const ctxH = sortColumn === 'ctx' ? dir + '
|
|
331
|
+
const sweH = sortColumn === 'swe' ? (dir + 'SWE%') : 'SWE%'
|
|
332
|
+
const ctxH = sortColumn === 'ctx' ? (dir + 'CTX') : 'CTX'
|
|
273
333
|
// 📖 Compact labels: 'Lat. P' / 'Avg. P' / 'StaB.' to save horizontal space
|
|
274
334
|
const pingLabel = isCompact ? 'Lat. P' : 'Latest Ping'
|
|
275
335
|
const avgLabel = isCompact ? 'Avg. P' : 'Avg Ping'
|
|
@@ -278,8 +338,10 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
278
338
|
const avgH = sortColumn === 'avg' ? dir + ' ' + avgLabel : avgLabel
|
|
279
339
|
const healthH = sortColumn === 'condition' ? dir + ' Health' : 'Health'
|
|
280
340
|
const verdictH = sortColumn === 'verdict' ? dir + ' Verdict' : 'Verdict'
|
|
281
|
-
|
|
282
|
-
|
|
341
|
+
// 📖 Stability: in non-compact the arrow eats 2 chars ('↑ '), so truncate to fit wStab.
|
|
342
|
+
// 📖 Compact is fine because '↑ StaB.' (7) < wStab (8).
|
|
343
|
+
const stabH = sortColumn === 'stability' ? (dir + (isCompact ? ' ' + stabLabel : 'Stability')) : stabLabel
|
|
344
|
+
const uptimeH = sortColumn === 'uptime' ? (dir + 'Up%') : 'Up%'
|
|
283
345
|
|
|
284
346
|
// 📖 Helper to colorize first letter for keyboard shortcuts
|
|
285
347
|
// 📖 IMPORTANT: Pad PLAIN TEXT first, then apply colors to avoid alignment issues
|
|
@@ -326,14 +388,7 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
326
388
|
const padding = ' '.repeat(Math.max(0, W_UPTIME - plain.length))
|
|
327
389
|
return themeColors.hotkey('U') + themeColors.dim('p%' + padding)
|
|
328
390
|
})()
|
|
329
|
-
|
|
330
|
-
const compatHeaderEmojis = COMPAT_COLUMN_SLOTS.map(slot => {
|
|
331
|
-
return chalk.rgb(...slot.color)(slot.emoji)
|
|
332
|
-
}).join('')
|
|
333
|
-
// 📖 padEndDisplay accounts for emoji widths (most are 2-wide, π is 1-wide)
|
|
334
|
-
const compatHeaderRaw = COMPAT_COLUMN_SLOTS.reduce((w, slot) => w + displayWidth(slot.emoji), 0)
|
|
335
|
-
const compatHeaderPad = Math.max(0, W_COMPAT - compatHeaderRaw)
|
|
336
|
-
const compatH_c = compatHeaderEmojis + ' '.repeat(compatHeaderPad)
|
|
391
|
+
|
|
337
392
|
// 📖 Usage column removed from UI – no header or separator for it.
|
|
338
393
|
// 📖 Header row: conditionally include columns based on responsive visibility
|
|
339
394
|
const headerParts = []
|
|
@@ -342,9 +397,12 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
342
397
|
headerParts.push(sweH_c, ctxH_c, modelH_c, originH_c, pingH_c, avgH_c, healthH_c, verdictH_c)
|
|
343
398
|
if (showStability) headerParts.push(stabH_c)
|
|
344
399
|
if (showUptime) headerParts.push(uptimeH_c)
|
|
345
|
-
if (showCompat) headerParts.push(compatH_c)
|
|
346
400
|
lines.push(' ' + headerParts.join(COL_SEP))
|
|
347
401
|
|
|
402
|
+
// 📖 Mouse support: the column header row is the last line we just pushed.
|
|
403
|
+
// 📖 Terminal rows are 1-based, so line index (lines.length-1) → terminal row lines.length.
|
|
404
|
+
_lastLayout.headerRow = lines.length
|
|
405
|
+
|
|
348
406
|
|
|
349
407
|
|
|
350
408
|
if (sorted.length === 0) {
|
|
@@ -376,6 +434,14 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
376
434
|
lines.push(themeColors.dim(` ... ${vp.startIdx} more above ...`))
|
|
377
435
|
}
|
|
378
436
|
|
|
437
|
+
// 📖 Mouse support: record where model rows begin in the terminal (1-based).
|
|
438
|
+
// 📖 The next line pushed will be the first visible model row.
|
|
439
|
+
const _firstModelLineIdx = lines.length // 📖 0-based index into lines[]
|
|
440
|
+
_lastLayout.viewportStartIdx = vp.startIdx
|
|
441
|
+
_lastLayout.viewportEndIdx = vp.endIdx
|
|
442
|
+
_lastLayout.hasAboveIndicator = vp.hasAbove
|
|
443
|
+
_lastLayout.hasBelowIndicator = vp.hasBelow
|
|
444
|
+
|
|
379
445
|
for (let i = vp.startIdx; i < vp.endIdx; i++) {
|
|
380
446
|
const r = sorted[i]
|
|
381
447
|
const tierFn = TIER_COLOR[r.tier] ?? ((text) => themeColors.text(text))
|
|
@@ -601,28 +667,6 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
601
667
|
const sourceCursorText = providerDisplay.padEnd(wSource)
|
|
602
668
|
const sourceCell = isCursor ? themeColors.provider(r.providerKey, sourceCursorText, { bold: true }) : source
|
|
603
669
|
|
|
604
|
-
// 📖 "Compatible with" column — show colored emojis for compatible tools
|
|
605
|
-
// 📖 Each slot in COMPAT_COLUMN_SLOTS maps to one or more tool keys.
|
|
606
|
-
// 📖 OpenCode CLI + Desktop are merged into a single 📦 slot.
|
|
607
|
-
let compatCell = ''
|
|
608
|
-
if (showCompat) {
|
|
609
|
-
const compatTools = getCompatibleTools(r.providerKey)
|
|
610
|
-
let compatDisplayWidth = 0
|
|
611
|
-
const emojiCells = COMPAT_COLUMN_SLOTS.map(slot => {
|
|
612
|
-
const isCompat = slot.toolKeys.some(tk => compatTools.includes(tk))
|
|
613
|
-
const ew = displayWidth(slot.emoji)
|
|
614
|
-
compatDisplayWidth += isCompat ? ew : ew
|
|
615
|
-
if (isCompat) {
|
|
616
|
-
return chalk.rgb(...slot.color)(slot.emoji)
|
|
617
|
-
}
|
|
618
|
-
// 📖 Replace incompatible emoji with dim spaces matching its display width
|
|
619
|
-
return themeColors.dim(' '.repeat(ew))
|
|
620
|
-
}).join('')
|
|
621
|
-
// 📖 Pad to W_COMPAT — account for actual emoji display widths
|
|
622
|
-
const extraPad = Math.max(0, W_COMPAT - compatDisplayWidth)
|
|
623
|
-
compatCell = emojiCells + ' '.repeat(extraPad)
|
|
624
|
-
}
|
|
625
|
-
|
|
626
670
|
// 📖 Check if this model is incompatible with the active tool mode
|
|
627
671
|
const isIncompatible = !isModelCompatibleWithTool(r.providerKey, mode)
|
|
628
672
|
|
|
@@ -637,7 +681,6 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
637
681
|
rowParts.push(sweCell, ctxCell, nameCell, sourceCell, pingCell, avgCell, status, speedCell)
|
|
638
682
|
if (showStability) rowParts.push(stabCell)
|
|
639
683
|
if (showUptime) rowParts.push(uptimeCell)
|
|
640
|
-
if (showCompat) rowParts.push(compatCell)
|
|
641
684
|
const row = ' ' + rowParts.join(COL_SEP)
|
|
642
685
|
|
|
643
686
|
if (isCursor) {
|
|
@@ -656,6 +699,11 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
656
699
|
}
|
|
657
700
|
}
|
|
658
701
|
|
|
702
|
+
// 📖 Mouse support: record the 1-based terminal row range of model data rows.
|
|
703
|
+
// 📖 _firstModelLineIdx was captured before the loop; lines.length is now past the last model row.
|
|
704
|
+
_lastLayout.firstModelRow = _firstModelLineIdx + 1 // 📖 convert 0-based line index → 1-based terminal row
|
|
705
|
+
_lastLayout.lastModelRow = lines.length // 📖 last pushed line is at lines.length (1-based)
|
|
706
|
+
|
|
659
707
|
if (vp.hasBelow) {
|
|
660
708
|
lines.push(themeColors.dim(` ... ${sorted.length - vp.endIdx} more below ...`))
|
|
661
709
|
}
|
|
@@ -670,7 +718,40 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
670
718
|
const activeHotkey = (keyLabel, text, bg) => themeColors.badge(`${keyLabel}${text}`, bg, getReadableTextRgb(bg))
|
|
671
719
|
const favoritesModeBg = favoritesPinnedAndSticky ? [157, 122, 48] : [95, 95, 95]
|
|
672
720
|
const favoritesModeLabel = favoritesPinnedAndSticky ? ' Favorites Pinned' : ' Favorites Normal'
|
|
721
|
+
|
|
722
|
+
// 📖 Mouse support: build footer hotkey zones alongside the footer lines.
|
|
723
|
+
// 📖 Each zone records { key, row (1-based terminal row), xStart, xEnd (1-based display cols) }.
|
|
724
|
+
// 📖 We accumulate display position as we build each footer line's parts.
|
|
725
|
+
const footerHotkeys = []
|
|
726
|
+
|
|
673
727
|
// 📖 Line 1: core navigation + filtering shortcuts
|
|
728
|
+
// 📖 Build as parts array so we can compute click zones and still join for display.
|
|
729
|
+
{
|
|
730
|
+
const parts = [
|
|
731
|
+
{ text: ' ', key: null },
|
|
732
|
+
{ text: 'F Toggle Favorite', key: 'f' },
|
|
733
|
+
{ text: ' • ', key: null },
|
|
734
|
+
{ text: 'Y' + favoritesModeLabel, key: 'y' },
|
|
735
|
+
{ text: ' • ', key: null },
|
|
736
|
+
{ text: tierFilterMode > 0 ? `T Tier (${activeTierLabel})` : 'T Tier', key: 't' },
|
|
737
|
+
{ text: ' • ', key: null },
|
|
738
|
+
{ text: originFilterMode > 0 ? `D Provider (${activeOriginLabel})` : 'D Provider', key: 'd' },
|
|
739
|
+
{ text: ' • ', key: null },
|
|
740
|
+
{ text: 'E Show only configured models', key: 'e' },
|
|
741
|
+
{ text: ' • ', key: null },
|
|
742
|
+
{ text: 'P Settings', key: 'p' },
|
|
743
|
+
{ text: ' • ', key: null },
|
|
744
|
+
{ text: 'K Help', key: 'k' },
|
|
745
|
+
]
|
|
746
|
+
const footerRow1 = lines.length + 1 // 📖 1-based terminal row (line hasn't been pushed yet)
|
|
747
|
+
let xPos = 1
|
|
748
|
+
for (const part of parts) {
|
|
749
|
+
const w = displayWidth(part.text)
|
|
750
|
+
if (part.key) footerHotkeys.push({ key: part.key, row: footerRow1, xStart: xPos, xEnd: xPos + w - 1 })
|
|
751
|
+
xPos += w
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
674
755
|
lines.push(
|
|
675
756
|
' ' + hotkey('F', ' Toggle Favorite') +
|
|
676
757
|
themeColors.dim(` • `) +
|
|
@@ -684,12 +765,35 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
684
765
|
? activeHotkey('D', ` Provider (${activeOriginLabel})`, PROVIDER_COLOR[[null, ...Object.keys(sources)][originFilterMode]] || [255, 255, 255])
|
|
685
766
|
: hotkey('D', ' Provider')) +
|
|
686
767
|
themeColors.dim(` • `) +
|
|
687
|
-
(hideUnconfiguredModels ? activeHotkey('E', '
|
|
768
|
+
(hideUnconfiguredModels ? activeHotkey('E', ' Show only configured models', configuredBadgeBg) : hotkey('E', ' Show only configured models')) +
|
|
688
769
|
themeColors.dim(` • `) +
|
|
689
770
|
hotkey('P', ' Settings') +
|
|
690
771
|
themeColors.dim(` • `) +
|
|
691
772
|
hotkey('K', ' Help')
|
|
692
773
|
)
|
|
774
|
+
|
|
775
|
+
// 📖 Line 2: command palette, recommend, feedback, theme
|
|
776
|
+
{
|
|
777
|
+
const cpText = ' NEW ! CTRL+P ⚡️ Command Palette '
|
|
778
|
+
const parts = [
|
|
779
|
+
{ text: ' ', key: null },
|
|
780
|
+
{ text: cpText, key: 'ctrl+p' },
|
|
781
|
+
{ text: ' • ', key: null },
|
|
782
|
+
{ text: 'Q Smart Recommend', key: 'q' },
|
|
783
|
+
{ text: ' • ', key: null },
|
|
784
|
+
{ text: 'G Theme', key: 'g' },
|
|
785
|
+
{ text: ' • ', key: null },
|
|
786
|
+
{ text: 'I Feedback, bugs & requests', key: 'i' },
|
|
787
|
+
]
|
|
788
|
+
const footerRow2 = lines.length + 1
|
|
789
|
+
let xPos = 1
|
|
790
|
+
for (const part of parts) {
|
|
791
|
+
const w = displayWidth(part.text)
|
|
792
|
+
if (part.key) footerHotkeys.push({ key: part.key, row: footerRow2, xStart: xPos, xEnd: xPos + w - 1 })
|
|
793
|
+
xPos += w
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
693
797
|
// 📖 Line 2: command palette (highlighted as new), recommend, feedback, and extended hints.
|
|
694
798
|
// 📖 CTRL+P ⚡️ Command Palette uses neon-green-on-dark-green background to highlight the feature.
|
|
695
799
|
const paletteLabel = chalk.bgRgb(0, 60, 0).rgb(57, 255, 20).bold(' NEW ! CTRL+P ⚡️ Command Palette ')
|
|
@@ -745,6 +849,29 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
|
|
|
745
849
|
filterBadge = chalk.bgYellow.black.bold(` ${filterPrefix}${visibleFilter}${filterSuffix} `)
|
|
746
850
|
}
|
|
747
851
|
|
|
852
|
+
// 📖 Mouse support: track last footer line hotkey zones
|
|
853
|
+
{
|
|
854
|
+
const lastFooterRow = lines.length + 1 // 📖 1-based terminal row (line about to be pushed)
|
|
855
|
+
const parts = [
|
|
856
|
+
{ text: ' ', key: null },
|
|
857
|
+
{ text: 'N Changelog', key: 'n' },
|
|
858
|
+
]
|
|
859
|
+
if (hasCustomFilter) {
|
|
860
|
+
parts.push({ text: ' • ', key: null })
|
|
861
|
+
// 📖 X key clears filter — compute width from rendered badge text
|
|
862
|
+
const badgePlain = `X Disable filter: "${customTextFilter.trim().replace(/\s+/g, ' ')}"`
|
|
863
|
+
parts.push({ text: ` ${badgePlain} `, key: 'x' })
|
|
864
|
+
}
|
|
865
|
+
let xPos = 1
|
|
866
|
+
for (const part of parts) {
|
|
867
|
+
const w = displayWidth(part.text)
|
|
868
|
+
if (part.key) footerHotkeys.push({ key: part.key, row: lastFooterRow, xStart: xPos, xEnd: xPos + w - 1 })
|
|
869
|
+
xPos += w
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
_lastLayout.footerHotkeys = footerHotkeys
|
|
874
|
+
|
|
748
875
|
lines.push(
|
|
749
876
|
' ' + themeColors.hotkey('N') + themeColors.dim(' Changelog') +
|
|
750
877
|
(filterBadge
|