free-coding-models 0.1.83 → 0.1.84
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/README.md +6 -17
- package/bin/free-coding-models.js +297 -4754
- package/package.json +2 -2
- package/src/analysis.js +197 -0
- package/src/constants.js +116 -0
- package/src/favorites.js +98 -0
- package/src/key-handler.js +1005 -0
- package/src/openclaw.js +131 -0
- package/src/opencode.js +952 -0
- package/src/overlays.js +840 -0
- package/src/ping.js +186 -0
- package/src/provider-metadata.js +218 -0
- package/src/quota-capabilities.js +112 -0
- package/src/render-helpers.js +239 -0
- package/src/render-table.js +567 -0
- package/src/setup.js +105 -0
- package/src/telemetry.js +382 -0
- package/src/tier-colors.js +37 -0
- package/{lib → src}/token-stats.js +71 -3
- package/src/token-usage-reader.js +63 -0
- package/src/updater.js +237 -0
- package/{lib → src}/usage-reader.js +63 -21
- package/lib/quota-capabilities.js +0 -79
- /package/{lib → src}/account-manager.js +0 -0
- /package/{lib → src}/config.js +0 -0
- /package/{lib → src}/error-classifier.js +0 -0
- /package/{lib → src}/log-reader.js +0 -0
- /package/{lib → src}/model-merger.js +0 -0
- /package/{lib → src}/opencode-sync.js +0 -0
- /package/{lib → src}/provider-quota-fetchers.js +0 -0
- /package/{lib → src}/proxy-server.js +0 -0
- /package/{lib → src}/request-transformer.js +0 -0
- /package/{lib → src}/utils.js +0 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file render-helpers.js
|
|
3
|
+
* @description Rendering utility functions for TUI display and layout.
|
|
4
|
+
*
|
|
5
|
+
* @details
|
|
6
|
+
* This module provides helper functions for rendering the various UI elements:
|
|
7
|
+
* - String display width calculation for proper alignment (handles emojis)
|
|
8
|
+
* - ANSI code stripping for text width estimation
|
|
9
|
+
* - API key masking for security
|
|
10
|
+
* - Overlay viewport management (scrolling, clamping, visibility)
|
|
11
|
+
* - Table viewport calculation
|
|
12
|
+
* - Sorting with pinned favorites and recommendations
|
|
13
|
+
* - Proxy status rendering
|
|
14
|
+
*
|
|
15
|
+
* 🎯 Key features:
|
|
16
|
+
* - Emoji-aware display width calculation without external dependencies
|
|
17
|
+
* - ANSI color/control sequence stripping
|
|
18
|
+
* - API key masking (keeps first 4 and last 3 chars visible)
|
|
19
|
+
* - Overlay viewport helpers (clamp, slice, scroll target visibility)
|
|
20
|
+
* - Table viewport calculation with scroll indicators
|
|
21
|
+
* - Sorting with pinned favorites/recommendations at top
|
|
22
|
+
* - Proxy status formatting
|
|
23
|
+
*
|
|
24
|
+
* → Functions:
|
|
25
|
+
* - `stripAnsi`: Remove ANSI color codes to estimate visible text width
|
|
26
|
+
* - `maskApiKey`: Mask API keys (first 4 + *** + last 3 chars)
|
|
27
|
+
* - `displayWidth`: Calculate display width of string with emoji support
|
|
28
|
+
* - `padEndDisplay`: Left-pad using display width for proper alignment
|
|
29
|
+
* - `tintOverlayLines`: Apply background color to overlay lines
|
|
30
|
+
* - `clampOverlayOffset`: Clamp scroll offset to valid bounds
|
|
31
|
+
* - `keepOverlayTargetVisible`: Ensure target line is visible in viewport
|
|
32
|
+
* - `sliceOverlayLines`: Slice lines to viewport and pad with blanks
|
|
33
|
+
* - `calculateViewport`: Compute visible slice of model rows
|
|
34
|
+
* - `sortResultsWithPinnedFavorites`: Sort with pinned items at top
|
|
35
|
+
* - `renderProxyStatusLine`: Format proxy status for footer display
|
|
36
|
+
* - `adjustScrollOffset`: Clamp scrollOffset so cursor stays visible
|
|
37
|
+
*
|
|
38
|
+
* 📦 Dependencies:
|
|
39
|
+
* - chalk: Terminal colors and formatting
|
|
40
|
+
* - ../src/constants.js: OVERLAY_PANEL_WIDTH, TABLE_FIXED_LINES
|
|
41
|
+
* - ../src/utils.js: sortResults, getProxyStatusInfo
|
|
42
|
+
*
|
|
43
|
+
* ⚙️ Configuration:
|
|
44
|
+
* - OVERLAY_PANEL_WIDTH: Fixed width for overlay panels (from constants.js)
|
|
45
|
+
* - TABLE_FIXED_LINES: Fixed lines in table (header + footer, from constants.js)
|
|
46
|
+
*
|
|
47
|
+
* @see {@link ../src/constants.js} Constants for overlay and table layout
|
|
48
|
+
* @see {@link ../src/utils.js} Core sorting functions
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
import chalk from 'chalk'
|
|
52
|
+
import { OVERLAY_PANEL_WIDTH, TABLE_FIXED_LINES } from './constants.js'
|
|
53
|
+
import { sortResults, getProxyStatusInfo } from './utils.js'
|
|
54
|
+
|
|
55
|
+
// 📖 stripAnsi: Remove ANSI color/control sequences to estimate visible text width before padding.
|
|
56
|
+
// 📖 Strips CSI sequences (SGR colors) and OSC sequences (hyperlinks).
|
|
57
|
+
export function stripAnsi(input) {
|
|
58
|
+
return String(input).replace(/\x1b\[[0-9;]*m/g, '').replace(/\x1b\][^\x1b]*\x1b\\/g, '')
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 📖 maskApiKey: Mask all but first 4 and last 3 characters of an API key.
|
|
62
|
+
// 📖 Prevents accidental disclosure of secrets in TUI display.
|
|
63
|
+
export function maskApiKey(key) {
|
|
64
|
+
if (!key || key.length < 10) return '***'
|
|
65
|
+
return key.slice(0, 4) + '***' + key.slice(-3)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 📖 displayWidth: Calculate display width of a string in terminal columns.
|
|
69
|
+
// 📖 Emojis and other wide characters occupy 2 columns, variation selectors (U+FE0F) are zero-width.
|
|
70
|
+
// 📖 This avoids pulling in a full `string-width` dependency for a lightweight CLI tool.
|
|
71
|
+
export function displayWidth(str) {
|
|
72
|
+
const plain = stripAnsi(String(str))
|
|
73
|
+
let w = 0
|
|
74
|
+
for (const ch of plain) {
|
|
75
|
+
const cp = ch.codePointAt(0)
|
|
76
|
+
// Zero-width: variation selectors (FE00-FE0F), zero-width joiner/non-joiner, combining marks
|
|
77
|
+
if ((cp >= 0xFE00 && cp <= 0xFE0F) || cp === 0x200D || cp === 0x200C || cp === 0x20E3) continue
|
|
78
|
+
// Wide: CJK, emoji (most above U+1F000), fullwidth forms
|
|
79
|
+
if (
|
|
80
|
+
cp > 0x1F000 || // emoji & symbols
|
|
81
|
+
(cp >= 0x2600 && cp <= 0x27BF) || // misc symbols, dingbats
|
|
82
|
+
(cp >= 0x2300 && cp <= 0x23FF) || // misc technical (⏳, ⏰, etc.)
|
|
83
|
+
(cp >= 0x2700 && cp <= 0x27BF) || // dingbats
|
|
84
|
+
(cp >= 0xFE10 && cp <= 0xFE19) || // vertical forms
|
|
85
|
+
(cp >= 0xFF01 && cp <= 0xFF60) || // fullwidth ASCII
|
|
86
|
+
(cp >= 0xFFE0 && cp <= 0xFFE6) || // fullwidth signs
|
|
87
|
+
(cp >= 0x4E00 && cp <= 0x9FFF) || // CJK unified
|
|
88
|
+
(cp >= 0x3000 && cp <= 0x303F) || // CJK symbols
|
|
89
|
+
(cp >= 0x2B50 && cp <= 0x2B55) || // stars, circles
|
|
90
|
+
cp === 0x2705 || cp === 0x2714 || cp === 0x2716 || // check/cross marks
|
|
91
|
+
cp === 0x26A0 // ⚠ warning sign
|
|
92
|
+
) {
|
|
93
|
+
w += 2
|
|
94
|
+
} else {
|
|
95
|
+
w += 1
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return w
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 📖 padEndDisplay: Left-pad (padEnd equivalent) using display width instead of string length.
|
|
102
|
+
// 📖 Ensures columns with emoji text align correctly in the terminal.
|
|
103
|
+
export function padEndDisplay(str, width) {
|
|
104
|
+
const dw = displayWidth(str)
|
|
105
|
+
const need = Math.max(0, width - dw)
|
|
106
|
+
return str + ' '.repeat(need)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 📖 tintOverlayLines: Tint overlay lines with a fixed dark panel width so the background is clearly visible.
|
|
110
|
+
// 📖 Applies bgColor to each line and pads to OVERLAY_PANEL_WIDTH for consistent panel look.
|
|
111
|
+
export function tintOverlayLines(lines, bgColor) {
|
|
112
|
+
return lines.map((line) => {
|
|
113
|
+
const text = String(line)
|
|
114
|
+
const visibleWidth = stripAnsi(text).length
|
|
115
|
+
const padding = ' '.repeat(Math.max(0, OVERLAY_PANEL_WIDTH - visibleWidth))
|
|
116
|
+
return bgColor(text + padding)
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 📖 clampOverlayOffset: Clamp overlay scroll to valid bounds for the current terminal height.
|
|
121
|
+
export function clampOverlayOffset(offset, totalLines, terminalRows) {
|
|
122
|
+
const viewportRows = Math.max(1, terminalRows || 1)
|
|
123
|
+
const maxOffset = Math.max(0, totalLines - viewportRows)
|
|
124
|
+
return Math.max(0, Math.min(maxOffset, offset))
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 📖 keepOverlayTargetVisible: Ensure a target line is visible inside overlay viewport (used by Settings cursor).
|
|
128
|
+
// 📖 Adjusts offset so the target line is always visible, scrolling if needed.
|
|
129
|
+
export function keepOverlayTargetVisible(offset, targetLine, totalLines, terminalRows) {
|
|
130
|
+
const viewportRows = Math.max(1, terminalRows || 1)
|
|
131
|
+
let next = clampOverlayOffset(offset, totalLines, terminalRows)
|
|
132
|
+
if (targetLine < next) next = targetLine
|
|
133
|
+
else if (targetLine >= next + viewportRows) next = targetLine - viewportRows + 1
|
|
134
|
+
return clampOverlayOffset(next, totalLines, terminalRows)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 📖 sliceOverlayLines: Slice overlay lines to terminal viewport and pad with blanks to avoid stale frames.
|
|
138
|
+
// 📖 Returns { visible, offset } where visible is the sliced/padded lines array.
|
|
139
|
+
export function sliceOverlayLines(lines, offset, terminalRows) {
|
|
140
|
+
const viewportRows = Math.max(1, terminalRows || 1)
|
|
141
|
+
const nextOffset = clampOverlayOffset(offset, lines.length, terminalRows)
|
|
142
|
+
const visible = lines.slice(nextOffset, nextOffset + viewportRows)
|
|
143
|
+
while (visible.length < viewportRows) visible.push('')
|
|
144
|
+
return { visible, offset: nextOffset }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ─── Table viewport calculation ────────────────────────────────────────────────
|
|
148
|
+
|
|
149
|
+
// 📖 calculateViewport: Computes the visible slice of model rows that fits in the terminal.
|
|
150
|
+
// 📖 When scroll indicators are needed, they each consume 1 line from the model budget.
|
|
151
|
+
// 📖 Returns { startIdx, endIdx, hasAbove, hasBelow } for rendering.
|
|
152
|
+
export function calculateViewport(terminalRows, scrollOffset, totalModels) {
|
|
153
|
+
if (terminalRows <= 0) return { startIdx: 0, endIdx: totalModels, hasAbove: false, hasBelow: false }
|
|
154
|
+
let maxSlots = terminalRows - TABLE_FIXED_LINES
|
|
155
|
+
if (maxSlots < 1) maxSlots = 1
|
|
156
|
+
if (totalModels <= maxSlots) return { startIdx: 0, endIdx: totalModels, hasAbove: false, hasBelow: false }
|
|
157
|
+
|
|
158
|
+
const hasAbove = scrollOffset > 0
|
|
159
|
+
const hasBelow = scrollOffset + maxSlots - (hasAbove ? 1 : 0) < totalModels
|
|
160
|
+
// Recalculate with indicator lines accounted for
|
|
161
|
+
const modelSlots = maxSlots - (hasAbove ? 1 : 0) - (hasBelow ? 1 : 0)
|
|
162
|
+
const endIdx = Math.min(scrollOffset + modelSlots, totalModels)
|
|
163
|
+
return { startIdx: scrollOffset, endIdx, hasAbove, hasBelow }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ─── Sorting helpers ───────────────────────────────────────────────────────────
|
|
167
|
+
|
|
168
|
+
// 📖 sortResultsWithPinnedFavorites: Recommended models are pinned above favorites, favorites above non-favorites.
|
|
169
|
+
// 📖 Recommended: sorted by recommendation score (highest first).
|
|
170
|
+
// 📖 Favorites: keep insertion order (favoriteRank).
|
|
171
|
+
// 📖 Non-favorites: active sort column/direction.
|
|
172
|
+
// 📖 Models that are both recommended AND favorite — show in recommended section.
|
|
173
|
+
export function sortResultsWithPinnedFavorites(results, sortColumn, sortDirection) {
|
|
174
|
+
const recommendedRows = results
|
|
175
|
+
.filter((r) => r.isRecommended && !r.isFavorite)
|
|
176
|
+
.sort((a, b) => (b.recommendScore || 0) - (a.recommendScore || 0))
|
|
177
|
+
const favoriteRows = results
|
|
178
|
+
.filter((r) => r.isFavorite && !r.isRecommended)
|
|
179
|
+
.sort((a, b) => a.favoriteRank - b.favoriteRank)
|
|
180
|
+
// 📖 Models that are both recommended AND favorite — show in recommended section
|
|
181
|
+
const bothRows = results
|
|
182
|
+
.filter((r) => r.isRecommended && r.isFavorite)
|
|
183
|
+
.sort((a, b) => (b.recommendScore || 0) - (a.recommendScore || 0))
|
|
184
|
+
const nonSpecialRows = sortResults(results.filter((r) => !r.isFavorite && !r.isRecommended), sortColumn, sortDirection)
|
|
185
|
+
return [...bothRows, ...recommendedRows, ...favoriteRows, ...nonSpecialRows]
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ─── Proxy status rendering ───────────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
// 📖 renderProxyStatusLine: Maps proxyStartupStatus + active proxy into a chalk-coloured footer line.
|
|
191
|
+
// 📖 Always returns a non-empty string (no hidden states) so the footer row is always present.
|
|
192
|
+
// 📖 Delegates state classification to the pure getProxyStatusInfo helper (testable in utils.js).
|
|
193
|
+
export function renderProxyStatusLine(proxyStartupStatus, proxyInstance) {
|
|
194
|
+
const info = getProxyStatusInfo(proxyStartupStatus, !!proxyInstance)
|
|
195
|
+
switch (info.state) {
|
|
196
|
+
case 'starting':
|
|
197
|
+
return chalk.dim(' ') + chalk.yellow('⟳ Proxy') + chalk.dim(' starting…')
|
|
198
|
+
case 'running': {
|
|
199
|
+
const portPart = info.port ? chalk.dim(` :${info.port}`) : ''
|
|
200
|
+
const acctPart = info.accountCount != null ? chalk.dim(` · ${info.accountCount} account${info.accountCount === 1 ? '' : 's'}`) : ''
|
|
201
|
+
return chalk.dim(' ') + chalk.rgb(57, 255, 20)('🔀 Proxy') + chalk.rgb(57, 255, 20)(' running') + portPart + acctPart
|
|
202
|
+
}
|
|
203
|
+
case 'failed':
|
|
204
|
+
return chalk.dim(' ') + chalk.red('✗ Proxy failed') + chalk.dim(` — ${info.reason}`)
|
|
205
|
+
default:
|
|
206
|
+
// stopped / not configured — dim but always present
|
|
207
|
+
return chalk.dim(' 🔀 Proxy not configured')
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ─── Scroll offset adjustment ──────────────────────────────────────────────────
|
|
212
|
+
|
|
213
|
+
// 📖 adjustScrollOffset: Clamp scrollOffset so cursor is always within the visible viewport window.
|
|
214
|
+
// 📖 Called after every cursor move, sort change, and terminal resize.
|
|
215
|
+
// 📖 Modifies st.scrollOffset in-place, returns undefined.
|
|
216
|
+
export function adjustScrollOffset(st) {
|
|
217
|
+
const total = st.visibleSorted ? st.visibleSorted.length : st.results.filter(r => !r.hidden).length
|
|
218
|
+
let maxSlots = st.terminalRows - TABLE_FIXED_LINES
|
|
219
|
+
if (maxSlots < 1) maxSlots = 1
|
|
220
|
+
if (total <= maxSlots) { st.scrollOffset = 0; return }
|
|
221
|
+
// Ensure cursor is not above the visible window
|
|
222
|
+
if (st.cursor < st.scrollOffset) {
|
|
223
|
+
st.scrollOffset = st.cursor
|
|
224
|
+
}
|
|
225
|
+
// Ensure cursor is not below the visible window
|
|
226
|
+
// Account for indicator lines eating into model slots
|
|
227
|
+
const hasAbove = st.scrollOffset > 0
|
|
228
|
+
const tentativeBelow = st.scrollOffset + maxSlots - (hasAbove ? 1 : 0) < total
|
|
229
|
+
const modelSlots = maxSlots - (hasAbove ? 1 : 0) - (tentativeBelow ? 1 : 0)
|
|
230
|
+
if (st.cursor >= st.scrollOffset + modelSlots) {
|
|
231
|
+
st.scrollOffset = st.cursor - modelSlots + 1
|
|
232
|
+
}
|
|
233
|
+
// Final clamp
|
|
234
|
+
// 📖 Keep one extra scroll step when top indicator is visible,
|
|
235
|
+
// 📖 otherwise the last rows become unreachable at the bottom.
|
|
236
|
+
const maxOffset = Math.max(0, total - maxSlots + 1)
|
|
237
|
+
if (st.scrollOffset > maxOffset) st.scrollOffset = maxOffset
|
|
238
|
+
if (st.scrollOffset < 0) st.scrollOffset = 0
|
|
239
|
+
}
|