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