free-coding-models 0.3.16 β 0.3.18
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 +23 -0
- package/README.md +195 -120
- package/package.json +1 -1
- package/src/app.js +20 -2
- package/src/config.js +3 -0
- package/src/key-handler.js +184 -38
- package/src/openclaw.js +39 -5
- package/src/opencode.js +2 -1
- package/src/overlays.js +314 -223
- package/src/render-helpers.js +1 -1
- package/src/render-table.js +152 -180
- package/src/theme.js +315 -0
- package/src/tier-colors.js +15 -17
- package/src/tool-bootstrap.js +310 -0
- package/src/tool-launchers.js +12 -7
- package/src/ui-config.js +24 -31
package/src/overlays.js
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
|
|
23
23
|
import { loadChangelog } from './changelog-loader.js'
|
|
24
24
|
import { buildCliHelpLines } from './cli-help.js'
|
|
25
|
+
import { themeColors, getThemeStatusLabel, getProviderRgb } from './theme.js'
|
|
25
26
|
|
|
26
27
|
export function createOverlayRenderers(state, deps) {
|
|
27
28
|
const {
|
|
@@ -34,9 +35,6 @@ export function createOverlayRenderers(state, deps) {
|
|
|
34
35
|
resolveApiKeys,
|
|
35
36
|
isProviderEnabled,
|
|
36
37
|
TIER_CYCLE,
|
|
37
|
-
SETTINGS_OVERLAY_BG,
|
|
38
|
-
HELP_OVERLAY_BG,
|
|
39
|
-
RECOMMEND_OVERLAY_BG,
|
|
40
38
|
OVERLAY_PANEL_WIDTH,
|
|
41
39
|
keepOverlayTargetVisible,
|
|
42
40
|
sliceOverlayLines,
|
|
@@ -56,8 +54,13 @@ export function createOverlayRenderers(state, deps) {
|
|
|
56
54
|
getInstallTargetModes,
|
|
57
55
|
getProviderCatalogModels,
|
|
58
56
|
getToolMeta,
|
|
57
|
+
getToolInstallPlan,
|
|
58
|
+
padEndDisplay,
|
|
59
59
|
} = deps
|
|
60
60
|
|
|
61
|
+
const bullet = (isCursor) => (isCursor ? themeColors.accentBold(' β― ') : themeColors.dim(' '))
|
|
62
|
+
const activeThemeSetting = () => state.config.settings?.theme || 'auto'
|
|
63
|
+
|
|
61
64
|
// π Wrap plain diagnostic text so long Settings messages stay readable inside
|
|
62
65
|
// π the overlay instead of turning into one truncated red line.
|
|
63
66
|
// π Uses 100% of terminal width minus padding for better readability.
|
|
@@ -90,25 +93,26 @@ export function createOverlayRenderers(state, deps) {
|
|
|
90
93
|
const providerKeys = Object.keys(sources)
|
|
91
94
|
const updateRowIdx = providerKeys.length
|
|
92
95
|
const widthWarningRowIdx = updateRowIdx + 1
|
|
93
|
-
const
|
|
96
|
+
const themeRowIdx = widthWarningRowIdx + 1
|
|
97
|
+
const cleanupLegacyProxyRowIdx = themeRowIdx + 1
|
|
94
98
|
const changelogViewRowIdx = cleanupLegacyProxyRowIdx + 1
|
|
95
99
|
const EL = '\x1b[K'
|
|
96
100
|
const lines = []
|
|
97
101
|
const cursorLineByRow = {}
|
|
98
102
|
|
|
99
103
|
// π Branding header
|
|
100
|
-
lines.push(` ${
|
|
101
|
-
lines.push(` ${
|
|
104
|
+
lines.push(` ${themeColors.accent('π')} ${themeColors.accentBold('free-coding-models')} ${themeColors.dim(`v${LOCAL_VERSION}`)}`)
|
|
105
|
+
lines.push(` ${themeColors.textBold('β Settings')}`)
|
|
102
106
|
|
|
103
107
|
if (state.settingsErrorMsg) {
|
|
104
|
-
lines.push(` ${
|
|
108
|
+
lines.push(` ${themeColors.errorBold(state.settingsErrorMsg)}`)
|
|
105
109
|
lines.push('')
|
|
106
110
|
}
|
|
107
111
|
|
|
108
|
-
lines.push(` ${
|
|
112
|
+
lines.push(` ${themeColors.textBold('π§© Providers')}`)
|
|
109
113
|
// π Dynamic separator line using 100% terminal width
|
|
110
114
|
const separatorWidth = Math.max(20, state.terminalCols - 10)
|
|
111
|
-
lines.push(` ${
|
|
115
|
+
lines.push(` ${themeColors.dim(' ' + 'β'.repeat(separatorWidth))}`)
|
|
112
116
|
lines.push('')
|
|
113
117
|
|
|
114
118
|
for (let i = 0; i < providerKeys.length; i++) {
|
|
@@ -126,44 +130,43 @@ export function createOverlayRenderers(state, deps) {
|
|
|
126
130
|
let keyDisplay
|
|
127
131
|
if ((state.settingsEditMode || state.settingsAddKeyMode) && isCursor) {
|
|
128
132
|
// π Inline editing/adding: show typed buffer with cursor indicator
|
|
129
|
-
const modePrefix = state.settingsAddKeyMode ?
|
|
130
|
-
keyDisplay =
|
|
133
|
+
const modePrefix = state.settingsAddKeyMode ? themeColors.dim('[+] ') : ''
|
|
134
|
+
keyDisplay = themeColors.accentBold(`${modePrefix}${state.settingsEditBuffer || ''}β`)
|
|
131
135
|
} else if (keyCount > 0) {
|
|
132
136
|
// π Show the primary (first/string) key masked + count indicator for extras
|
|
133
137
|
const primaryKey = allKeys[0]
|
|
134
138
|
const visible = primaryKey.slice(-4)
|
|
135
139
|
const masked = 'β’'.repeat(Math.min(16, Math.max(4, primaryKey.length - 4)))
|
|
136
|
-
const keyMasked =
|
|
137
|
-
const extra = keyCount > 1 ?
|
|
140
|
+
const keyMasked = themeColors.dim(masked + visible)
|
|
141
|
+
const extra = keyCount > 1 ? themeColors.info(` (+${keyCount - 1} more)`) : ''
|
|
138
142
|
keyDisplay = keyMasked + extra
|
|
139
143
|
} else {
|
|
140
|
-
keyDisplay =
|
|
144
|
+
keyDisplay = themeColors.dim('(no key set)')
|
|
141
145
|
}
|
|
142
146
|
|
|
143
147
|
// π Test result badge
|
|
144
148
|
const testResult = state.settingsTestResults[pk]
|
|
145
149
|
// π Default badge reflects configuration first: a saved key should look
|
|
146
150
|
// π ready to test even before the user has run the probe once.
|
|
147
|
-
let testBadge = keyCount > 0 ?
|
|
148
|
-
if (testResult === 'pending') testBadge =
|
|
149
|
-
else if (testResult === 'ok') testBadge =
|
|
150
|
-
else if (testResult === 'missing_key') testBadge =
|
|
151
|
-
else if (testResult === 'auth_error') testBadge =
|
|
152
|
-
else if (testResult === 'rate_limited') testBadge =
|
|
153
|
-
else if (testResult === 'no_callable_model') testBadge = chalk.
|
|
154
|
-
else if (testResult === 'fail') testBadge =
|
|
151
|
+
let testBadge = keyCount > 0 ? themeColors.info('[Test]') : themeColors.dim('[Missing Key π]')
|
|
152
|
+
if (testResult === 'pending') testBadge = themeColors.warning('[Testingβ¦]')
|
|
153
|
+
else if (testResult === 'ok') testBadge = themeColors.successBold('[Test β
]')
|
|
154
|
+
else if (testResult === 'missing_key') testBadge = themeColors.dim('[Missing Key π]')
|
|
155
|
+
else if (testResult === 'auth_error') testBadge = themeColors.error('[Auth β]')
|
|
156
|
+
else if (testResult === 'rate_limited') testBadge = themeColors.warning('[Rate limit β³]')
|
|
157
|
+
else if (testResult === 'no_callable_model') testBadge = chalk.rgb(...getProviderRgb('openrouter'))('[No model β ]')
|
|
158
|
+
else if (testResult === 'fail') testBadge = themeColors.error('[Test β]')
|
|
155
159
|
// π No truncation of rate limits - overlay now uses 100% terminal width
|
|
156
|
-
const rateSummary =
|
|
160
|
+
const rateSummary = themeColors.dim(meta.rateLimits || 'No limit info')
|
|
157
161
|
|
|
158
|
-
const enabledBadge = enabled ?
|
|
162
|
+
const enabledBadge = enabled ? themeColors.successBold('β
') : themeColors.errorBold('β')
|
|
159
163
|
// π Color provider names the same way as in the main table
|
|
160
164
|
const providerRgb = PROVIDER_COLOR[pk] ?? [105, 190, 245]
|
|
161
165
|
const providerName = chalk.bold.rgb(...providerRgb)((meta.label || src.name || pk).slice(0, 22).padEnd(22))
|
|
162
|
-
const bullet = isCursor ? chalk.bold.cyan(' β― ') : chalk.dim(' ')
|
|
163
166
|
|
|
164
|
-
const row = `${bullet}[ ${enabledBadge} ] ${providerName} ${keyDisplay
|
|
167
|
+
const row = `${bullet(isCursor)}[ ${enabledBadge} ] ${providerName} ${padEndDisplay(keyDisplay, 30)} ${testBadge} ${rateSummary}`
|
|
165
168
|
cursorLineByRow[i] = lines.length
|
|
166
|
-
lines.push(isCursor ?
|
|
169
|
+
lines.push(isCursor ? themeColors.bgCursor(row) : row)
|
|
167
170
|
}
|
|
168
171
|
|
|
169
172
|
lines.push('')
|
|
@@ -172,100 +175,101 @@ export function createOverlayRenderers(state, deps) {
|
|
|
172
175
|
const selectedMeta = PROVIDER_METADATA[selectedProviderKey] || {}
|
|
173
176
|
if (selectedSource && state.settingsCursor < providerKeys.length) {
|
|
174
177
|
const selectedKey = getApiKey(state.config, selectedProviderKey)
|
|
175
|
-
const setupStatus = selectedKey ?
|
|
178
|
+
const setupStatus = selectedKey ? themeColors.success('API key detected β
') : themeColors.warning('API key missing β ')
|
|
176
179
|
// π Color the provider name in the setup instructions header
|
|
177
180
|
const selectedProviderRgb = PROVIDER_COLOR[selectedProviderKey] ?? [105, 190, 245]
|
|
178
181
|
const coloredProviderName = chalk.bold.rgb(...selectedProviderRgb)(selectedMeta.label || selectedSource.name || selectedProviderKey)
|
|
179
|
-
lines.push(` ${
|
|
180
|
-
lines.push(
|
|
181
|
-
lines.push(
|
|
182
|
-
lines.push(
|
|
182
|
+
lines.push(` ${themeColors.textBold('Setup Instructions')} β ${coloredProviderName}`)
|
|
183
|
+
lines.push(themeColors.dim(` 1) Create a ${selectedMeta.label || selectedSource.name} account: ${selectedMeta.signupUrl || 'signup link missing'}`))
|
|
184
|
+
lines.push(themeColors.dim(` 2) ${selectedMeta.signupHint || 'Generate an API key and paste it with Enter on this row'}`))
|
|
185
|
+
lines.push(themeColors.dim(` 3) Press ${themeColors.hotkey('T')} to test your key. Status: ${setupStatus}`))
|
|
183
186
|
if (selectedProviderKey === 'cloudflare') {
|
|
184
187
|
const hasAccountId = Boolean((process.env.CLOUDFLARE_ACCOUNT_ID || '').trim())
|
|
185
|
-
const accountIdStatus = hasAccountId ?
|
|
186
|
-
lines.push(
|
|
188
|
+
const accountIdStatus = hasAccountId ? themeColors.success('CLOUDFLARE_ACCOUNT_ID detected β
') : themeColors.warning('Set CLOUDFLARE_ACCOUNT_ID β ')
|
|
189
|
+
lines.push(themeColors.dim(` 4) Export ${themeColors.hotkey('CLOUDFLARE_ACCOUNT_ID')} in your shell. Status: ${accountIdStatus}`))
|
|
187
190
|
}
|
|
188
191
|
const testDetail = state.settingsTestDetails?.[selectedProviderKey]
|
|
189
192
|
if (testDetail) {
|
|
190
193
|
lines.push('')
|
|
191
|
-
lines.push(
|
|
194
|
+
lines.push(themeColors.errorBold(' Test Diagnostics'))
|
|
192
195
|
for (const detailLine of wrapPlainText(testDetail)) {
|
|
193
|
-
lines.push(
|
|
196
|
+
lines.push(themeColors.error(` ${detailLine}`))
|
|
194
197
|
}
|
|
195
198
|
}
|
|
196
199
|
lines.push('')
|
|
197
200
|
}
|
|
198
201
|
|
|
199
202
|
lines.push('')
|
|
200
|
-
lines.push(` ${
|
|
201
|
-
lines.push(` ${
|
|
203
|
+
lines.push(` ${themeColors.textBold('π Maintenance')}`)
|
|
204
|
+
lines.push(` ${themeColors.dim(' ' + 'β'.repeat(separatorWidth))}`)
|
|
202
205
|
lines.push('')
|
|
203
206
|
|
|
204
207
|
const updateCursor = state.settingsCursor === updateRowIdx
|
|
205
|
-
const updateBullet = updateCursor ? chalk.bold.cyan(' β― ') : chalk.dim(' ')
|
|
206
208
|
const updateState = state.settingsUpdateState
|
|
207
209
|
const latestFound = state.settingsUpdateLatestVersion
|
|
208
210
|
const updateActionLabel = updateState === 'available' && latestFound
|
|
209
211
|
? `Install update (v${latestFound})`
|
|
210
212
|
: 'Check for updates manually'
|
|
211
|
-
let updateStatus =
|
|
212
|
-
if (updateState === 'checking') updateStatus =
|
|
213
|
-
if (updateState === 'available' && latestFound) updateStatus =
|
|
214
|
-
if (updateState === 'up-to-date') updateStatus =
|
|
215
|
-
if (updateState === 'error') updateStatus =
|
|
216
|
-
if (updateState === 'installing') updateStatus =
|
|
217
|
-
const updateRow = `${
|
|
213
|
+
let updateStatus = themeColors.dim('Press Enter or U to check npm registry')
|
|
214
|
+
if (updateState === 'checking') updateStatus = themeColors.warning('Checking npm registryβ¦')
|
|
215
|
+
if (updateState === 'available' && latestFound) updateStatus = themeColors.successBold(`Update available: v${latestFound} (Enter to install)`)
|
|
216
|
+
if (updateState === 'up-to-date') updateStatus = themeColors.success('Already on latest version')
|
|
217
|
+
if (updateState === 'error') updateStatus = themeColors.error('Check failed (press U to retry)')
|
|
218
|
+
if (updateState === 'installing') updateStatus = themeColors.info('Installing updateβ¦')
|
|
219
|
+
const updateRow = `${bullet(updateCursor)}${themeColors.textBold(updateActionLabel).padEnd(44)} ${updateStatus}`
|
|
218
220
|
cursorLineByRow[updateRowIdx] = lines.length
|
|
219
|
-
lines.push(updateCursor ?
|
|
221
|
+
lines.push(updateCursor ? themeColors.bgCursor(updateRow) : updateRow)
|
|
220
222
|
// π Width warning visibility row for the startup narrow-terminal overlay.
|
|
221
223
|
const disableWidthsWarning = Boolean(state.config.settings?.disableWidthsWarning)
|
|
222
|
-
const widthWarningBullet = state.settingsCursor === widthWarningRowIdx ? chalk.bold.cyan(' β― ') : chalk.dim(' ')
|
|
223
224
|
const widthWarningStatus = disableWidthsWarning
|
|
224
|
-
?
|
|
225
|
-
:
|
|
226
|
-
const widthWarningRow = `${
|
|
225
|
+
? themeColors.errorBold('π Disabled')
|
|
226
|
+
: themeColors.successBold('π Enabled')
|
|
227
|
+
const widthWarningRow = `${bullet(state.settingsCursor === widthWarningRowIdx)}${themeColors.textBold('Small Width Warnings').padEnd(44)} ${widthWarningStatus}`
|
|
227
228
|
cursorLineByRow[widthWarningRowIdx] = lines.length
|
|
228
|
-
lines.push(state.settingsCursor === widthWarningRowIdx ?
|
|
229
|
+
lines.push(state.settingsCursor === widthWarningRowIdx ? themeColors.bgCursor(widthWarningRow) : widthWarningRow)
|
|
230
|
+
const themeStatus = getThemeStatusLabel(activeThemeSetting())
|
|
231
|
+
const themeStatusColor = themeStatus.includes('Dark') ? themeColors.warningBold : themeColors.info
|
|
232
|
+
const themeRow = `${bullet(state.settingsCursor === themeRowIdx)}${themeColors.textBold('Global Theme').padEnd(44)} ${themeStatusColor(themeStatus)}`
|
|
233
|
+
cursorLineByRow[themeRowIdx] = lines.length
|
|
234
|
+
lines.push(state.settingsCursor === themeRowIdx ? themeColors.bgCursor(themeRow) : themeRow)
|
|
229
235
|
if (updateState === 'error' && state.settingsUpdateError) {
|
|
230
|
-
lines.push(
|
|
236
|
+
lines.push(themeColors.error(` ${state.settingsUpdateError}`))
|
|
231
237
|
}
|
|
232
238
|
|
|
233
239
|
// π Cleanup row removes stale proxy-era config left behind by older builds.
|
|
234
|
-
const
|
|
235
|
-
const cleanupLegacyProxyRow = `${cleanupLegacyProxyBullet}${chalk.bold('Clean Legacy Proxy Config').padEnd(44)} ${chalk.magentaBright('Enter remove discontinued bridge leftovers')}`
|
|
240
|
+
const cleanupLegacyProxyRow = `${bullet(state.settingsCursor === cleanupLegacyProxyRowIdx)}${themeColors.textBold('Clean Legacy Proxy Config').padEnd(44)} ${themeColors.warning('Enter remove discontinued bridge leftovers')}`
|
|
236
241
|
cursorLineByRow[cleanupLegacyProxyRowIdx] = lines.length
|
|
237
|
-
lines.push(state.settingsCursor === cleanupLegacyProxyRowIdx ?
|
|
242
|
+
lines.push(state.settingsCursor === cleanupLegacyProxyRowIdx ? themeColors.bgCursorLegacy(cleanupLegacyProxyRow) : cleanupLegacyProxyRow)
|
|
238
243
|
|
|
239
244
|
// π Changelog viewer row
|
|
240
|
-
const
|
|
241
|
-
const changelogViewRow = `${changelogViewBullet}${chalk.bold('View Changelog').padEnd(44)} ${chalk.dim('Enter browse version history')}`
|
|
245
|
+
const changelogViewRow = `${bullet(state.settingsCursor === changelogViewRowIdx)}${themeColors.textBold('View Changelog').padEnd(44)} ${themeColors.dim('Enter browse version history')}`
|
|
242
246
|
cursorLineByRow[changelogViewRowIdx] = lines.length
|
|
243
|
-
lines.push(state.settingsCursor === changelogViewRowIdx ?
|
|
247
|
+
lines.push(state.settingsCursor === changelogViewRowIdx ? themeColors.bgCursorSettingsList(changelogViewRow) : changelogViewRow)
|
|
244
248
|
|
|
245
249
|
// π Profile system removed - API keys now persist permanently across all sessions
|
|
246
250
|
|
|
247
251
|
lines.push('')
|
|
248
252
|
if (state.settingsEditMode) {
|
|
249
|
-
lines.push(
|
|
253
|
+
lines.push(themeColors.dim(' Type API key β’ Enter Save β’ Esc Cancel'))
|
|
250
254
|
} else {
|
|
251
|
-
lines.push(
|
|
255
|
+
lines.push(themeColors.dim(' ββ Navigate β’ Enter Edit/Run/Cycle β’ + Add key β’ - Remove key β’ Space Toggle/Cycle β’ T Test key β’ U Updates β’ G Global theme β’ Esc Close'))
|
|
252
256
|
}
|
|
253
257
|
// π Show sync/restore status message if set
|
|
254
258
|
if (state.settingsSyncStatus) {
|
|
255
259
|
const { type, msg } = state.settingsSyncStatus
|
|
256
|
-
lines.push(type === 'success' ?
|
|
260
|
+
lines.push(type === 'success' ? themeColors.successBold(` ${msg}`) : themeColors.warning(` ${msg}`))
|
|
257
261
|
}
|
|
258
262
|
lines.push('')
|
|
259
263
|
|
|
260
264
|
// π Footer with credits
|
|
261
265
|
lines.push('')
|
|
262
266
|
lines.push(
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
267
|
+
themeColors.dim(' ') +
|
|
268
|
+
themeColors.footerLove('Made with π & β by ') +
|
|
269
|
+
themeColors.link('\x1b]8;;https://github.com/vava-nessa\x1b\\vava-nessa\x1b]8;;\x1b\\') +
|
|
270
|
+
themeColors.dim(' β’ β ') +
|
|
271
|
+
themeColors.footerCoffee('\x1b]8;;https://buymeacoffee.com/vavanessadev\x1b\\Buy me a coffee\x1b]8;;\x1b\\') +
|
|
272
|
+
themeColors.dim(' β’ ') +
|
|
269
273
|
'Esc to close'
|
|
270
274
|
)
|
|
271
275
|
|
|
@@ -280,7 +284,7 @@ export function createOverlayRenderers(state, deps) {
|
|
|
280
284
|
const { visible, offset } = sliceOverlayLines(lines, state.settingsScrollOffset, state.terminalRows)
|
|
281
285
|
state.settingsScrollOffset = offset
|
|
282
286
|
|
|
283
|
-
const tintedLines = tintOverlayLines(visible,
|
|
287
|
+
const tintedLines = tintOverlayLines(visible, themeColors.overlayBgSettings, state.terminalCols)
|
|
284
288
|
const cleared = tintedLines.map(l => l + EL)
|
|
285
289
|
return cleared.join('\n')
|
|
286
290
|
}
|
|
@@ -325,37 +329,36 @@ export function createOverlayRenderers(state, deps) {
|
|
|
325
329
|
|
|
326
330
|
lines.push('')
|
|
327
331
|
// π Branding header
|
|
328
|
-
lines.push(` ${
|
|
329
|
-
lines.push(` ${
|
|
332
|
+
lines.push(` ${themeColors.accent('π')} ${themeColors.accentBold('free-coding-models')} ${themeColors.dim(`v${LOCAL_VERSION}`)}`)
|
|
333
|
+
lines.push(` ${themeColors.textBold('π Install Endpoints')}`)
|
|
330
334
|
lines.push('')
|
|
331
|
-
lines.push(
|
|
335
|
+
lines.push(themeColors.dim(' β install provider catalogs into supported coding tools'))
|
|
332
336
|
if (state.installEndpointsErrorMsg) {
|
|
333
|
-
lines.push(` ${
|
|
337
|
+
lines.push(` ${themeColors.warning(state.installEndpointsErrorMsg)}`)
|
|
334
338
|
}
|
|
335
339
|
lines.push('')
|
|
336
340
|
|
|
337
341
|
if (state.installEndpointsPhase === 'providers') {
|
|
338
|
-
lines.push(` ${
|
|
342
|
+
lines.push(` ${themeColors.textBold(`Step 1/${totalSteps}`)} ${themeColors.info('Choose a configured provider')}`)
|
|
339
343
|
lines.push('')
|
|
340
344
|
|
|
341
345
|
if (providerChoices.length === 0) {
|
|
342
|
-
lines.push(
|
|
343
|
-
lines.push(
|
|
346
|
+
lines.push(themeColors.dim(' No configured providers can be installed directly right now.'))
|
|
347
|
+
lines.push(themeColors.dim(' Add an API key in Settings (`P`) first, then reopen this screen.'))
|
|
344
348
|
} else {
|
|
345
349
|
providerChoices.forEach((provider, idx) => {
|
|
346
350
|
const isCursor = idx === state.installEndpointsCursor
|
|
347
|
-
const
|
|
348
|
-
const row = `${bullet}${chalk.bold(provider.label.padEnd(24))} ${chalk.dim(`${provider.modelCount} models`)}`
|
|
351
|
+
const row = `${bullet(isCursor)}${themeColors.textBold(provider.label.padEnd(24))} ${themeColors.dim(`${provider.modelCount} models`)}`
|
|
349
352
|
cursorLineByRow[idx] = lines.length
|
|
350
|
-
lines.push(isCursor ?
|
|
353
|
+
lines.push(isCursor ? themeColors.bgCursorInstall(row) : row)
|
|
351
354
|
})
|
|
352
355
|
}
|
|
353
356
|
|
|
354
357
|
lines.push('')
|
|
355
|
-
lines.push(
|
|
358
|
+
lines.push(themeColors.dim(' ββ Navigate β’ Enter Choose provider β’ Esc Close'))
|
|
356
359
|
} else if (state.installEndpointsPhase === 'tools') {
|
|
357
|
-
lines.push(` ${
|
|
358
|
-
lines.push(
|
|
360
|
+
lines.push(` ${themeColors.textBold(`Step 2/${totalSteps}`)} ${themeColors.info('Choose the target tool')}`)
|
|
361
|
+
lines.push(themeColors.dim(` Provider: ${selectedProviderLabel}`))
|
|
359
362
|
lines.push('')
|
|
360
363
|
|
|
361
364
|
// π Use getToolMeta for labels instead of hard-coded ternary chains
|
|
@@ -364,60 +367,57 @@ export function createOverlayRenderers(state, deps) {
|
|
|
364
367
|
const meta = getToolMeta(toolMode)
|
|
365
368
|
const label = `${meta.emoji} ${meta.label}`
|
|
366
369
|
const note = toolMode.startsWith('opencode')
|
|
367
|
-
?
|
|
370
|
+
? themeColors.dim('shared config file')
|
|
368
371
|
: toolMode === 'openhands'
|
|
369
|
-
?
|
|
370
|
-
:
|
|
371
|
-
const
|
|
372
|
-
const row = `${bullet}${chalk.bold(label.padEnd(26))} ${note}`
|
|
372
|
+
? themeColors.dim('env file (~/.fcm-*-env)')
|
|
373
|
+
: themeColors.dim('managed config install')
|
|
374
|
+
const row = `${bullet(isCursor)}${themeColors.textBold(label.padEnd(26))} ${note}`
|
|
373
375
|
cursorLineByRow[idx] = lines.length
|
|
374
|
-
lines.push(isCursor ?
|
|
376
|
+
lines.push(isCursor ? themeColors.bgCursorInstall(row) : row)
|
|
375
377
|
})
|
|
376
378
|
|
|
377
379
|
lines.push('')
|
|
378
|
-
lines.push(
|
|
380
|
+
lines.push(themeColors.dim(' ββ Navigate β’ Enter Choose tool β’ Esc Back'))
|
|
379
381
|
} else if (state.installEndpointsPhase === 'scope') {
|
|
380
|
-
lines.push(` ${
|
|
381
|
-
lines.push(
|
|
382
|
+
lines.push(` ${themeColors.textBold(`Step 3/${totalSteps}`)} ${themeColors.info('Choose the install scope')}`)
|
|
383
|
+
lines.push(themeColors.dim(` Provider: ${selectedProviderLabel} β’ Tool: ${selectedToolLabel} β’ ${selectedConnectionLabel}`))
|
|
382
384
|
lines.push('')
|
|
383
385
|
|
|
384
386
|
scopeChoices.forEach((scope, idx) => {
|
|
385
387
|
const isCursor = idx === state.installEndpointsCursor
|
|
386
|
-
const
|
|
387
|
-
const row = `${bullet}${chalk.bold(scope.label)}`
|
|
388
|
+
const row = `${bullet(isCursor)}${themeColors.textBold(scope.label)}`
|
|
388
389
|
cursorLineByRow[idx] = lines.length
|
|
389
|
-
lines.push(isCursor ?
|
|
390
|
-
lines.push(
|
|
390
|
+
lines.push(isCursor ? themeColors.bgCursorInstall(row) : row)
|
|
391
|
+
lines.push(themeColors.dim(` ${scope.hint}`))
|
|
391
392
|
lines.push('')
|
|
392
393
|
})
|
|
393
394
|
|
|
394
|
-
lines.push(
|
|
395
|
+
lines.push(themeColors.dim(' Enter Continue β’ Esc Back'))
|
|
395
396
|
} else if (state.installEndpointsPhase === 'models') {
|
|
396
397
|
const models = getProviderCatalogModels(state.installEndpointsProviderKey)
|
|
397
398
|
const selectedCount = state.installEndpointsSelectedModelIds.size
|
|
398
399
|
|
|
399
|
-
lines.push(` ${
|
|
400
|
-
lines.push(
|
|
401
|
-
lines.push(
|
|
400
|
+
lines.push(` ${themeColors.textBold(`Step 4/${totalSteps}`)} ${themeColors.info('Choose which models to install')}`)
|
|
401
|
+
lines.push(themeColors.dim(` Provider: ${selectedProviderLabel} β’ Tool: ${selectedToolLabel} β’ ${selectedConnectionLabel}`))
|
|
402
|
+
lines.push(themeColors.dim(` Selected: ${selectedCount}/${models.length}`))
|
|
402
403
|
lines.push('')
|
|
403
404
|
|
|
404
405
|
models.forEach((model, idx) => {
|
|
405
406
|
const isCursor = idx === state.installEndpointsCursor
|
|
406
407
|
const selected = state.installEndpointsSelectedModelIds.has(model.modelId)
|
|
407
|
-
const
|
|
408
|
-
const
|
|
409
|
-
const
|
|
410
|
-
const row = `${bullet}${checkbox} ${chalk.bold(model.label.padEnd(26))} ${tier} ${chalk.dim(model.ctx.padEnd(6))} ${chalk.dim(model.modelId)}`
|
|
408
|
+
const checkbox = selected ? themeColors.successBold('[β]') : themeColors.dim('[ ]')
|
|
409
|
+
const tier = themeColors.info(model.tier.padEnd(2))
|
|
410
|
+
const row = `${bullet(isCursor)}${checkbox} ${themeColors.textBold(model.label.padEnd(26))} ${tier} ${themeColors.dim(model.ctx.padEnd(6))} ${themeColors.dim(model.modelId)}`
|
|
411
411
|
cursorLineByRow[idx] = lines.length
|
|
412
|
-
lines.push(isCursor ?
|
|
412
|
+
lines.push(isCursor ? themeColors.bgCursorInstall(row) : row)
|
|
413
413
|
})
|
|
414
414
|
|
|
415
415
|
lines.push('')
|
|
416
|
-
lines.push(
|
|
416
|
+
lines.push(themeColors.dim(' ββ Navigate β’ Space Toggle model β’ A All/None β’ Enter Install β’ Esc Back'))
|
|
417
417
|
} else if (state.installEndpointsPhase === 'result') {
|
|
418
418
|
const result = state.installEndpointsResult
|
|
419
|
-
const accent = result?.type === 'success' ?
|
|
420
|
-
lines.push(` ${
|
|
419
|
+
const accent = result?.type === 'success' ? themeColors.successBold : themeColors.errorBold
|
|
420
|
+
lines.push(` ${themeColors.textBold('Result')} ${accent(result?.title || 'Install result unavailable')}`)
|
|
421
421
|
lines.push('')
|
|
422
422
|
|
|
423
423
|
for (const detail of result?.lines || []) {
|
|
@@ -426,14 +426,100 @@ export function createOverlayRenderers(state, deps) {
|
|
|
426
426
|
|
|
427
427
|
if (result?.type === 'success') {
|
|
428
428
|
lines.push('')
|
|
429
|
-
lines.push(
|
|
429
|
+
lines.push(themeColors.dim(' Future FCM launches will refresh this catalog automatically when the provider list evolves.'))
|
|
430
430
|
}
|
|
431
431
|
|
|
432
432
|
lines.push('')
|
|
433
|
-
lines.push(
|
|
433
|
+
lines.push(themeColors.dim(' Enter or Esc Close'))
|
|
434
434
|
}
|
|
435
435
|
|
|
436
436
|
const targetLine = cursorLineByRow[state.installEndpointsCursor] ?? 0
|
|
437
|
+
state.toolInstallPromptScrollOffset = keepOverlayTargetVisible(
|
|
438
|
+
state.toolInstallPromptScrollOffset,
|
|
439
|
+
targetLine,
|
|
440
|
+
lines.length,
|
|
441
|
+
state.terminalRows
|
|
442
|
+
)
|
|
443
|
+
const { visible, offset } = sliceOverlayLines(lines, state.toolInstallPromptScrollOffset, state.terminalRows)
|
|
444
|
+
state.toolInstallPromptScrollOffset = offset
|
|
445
|
+
|
|
446
|
+
const tintedLines = tintOverlayLines(visible, themeColors.overlayBgSettings, state.terminalCols)
|
|
447
|
+
const cleared = tintedLines.map((line) => line + EL)
|
|
448
|
+
return cleared.join('\n')
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// βββ Missing-tool install confirmation overlay ββββββββββββββββββββββββββββ
|
|
452
|
+
// π renderToolInstallPrompt keeps the user inside the TUI long enough to
|
|
453
|
+
// π confirm the auto-install, then the key handler exits the alt screen and
|
|
454
|
+
// π runs the official installer before retrying the selected launch.
|
|
455
|
+
function renderToolInstallPrompt() {
|
|
456
|
+
const EL = '\x1b[K'
|
|
457
|
+
const lines = []
|
|
458
|
+
const cursorLineByRow = {}
|
|
459
|
+
const installPlan = state.toolInstallPromptPlan || getToolInstallPlan(state.toolInstallPromptMode)
|
|
460
|
+
const toolMeta = state.toolInstallPromptMode ? getToolMeta(state.toolInstallPromptMode) : null
|
|
461
|
+
const selectedModel = state.toolInstallPromptModel
|
|
462
|
+
const options = [
|
|
463
|
+
{
|
|
464
|
+
label: 'Yes, install it now',
|
|
465
|
+
hint: installPlan?.summary || 'Run the official installer, then continue with the selected model.',
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
label: 'No, go back',
|
|
469
|
+
hint: 'Return to the model list without installing anything.',
|
|
470
|
+
},
|
|
471
|
+
]
|
|
472
|
+
|
|
473
|
+
lines.push(` ${chalk.cyanBright('π')} ${chalk.bold.cyanBright('free-coding-models')}`)
|
|
474
|
+
lines.push(` ${chalk.bold('π¦ Missing Tool')}`)
|
|
475
|
+
lines.push('')
|
|
476
|
+
|
|
477
|
+
if (!toolMeta || !installPlan) {
|
|
478
|
+
lines.push(chalk.red(' No install metadata is available for the selected tool.'))
|
|
479
|
+
lines.push('')
|
|
480
|
+
lines.push(chalk.dim(' Esc Close'))
|
|
481
|
+
} else {
|
|
482
|
+
const title = `${toolMeta.emoji} ${toolMeta.label}`
|
|
483
|
+
lines.push(` ${chalk.bold(title)} is not installed on this machine.`)
|
|
484
|
+
lines.push(chalk.dim(` Selected model: ${selectedModel?.label || 'Unknown model'}`))
|
|
485
|
+
lines.push('')
|
|
486
|
+
|
|
487
|
+
if (!installPlan.supported) {
|
|
488
|
+
lines.push(chalk.yellow(` ${installPlan.reason || 'FCM cannot auto-install this tool on the current platform.'}`))
|
|
489
|
+
if (installPlan.docsUrl) {
|
|
490
|
+
lines.push(chalk.dim(` Docs: ${installPlan.docsUrl}`))
|
|
491
|
+
}
|
|
492
|
+
lines.push('')
|
|
493
|
+
lines.push(chalk.dim(' Enter or Esc Close'))
|
|
494
|
+
} else {
|
|
495
|
+
lines.push(chalk.dim(` Command: ${installPlan.shellCommand}`))
|
|
496
|
+
if (installPlan.note) {
|
|
497
|
+
lines.push(chalk.dim(` Note: ${installPlan.note}`))
|
|
498
|
+
}
|
|
499
|
+
if (installPlan.docsUrl) {
|
|
500
|
+
lines.push(chalk.dim(` Docs: ${installPlan.docsUrl}`))
|
|
501
|
+
}
|
|
502
|
+
if (state.toolInstallPromptErrorMsg) {
|
|
503
|
+
lines.push('')
|
|
504
|
+
lines.push(chalk.yellow(` ${state.toolInstallPromptErrorMsg}`))
|
|
505
|
+
}
|
|
506
|
+
lines.push('')
|
|
507
|
+
|
|
508
|
+
options.forEach((option, idx) => {
|
|
509
|
+
const isCursor = idx === state.toolInstallPromptCursor
|
|
510
|
+
const bullet = isCursor ? chalk.bold.cyan(' β― ') : chalk.dim(' ')
|
|
511
|
+
const row = `${bullet}${chalk.bold(option.label)}`
|
|
512
|
+
cursorLineByRow[idx] = lines.length
|
|
513
|
+
lines.push(isCursor ? themeColors.bgCursorInstall(row) : row)
|
|
514
|
+
lines.push(chalk.dim(` ${option.hint}`))
|
|
515
|
+
lines.push('')
|
|
516
|
+
})
|
|
517
|
+
|
|
518
|
+
lines.push(chalk.dim(' ββ Navigate β’ Enter Confirm β’ Esc Cancel'))
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const targetLine = cursorLineByRow[state.toolInstallPromptCursor] ?? 0
|
|
437
523
|
state.installEndpointsScrollOffset = keepOverlayTargetVisible(
|
|
438
524
|
state.installEndpointsScrollOffset,
|
|
439
525
|
targetLine,
|
|
@@ -443,7 +529,7 @@ export function createOverlayRenderers(state, deps) {
|
|
|
443
529
|
const { visible, offset } = sliceOverlayLines(lines, state.installEndpointsScrollOffset, state.terminalRows)
|
|
444
530
|
state.installEndpointsScrollOffset = offset
|
|
445
531
|
|
|
446
|
-
const tintedLines = tintOverlayLines(visible,
|
|
532
|
+
const tintedLines = tintOverlayLines(visible, themeColors.overlayBgSettings, state.terminalCols)
|
|
447
533
|
const cleared = tintedLines.map((line) => line + EL)
|
|
448
534
|
return cleared.join('\n')
|
|
449
535
|
}
|
|
@@ -454,92 +540,99 @@ export function createOverlayRenderers(state, deps) {
|
|
|
454
540
|
function renderHelp() {
|
|
455
541
|
const EL = '\x1b[K'
|
|
456
542
|
const lines = []
|
|
543
|
+
const label = themeColors.info
|
|
544
|
+
const hint = themeColors.dim
|
|
545
|
+
const key = themeColors.hotkey
|
|
546
|
+
const heading = themeColors.textBold
|
|
457
547
|
|
|
458
548
|
// π Branding header
|
|
459
|
-
lines.push(` ${
|
|
460
|
-
lines.push(` ${
|
|
549
|
+
lines.push(` ${themeColors.accent('π')} ${themeColors.accentBold('free-coding-models')} ${themeColors.dim(`v${LOCAL_VERSION}`)}`)
|
|
550
|
+
lines.push(` ${heading('β Help & Keyboard Shortcuts')}`)
|
|
461
551
|
lines.push('')
|
|
462
|
-
lines.push(` ${
|
|
463
|
-
lines.push(` ${
|
|
552
|
+
lines.push(` ${hint('β ββ / PgUp / PgDn / Home / End scroll β’ K or Esc close')}`)
|
|
553
|
+
lines.push(` ${heading('Columns')}`)
|
|
464
554
|
lines.push('')
|
|
465
|
-
lines.push(` ${
|
|
466
|
-
lines.push(` ${
|
|
555
|
+
lines.push(` ${label('Rank')} SWE-bench rank (1 = best coding score) ${hint('Sort:')} ${key('R')}`)
|
|
556
|
+
lines.push(` ${hint('Quick glance at which model is objectively the best coder right now.')}`)
|
|
467
557
|
lines.push('')
|
|
468
|
-
lines.push(` ${
|
|
469
|
-
lines.push(` ${
|
|
558
|
+
lines.push(` ${label('Tier')} S+ / S / A+ / A / A- / B+ / B / C based on SWE-bench score ${hint('Cycle:')} ${key('T')}`)
|
|
559
|
+
lines.push(` ${hint('Skip the noise β S/S+ models solve real GitHub issues, C models are for light tasks.')}`)
|
|
470
560
|
lines.push('')
|
|
471
|
-
lines.push(` ${
|
|
472
|
-
lines.push(` ${
|
|
561
|
+
lines.push(` ${label('SWE%')} SWE-bench score β coding ability benchmark (color-coded) ${hint('Sort:')} ${key('S')}`)
|
|
562
|
+
lines.push(` ${hint('The raw number behind the tier. Higher = better at writing, fixing, and refactoring code.')}`)
|
|
473
563
|
lines.push('')
|
|
474
|
-
lines.push(` ${
|
|
475
|
-
lines.push(` ${
|
|
564
|
+
lines.push(` ${label('CTX')} Context window size (128k, 200k, 256k, 1m, etc.) ${hint('Sort:')} ${key('C')}`)
|
|
565
|
+
lines.push(` ${hint('Bigger context = the model can read more of your codebase at once without forgetting.')}`)
|
|
476
566
|
lines.push('')
|
|
477
|
-
lines.push(` ${
|
|
478
|
-
lines.push(` ${
|
|
567
|
+
lines.push(` ${label('Model')} Model name (β = favorited, pinned at top) ${hint('Sort:')} ${key('M')} ${hint('Favorite:')} ${key('F')}`)
|
|
568
|
+
lines.push(` ${hint('Star the ones you like β they stay pinned at the top across restarts.')}`)
|
|
479
569
|
lines.push('')
|
|
480
|
-
lines.push(` ${
|
|
481
|
-
lines.push(` ${
|
|
570
|
+
lines.push(` ${label('Provider')} Provider source (NIM, Groq, Cerebras, etc.) ${hint('Sort:')} ${key('O')} ${hint('Cycle:')} ${key('D')}`)
|
|
571
|
+
lines.push(` ${hint('Same model on different providers can have very different speed and uptime.')}`)
|
|
482
572
|
lines.push('')
|
|
483
|
-
lines.push(` ${
|
|
484
|
-
lines.push(` ${
|
|
573
|
+
lines.push(` ${label('Latest')} Most recent ping response time (ms) ${hint('Sort:')} ${key('L')}`)
|
|
574
|
+
lines.push(` ${hint('Shows how fast the server is responding right now β useful to catch live slowdowns.')}`)
|
|
485
575
|
lines.push('')
|
|
486
|
-
lines.push(` ${
|
|
487
|
-
lines.push(` ${
|
|
576
|
+
lines.push(` ${label('Avg Ping')} Average response time across all measurable pings (200 + 401) (ms) ${hint('Sort:')} ${key('A')}`)
|
|
577
|
+
lines.push(` ${hint('The long-term truth. Even without a key, a 401 still gives real latency so the average stays useful.')}`)
|
|
488
578
|
lines.push('')
|
|
489
|
-
lines.push(` ${
|
|
490
|
-
lines.push(` ${
|
|
579
|
+
lines.push(` ${label('Health')} Live status: β
UP / π₯ 429 / β³ TIMEOUT / β ERR / π NO KEY ${hint('Sort:')} ${key('H')}`)
|
|
580
|
+
lines.push(` ${hint('Tells you instantly if a model is reachable or down β no guesswork needed.')}`)
|
|
491
581
|
lines.push('')
|
|
492
|
-
lines.push(` ${
|
|
493
|
-
lines.push(` ${
|
|
582
|
+
lines.push(` ${label('Verdict')} Overall assessment: Perfect / Normal / Spiky / Slow / Overloaded ${hint('Sort:')} ${key('V')}`)
|
|
583
|
+
lines.push(` ${hint('One-word summary so you don\'t have to cross-check speed, health, and stability yourself.')}`)
|
|
494
584
|
lines.push('')
|
|
495
|
-
lines.push(` ${
|
|
496
|
-
lines.push(` ${
|
|
585
|
+
lines.push(` ${label('Stability')} Composite 0β100 score: p95 + jitter + spike rate + uptime ${hint('Sort:')} ${key('B')}`)
|
|
586
|
+
lines.push(` ${hint('A fast model that randomly freezes is worse than a steady one. This catches that.')}`)
|
|
497
587
|
lines.push('')
|
|
498
|
-
lines.push(` ${
|
|
499
|
-
lines.push(` ${
|
|
588
|
+
lines.push(` ${label('Up%')} Uptime β ratio of successful pings to total pings ${hint('Sort:')} ${key('U')}`)
|
|
589
|
+
lines.push(` ${hint('If a model only works half the time, you\'ll waste time retrying. Higher = more reliable.')}`)
|
|
500
590
|
lines.push('')
|
|
501
|
-
lines.push(` ${
|
|
502
|
-
lines.push(` ${
|
|
591
|
+
lines.push(` ${label('Used')} Historical prompt+completion tokens tracked for this exact provider/model pair`)
|
|
592
|
+
lines.push(` ${hint('Loaded from local stats snapshots. Displayed in K tokens, or M tokens above one million.')}`)
|
|
503
593
|
lines.push('')
|
|
504
594
|
|
|
505
595
|
|
|
506
596
|
lines.push('')
|
|
507
|
-
lines.push(` ${
|
|
508
|
-
lines.push(` ${
|
|
509
|
-
lines.push(` ${
|
|
510
|
-
lines.push(` ${
|
|
597
|
+
lines.push(` ${heading('Main TUI')}`)
|
|
598
|
+
lines.push(` ${heading('Navigation')}`)
|
|
599
|
+
lines.push(` ${key('ββ')} Navigate rows`)
|
|
600
|
+
lines.push(` ${key('Enter')} Select model and launch`)
|
|
601
|
+
lines.push(` ${hint('If the active CLI is missing, FCM offers a one-click install prompt first.')}`)
|
|
511
602
|
lines.push('')
|
|
512
|
-
lines.push(` ${
|
|
513
|
-
lines.push(` ${
|
|
514
|
-
lines.push(` ${
|
|
515
|
-
lines.push(` ${
|
|
516
|
-
lines.push(` ${
|
|
517
|
-
lines.push(` ${
|
|
518
|
-
lines.push(` ${
|
|
519
|
-
lines.push(` ${
|
|
520
|
-
lines.push(` ${
|
|
603
|
+
lines.push(` ${heading('Controls')}`)
|
|
604
|
+
lines.push(` ${key('W')} Toggle ping mode ${hint('(speed 2s β normal 10s β slow 30s β forced 4s)')}`)
|
|
605
|
+
lines.push(` ${key('E')} Toggle configured models only ${hint('(enabled by default)')}`)
|
|
606
|
+
lines.push(` ${key('Z')} Cycle tool mode ${hint('(OpenCode β Desktop β OpenClaw β Crush β Goose β Pi β Aider β Qwen β OpenHands β Amp)')}`)
|
|
607
|
+
lines.push(` ${key('F')} Toggle favorite on selected row ${hint('(β pinned at top, persisted)')}`)
|
|
608
|
+
lines.push(` ${key('Y')} Install endpoints ${hint('(provider catalog β compatible tools, direct provider only)')}`)
|
|
609
|
+
lines.push(` ${key('Q')} Smart Recommend ${hint('(π― find the best model for your task β questionnaire + live analysis)')}`)
|
|
610
|
+
lines.push(` ${key('G')} Cycle theme ${hint('(auto β dark β light)')}`)
|
|
611
|
+
lines.push(` ${themeColors.errorBold('I')} Feedback, bugs & requests ${hint('(π send anonymous feedback, bug reports, or feature requests)')}`)
|
|
612
|
+
lines.push(` ${key('P')} Open settings ${hint('(manage API keys, provider toggles, updates, legacy cleanup)')}`)
|
|
521
613
|
// π Profile system removed - API keys now persist permanently across all sessions
|
|
522
|
-
lines.push(` ${
|
|
523
|
-
lines.push(` ${
|
|
524
|
-
lines.push(` ${
|
|
525
|
-
lines.push(` ${
|
|
526
|
-
lines.push('')
|
|
527
|
-
lines.push(` ${
|
|
528
|
-
lines.push(` ${
|
|
529
|
-
lines.push(` ${
|
|
530
|
-
lines.push(` ${
|
|
531
|
-
lines.push(` ${
|
|
532
|
-
lines.push(` ${
|
|
533
|
-
lines.push(` ${
|
|
534
|
-
lines.push(` ${
|
|
535
|
-
lines.push(` ${
|
|
614
|
+
lines.push(` ${key('Shift+R')} Reset view settings ${hint('(tier filter, sort, provider filter β defaults)')}`)
|
|
615
|
+
lines.push(` ${key('N')} Changelog ${hint('(π browse all versions, Enter to view details)')}`)
|
|
616
|
+
lines.push(` ${key('K')} / ${key('Esc')} Show/hide this help`)
|
|
617
|
+
lines.push(` ${key('Ctrl+C')} Exit`)
|
|
618
|
+
lines.push('')
|
|
619
|
+
lines.push(` ${heading('Settings (P)')}`)
|
|
620
|
+
lines.push(` ${key('ββ')} Navigate rows`)
|
|
621
|
+
lines.push(` ${key('PgUp/PgDn')} Jump by page`)
|
|
622
|
+
lines.push(` ${key('Home/End')} Jump first/last row`)
|
|
623
|
+
lines.push(` ${key('Enter')} Edit key / run selected maintenance action`)
|
|
624
|
+
lines.push(` ${key('Space')} Toggle provider enable/disable`)
|
|
625
|
+
lines.push(` ${key('T')} Test selected provider key`)
|
|
626
|
+
lines.push(` ${key('U')} Check updates manually`)
|
|
627
|
+
lines.push(` ${key('G')} Cycle theme globally`)
|
|
628
|
+
lines.push(` ${key('Esc')} Close settings`)
|
|
536
629
|
lines.push('')
|
|
537
630
|
lines.push(...buildCliHelpLines({ chalk, indent: ' ', title: 'CLI Flags' }))
|
|
538
631
|
lines.push('')
|
|
539
632
|
// π Help overlay can be longer than viewport, so keep a dedicated scroll offset.
|
|
540
633
|
const { visible, offset } = sliceOverlayLines(lines, state.helpScrollOffset, state.terminalRows)
|
|
541
634
|
state.helpScrollOffset = offset
|
|
542
|
-
const tintedLines = tintOverlayLines(visible,
|
|
635
|
+
const tintedLines = tintOverlayLines(visible, themeColors.overlayBgHelp, state.terminalCols)
|
|
543
636
|
const cleared = tintedLines.map(l => l + EL)
|
|
544
637
|
return cleared.join('\n')
|
|
545
638
|
}
|
|
@@ -554,10 +647,10 @@ export function createOverlayRenderers(state, deps) {
|
|
|
554
647
|
|
|
555
648
|
// π Branding header
|
|
556
649
|
lines.push('')
|
|
557
|
-
lines.push(` ${
|
|
558
|
-
lines.push(` ${
|
|
650
|
+
lines.push(` ${themeColors.accent('π')} ${themeColors.accentBold('free-coding-models')} ${themeColors.dim(`v${LOCAL_VERSION}`)}`)
|
|
651
|
+
lines.push(` ${themeColors.textBold('π― Smart Recommend')}`)
|
|
559
652
|
lines.push('')
|
|
560
|
-
lines.push(
|
|
653
|
+
lines.push(themeColors.dim(' β find the best model for your task'))
|
|
561
654
|
lines.push('')
|
|
562
655
|
|
|
563
656
|
if (state.recommendPhase === 'questionnaire') {
|
|
@@ -590,7 +683,7 @@ export function createOverlayRenderers(state, deps) {
|
|
|
590
683
|
const answered = state.recommendAnswers[questions[i].answerKey]
|
|
591
684
|
if (i < state.recommendQuestion && answered) {
|
|
592
685
|
const answeredLabel = questions[i].options.find(o => o.key === answered)?.label || answered
|
|
593
|
-
breadcrumbs +=
|
|
686
|
+
breadcrumbs += themeColors.successBold(` β ${questions[i].title} ${themeColors.textBold(answeredLabel)}`) + '\n'
|
|
594
687
|
}
|
|
595
688
|
}
|
|
596
689
|
if (breadcrumbs) {
|
|
@@ -598,19 +691,18 @@ export function createOverlayRenderers(state, deps) {
|
|
|
598
691
|
lines.push('')
|
|
599
692
|
}
|
|
600
693
|
|
|
601
|
-
lines.push(` ${
|
|
694
|
+
lines.push(` ${themeColors.textBold(`Question ${qNum}/${qTotal}:`)} ${themeColors.info(q.title)}`)
|
|
602
695
|
lines.push('')
|
|
603
696
|
|
|
604
697
|
for (let i = 0; i < q.options.length; i++) {
|
|
605
698
|
const opt = q.options[i]
|
|
606
699
|
const isCursor = i === state.recommendCursor
|
|
607
|
-
const
|
|
608
|
-
|
|
609
|
-
lines.push(`${bullet}${label}`)
|
|
700
|
+
const label = isCursor ? themeColors.textBold(opt.label) : themeColors.text(opt.label)
|
|
701
|
+
lines.push(`${bullet(isCursor)}${label}`)
|
|
610
702
|
}
|
|
611
703
|
|
|
612
704
|
lines.push('')
|
|
613
|
-
lines.push(
|
|
705
|
+
lines.push(themeColors.dim(' ββ navigate β’ Enter select β’ Esc cancel'))
|
|
614
706
|
|
|
615
707
|
} else if (state.recommendPhase === 'analyzing') {
|
|
616
708
|
// π Loading screen with progress bar
|
|
@@ -618,38 +710,38 @@ export function createOverlayRenderers(state, deps) {
|
|
|
618
710
|
const barWidth = 40
|
|
619
711
|
const filled = Math.round(barWidth * pct / 100)
|
|
620
712
|
const empty = barWidth - filled
|
|
621
|
-
const bar =
|
|
713
|
+
const bar = themeColors.successBold('β'.repeat(filled)) + themeColors.dim('β'.repeat(empty))
|
|
622
714
|
|
|
623
|
-
lines.push(` ${
|
|
715
|
+
lines.push(` ${themeColors.textBold('Analyzing models...')}`)
|
|
624
716
|
lines.push('')
|
|
625
|
-
lines.push(` ${bar} ${
|
|
717
|
+
lines.push(` ${bar} ${themeColors.textBold(String(pct) + '%')}`)
|
|
626
718
|
lines.push('')
|
|
627
719
|
|
|
628
720
|
// π Show what we're doing
|
|
629
721
|
const taskLabel = TASK_TYPES[state.recommendAnswers.taskType]?.label || 'β'
|
|
630
722
|
const prioLabel = PRIORITY_TYPES[state.recommendAnswers.priority]?.label || 'β'
|
|
631
723
|
const ctxLabel = CONTEXT_BUDGETS[state.recommendAnswers.contextBudget]?.label || 'β'
|
|
632
|
-
lines.push(
|
|
724
|
+
lines.push(themeColors.dim(` Task: ${taskLabel} β’ Priority: ${prioLabel} β’ Context: ${ctxLabel}`))
|
|
633
725
|
lines.push('')
|
|
634
726
|
|
|
635
727
|
// π Spinning indicator
|
|
636
728
|
const spinIdx = state.frame % FRAMES.length
|
|
637
|
-
lines.push(` ${
|
|
729
|
+
lines.push(` ${themeColors.warning(FRAMES[spinIdx])} Pinging models at 2 pings/sec to gather fresh latency data...`)
|
|
638
730
|
lines.push('')
|
|
639
|
-
lines.push(
|
|
731
|
+
lines.push(themeColors.dim(' Esc to cancel'))
|
|
640
732
|
|
|
641
733
|
} else if (state.recommendPhase === 'results') {
|
|
642
734
|
// π Show Top 3 results with detailed info
|
|
643
735
|
const taskLabel = TASK_TYPES[state.recommendAnswers.taskType]?.label || 'β'
|
|
644
736
|
const prioLabel = PRIORITY_TYPES[state.recommendAnswers.priority]?.label || 'β'
|
|
645
737
|
const ctxLabel = CONTEXT_BUDGETS[state.recommendAnswers.contextBudget]?.label || 'β'
|
|
646
|
-
lines.push(
|
|
738
|
+
lines.push(themeColors.dim(` Task: ${taskLabel} β’ Priority: ${prioLabel} β’ Context: ${ctxLabel}`))
|
|
647
739
|
lines.push('')
|
|
648
740
|
|
|
649
741
|
if (state.recommendResults.length === 0) {
|
|
650
|
-
lines.push(` ${
|
|
742
|
+
lines.push(` ${themeColors.warning('No models could be scored. Try different criteria or wait for more pings.')}`)
|
|
651
743
|
} else {
|
|
652
|
-
lines.push(` ${
|
|
744
|
+
lines.push(` ${themeColors.textBold('Top Recommendations:')}`)
|
|
653
745
|
lines.push('')
|
|
654
746
|
|
|
655
747
|
for (let i = 0; i < state.recommendResults.length; i++) {
|
|
@@ -657,7 +749,7 @@ export function createOverlayRenderers(state, deps) {
|
|
|
657
749
|
const r = rec.result
|
|
658
750
|
const medal = i === 0 ? 'π₯' : i === 1 ? 'π₯' : 'π₯'
|
|
659
751
|
const providerName = sources[r.providerKey]?.name ?? r.providerKey
|
|
660
|
-
const tierFn = TIER_COLOR[r.tier] ?? (
|
|
752
|
+
const tierFn = TIER_COLOR[r.tier] ?? ((text) => themeColors.text(text))
|
|
661
753
|
const avg = getAvg(r)
|
|
662
754
|
const avgStr = avg === Infinity ? 'β' : Math.round(avg) + 'ms'
|
|
663
755
|
const sweStr = r.sweScore ?? 'β'
|
|
@@ -666,24 +758,24 @@ export function createOverlayRenderers(state, deps) {
|
|
|
666
758
|
const stabStr = stability === -1 ? 'β' : String(stability)
|
|
667
759
|
|
|
668
760
|
const isCursor = i === state.recommendCursor
|
|
669
|
-
const highlight = isCursor ?
|
|
761
|
+
const highlight = isCursor ? themeColors.bgCursor : (text) => text
|
|
670
762
|
|
|
671
|
-
lines.push(highlight(` ${medal} ${
|
|
672
|
-
lines.push(highlight(` Score: ${
|
|
763
|
+
lines.push(highlight(` ${medal} ${themeColors.textBold('#' + (i + 1))} ${themeColors.textBold(r.label)} ${themeColors.dim('(' + providerName + ')')}`))
|
|
764
|
+
lines.push(highlight(` Score: ${themeColors.successBold(String(rec.score) + '/100')} β Tier: ${tierFn(r.tier)} β SWE: ${themeColors.info(sweStr)} β Avg: ${themeColors.warning(avgStr)} β CTX: ${themeColors.info(ctxStr)} β Stability: ${themeColors.info(stabStr)}`))
|
|
673
765
|
lines.push('')
|
|
674
766
|
}
|
|
675
767
|
}
|
|
676
768
|
|
|
677
769
|
lines.push('')
|
|
678
|
-
lines.push(` ${
|
|
770
|
+
lines.push(` ${themeColors.dim('These models are now')} ${themeColors.successBold('highlighted')} ${themeColors.dim('and')} π― ${themeColors.dim('pinned in the main table.')}`)
|
|
679
771
|
lines.push('')
|
|
680
|
-
lines.push(
|
|
772
|
+
lines.push(themeColors.dim(' ββ navigate β’ Enter select & close β’ Esc close β’ Q new search'))
|
|
681
773
|
}
|
|
682
774
|
|
|
683
775
|
lines.push('')
|
|
684
776
|
const { visible, offset } = sliceOverlayLines(lines, state.recommendScrollOffset, state.terminalRows)
|
|
685
777
|
state.recommendScrollOffset = offset
|
|
686
|
-
const tintedLines = tintOverlayLines(visible,
|
|
778
|
+
const tintedLines = tintOverlayLines(visible, themeColors.overlayBgRecommend, state.terminalCols)
|
|
687
779
|
const cleared2 = tintedLines.map(l => l + EL)
|
|
688
780
|
return cleared2.join('\n')
|
|
689
781
|
}
|
|
@@ -784,34 +876,34 @@ export function createOverlayRenderers(state, deps) {
|
|
|
784
876
|
|
|
785
877
|
// π Branding header
|
|
786
878
|
lines.push('')
|
|
787
|
-
lines.push(` ${
|
|
788
|
-
lines.push(` ${
|
|
879
|
+
lines.push(` ${themeColors.accent('π')} ${themeColors.accentBold('free-coding-models')} ${themeColors.dim(`v${LOCAL_VERSION}`)}`)
|
|
880
|
+
lines.push(` ${themeColors.successBold('π Feedback, bugs & requests')}`)
|
|
789
881
|
lines.push('')
|
|
790
|
-
lines.push(
|
|
882
|
+
lines.push(themeColors.dim(" β don't hesitate to send us feedback, bug reports, or just your feeling about the app"))
|
|
791
883
|
lines.push('')
|
|
792
884
|
|
|
793
885
|
// π Status messages (if any)
|
|
794
886
|
if (state.bugReportStatus === 'sending') {
|
|
795
|
-
lines.push(` ${
|
|
887
|
+
lines.push(` ${themeColors.warning('β³ Sending...')}`)
|
|
796
888
|
lines.push('')
|
|
797
889
|
} else if (state.bugReportStatus === 'success') {
|
|
798
|
-
lines.push(` ${
|
|
890
|
+
lines.push(` ${themeColors.successBold('β
Successfully sent!')} ${themeColors.dim('Closing overlay in 3 seconds...')}`)
|
|
799
891
|
lines.push('')
|
|
800
|
-
lines.push(` ${
|
|
892
|
+
lines.push(` ${themeColors.dim('Thank you for your feedback! It has been sent to the project team.')}`)
|
|
801
893
|
lines.push('')
|
|
802
894
|
} else if (state.bugReportStatus === 'error') {
|
|
803
|
-
lines.push(` ${
|
|
804
|
-
lines.push(` ${
|
|
895
|
+
lines.push(` ${themeColors.error('β Error:')} ${themeColors.warning(state.bugReportError || 'Failed to send')}`)
|
|
896
|
+
lines.push(` ${themeColors.dim('Press Backspace to edit, or Esc to close')}`)
|
|
805
897
|
lines.push('')
|
|
806
898
|
} else {
|
|
807
|
-
lines.push(` ${
|
|
808
|
-
lines.push(` ${
|
|
899
|
+
lines.push(` ${themeColors.dim('Type your feedback below. Press Enter to send, Esc to cancel.')}`)
|
|
900
|
+
lines.push(` ${themeColors.dim('Your message will be sent anonymously to the project team.')}`)
|
|
809
901
|
lines.push('')
|
|
810
902
|
}
|
|
811
903
|
|
|
812
904
|
// π Simple input area β left-aligned, framed by horizontal lines
|
|
813
|
-
lines.push(` ${
|
|
814
|
-
lines.push(` ${
|
|
905
|
+
lines.push(` ${themeColors.info('Message')} (${state.bugReportBuffer.length}/500 chars)`)
|
|
906
|
+
lines.push(` ${themeColors.dim('β'.repeat(maxInputWidth))}`)
|
|
815
907
|
// π Input lines β left-aligned, or placeholder when empty
|
|
816
908
|
if (displayLines.length > 0) {
|
|
817
909
|
for (const line of displayLines) {
|
|
@@ -819,19 +911,18 @@ export function createOverlayRenderers(state, deps) {
|
|
|
819
911
|
}
|
|
820
912
|
// π Show cursor on last line
|
|
821
913
|
if (state.bugReportStatus === 'idle' || state.bugReportStatus === 'error') {
|
|
822
|
-
lines[lines.length - 1] +=
|
|
914
|
+
lines[lines.length - 1] += themeColors.accentBold('β')
|
|
823
915
|
}
|
|
824
916
|
} else {
|
|
825
|
-
const placeholderBR = state.bugReportStatus === 'idle' ? chalk.
|
|
826
|
-
lines.push(` ${placeholderBR}${
|
|
917
|
+
const placeholderBR = state.bugReportStatus === 'idle' ? chalk.italic.rgb(...getProviderRgb('googleai'))('Type your message here...') : ''
|
|
918
|
+
lines.push(` ${placeholderBR}${themeColors.accentBold('β')}`)
|
|
827
919
|
}
|
|
828
|
-
lines.push(` ${
|
|
920
|
+
lines.push(` ${themeColors.dim('β'.repeat(maxInputWidth))}`)
|
|
829
921
|
lines.push('')
|
|
830
|
-
lines.push(
|
|
922
|
+
lines.push(themeColors.dim(' Enter Send β’ Esc Cancel β’ Backspace Delete'))
|
|
831
923
|
|
|
832
924
|
// π Apply overlay tint and return
|
|
833
|
-
const
|
|
834
|
-
const tintedLines = tintOverlayLines(lines, BUG_REPORT_OVERLAY_BG, state.terminalCols)
|
|
925
|
+
const tintedLines = tintOverlayLines(lines, themeColors.overlayBgFeedback, state.terminalCols)
|
|
835
926
|
const cleared = tintedLines.map(l => l + EL)
|
|
836
927
|
return cleared.join('\n')
|
|
837
928
|
}
|
|
@@ -855,14 +946,14 @@ export function createOverlayRenderers(state, deps) {
|
|
|
855
946
|
})
|
|
856
947
|
|
|
857
948
|
// π Branding header
|
|
858
|
-
lines.push(` ${
|
|
949
|
+
lines.push(` ${themeColors.accent('π')} ${themeColors.accentBold('free-coding-models')} ${themeColors.dim(`v${LOCAL_VERSION}`)}`)
|
|
859
950
|
|
|
860
951
|
if (state.changelogPhase === 'index') {
|
|
861
952
|
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
862
953
|
// π INDEX PHASE: Show all versions with selection
|
|
863
954
|
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
864
|
-
lines.push(` ${
|
|
865
|
-
lines.push(` ${
|
|
955
|
+
lines.push(` ${themeColors.textBold('π Changelog - All Versions')}`)
|
|
956
|
+
lines.push(` ${themeColors.dim('β ββ navigate β’ Enter select β’ Esc close')}`)
|
|
866
957
|
lines.push('')
|
|
867
958
|
|
|
868
959
|
for (let i = 0; i < versionList.length; i++) {
|
|
@@ -898,22 +989,22 @@ export function createOverlayRenderers(state, deps) {
|
|
|
898
989
|
const prefix = ` v${version.padEnd(8)} β ${countStr}`
|
|
899
990
|
if (isSelected) {
|
|
900
991
|
const full = summary ? `${prefix} Β· ${summary}` : prefix
|
|
901
|
-
lines.push(
|
|
992
|
+
lines.push(themeColors.bgCursor(full))
|
|
902
993
|
} else {
|
|
903
|
-
const dimSummary = summary ?
|
|
994
|
+
const dimSummary = summary ? themeColors.dim(` Β· ${summary}`) : ''
|
|
904
995
|
lines.push(`${prefix}${dimSummary}`)
|
|
905
996
|
}
|
|
906
997
|
}
|
|
907
998
|
|
|
908
999
|
lines.push('')
|
|
909
|
-
lines.push(` ${
|
|
1000
|
+
lines.push(` ${themeColors.dim(`Total: ${versionList.length} versions`)}`)
|
|
910
1001
|
|
|
911
1002
|
} else if (state.changelogPhase === 'details') {
|
|
912
1003
|
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
913
1004
|
// π DETAILS PHASE: Show detailed changes for selected version
|
|
914
1005
|
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
915
|
-
lines.push(` ${
|
|
916
|
-
lines.push(` ${
|
|
1006
|
+
lines.push(` ${themeColors.textBold(`π v${state.changelogSelectedVersion}`)}`)
|
|
1007
|
+
lines.push(` ${themeColors.dim('β ββ / PgUp / PgDn scroll β’ B back β’ Esc close')}`)
|
|
917
1008
|
lines.push('')
|
|
918
1009
|
|
|
919
1010
|
const changes = versions[state.changelogSelectedVersion]
|
|
@@ -921,7 +1012,7 @@ export function createOverlayRenderers(state, deps) {
|
|
|
921
1012
|
const sections = { added: 'β¨ Added', fixed: 'π Fixed', changed: 'π Changed', updated: 'π Updated' }
|
|
922
1013
|
for (const [key, label] of Object.entries(sections)) {
|
|
923
1014
|
if (changes[key] && changes[key].length > 0) {
|
|
924
|
-
lines.push(` ${
|
|
1015
|
+
lines.push(` ${themeColors.warning(label)}`)
|
|
925
1016
|
for (const item of changes[key]) {
|
|
926
1017
|
// π Unwrap markdown bold/code markers for display
|
|
927
1018
|
let displayText = item.replace(/\*\*([^*]+)\*\*/g, '$1').replace(/`([^`]+)`/g, '$1')
|
|
@@ -950,10 +1041,9 @@ export function createOverlayRenderers(state, deps) {
|
|
|
950
1041
|
}
|
|
951
1042
|
|
|
952
1043
|
// π Use scrolling with overlay handler
|
|
953
|
-
const CHANGELOG_OVERLAY_BG = chalk.bgRgb(10, 40, 80) // Dark blue background
|
|
954
1044
|
const { visible, offset } = sliceOverlayLines(lines, state.changelogScrollOffset, state.terminalRows)
|
|
955
1045
|
state.changelogScrollOffset = offset
|
|
956
|
-
const tintedLines = tintOverlayLines(visible,
|
|
1046
|
+
const tintedLines = tintOverlayLines(visible, themeColors.overlayBgChangelog, state.terminalCols)
|
|
957
1047
|
const cleared = tintedLines.map(l => l + EL)
|
|
958
1048
|
return cleared.join('\n')
|
|
959
1049
|
}
|
|
@@ -967,6 +1057,7 @@ export function createOverlayRenderers(state, deps) {
|
|
|
967
1057
|
return {
|
|
968
1058
|
renderSettings,
|
|
969
1059
|
renderInstallEndpoints,
|
|
1060
|
+
renderToolInstallPrompt,
|
|
970
1061
|
renderHelp,
|
|
971
1062
|
renderRecommend,
|
|
972
1063
|
renderFeedback,
|