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