free-coding-models 0.3.16 β†’ 0.3.18

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