free-coding-models 0.3.17 β†’ 0.3.19

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