free-coding-models 0.5.95 β 0.5.97
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/README.md +15 -11
- package/bin/free-coding-models.js +2 -0
- package/changelog/v0.5.96.md +22 -0
- package/changelog/v0.5.97.md +24 -0
- package/package.json +1 -1
- package/sources.js +197 -91
- package/src/core/config.js +28 -3
- package/src/core/i18n/index.js +49 -0
- package/src/core/i18n/locales/en.js +991 -0
- package/src/core/i18n/locales/zh-CN.js +991 -0
- package/src/core/opencode.js +15 -8
- package/src/core/ping.js +31 -1
- package/src/core/playground.js +20 -19
- package/src/core/provider-key-tester.js +1 -0
- package/src/core/provider-metadata.js +11 -2
- package/src/core/quota-capabilities.js +2 -0
- package/src/core/router-daemon.js +58 -8
- package/src/core/router-dashboard.js +15 -14
- package/src/core/router-v2/tui-dashboard.js +27 -26
- package/src/core/shell-env.js +6 -0
- package/src/core/utils.js +22 -0
- package/src/tui/app.js +2 -0
- package/src/tui/cli-help.js +45 -21
- package/src/tui/command-palette.js +19 -1
- package/src/tui/key-handler.js +21 -1
- package/src/tui/overlays.js +113 -107
- package/src/tui/render-helpers.js +4 -3
- package/src/tui/render-table.js +137 -132
- package/src/tui/theme.js +2 -0
- package/web/dist/assets/index-C2lIYp0O.js +45 -0
- package/web/dist/assets/index-DZFdxy2N.css +1 -0
- package/web/dist/index.html +2 -2
- package/web/server.js +58 -6
- package/web/dist/assets/index-BOpGJzSO.js +0 -45
- package/web/dist/assets/index-CCaxIOti.css +0 -1
package/src/tui/overlays.js
CHANGED
|
@@ -26,6 +26,7 @@ import { renderPlayground as renderPlaygroundOverlay } from '../core/playground.
|
|
|
26
26
|
import { themeColors, getThemeStatusLabel, getProviderRgb } from './theme.js'
|
|
27
27
|
import { getProviderBillingNote, getProviderLabelWithBilling } from '../core/provider-metadata.js'
|
|
28
28
|
import { pickAccountIdFromSettings } from '../core/cloudflare-account.js'
|
|
29
|
+
import { t } from '../core/i18n/index.js'
|
|
29
30
|
import { detectTerminalCapabilities } from '../core/utils.js'
|
|
30
31
|
import { truncateAnsiWidth, loadChangelogCached } from './render-helpers.js'
|
|
31
32
|
import { COMMAND_PALETTE_MAX_RESULTS } from './command-palette.js'
|
|
@@ -72,22 +73,20 @@ export function createOverlayRenderers(state, deps) {
|
|
|
72
73
|
// π the overlay instead of turning into one truncated red line.
|
|
73
74
|
// π Uses 100% of terminal width minus padding for better readability.
|
|
74
75
|
const wrapPlainText = (text, width = null) => {
|
|
75
|
-
const effectiveWidth = width || (state.terminalCols - 16)
|
|
76
|
+
const effectiveWidth = Math.max(1, width || (state.terminalCols - 16))
|
|
76
77
|
const normalized = typeof text === 'string' ? text.trim() : ''
|
|
77
78
|
if (!normalized) return []
|
|
78
|
-
const words = normalized.split(/\s+/)
|
|
79
79
|
const lines = []
|
|
80
80
|
let current = ''
|
|
81
|
-
for (const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
current = word
|
|
81
|
+
for (const character of [...normalized]) {
|
|
82
|
+
if (displayWidth(current + character) > effectiveWidth && current) {
|
|
83
|
+
lines.push(current.trimEnd())
|
|
84
|
+
current = character.trimStart()
|
|
86
85
|
} else {
|
|
87
|
-
current
|
|
86
|
+
current += character
|
|
88
87
|
}
|
|
89
88
|
}
|
|
90
|
-
if (current) lines.push(current)
|
|
89
|
+
if (current.trim()) lines.push(current.trimEnd())
|
|
91
90
|
return lines
|
|
92
91
|
}
|
|
93
92
|
|
|
@@ -123,7 +122,8 @@ export function createOverlayRenderers(state, deps) {
|
|
|
123
122
|
const providerKeys = Object.keys(sources)
|
|
124
123
|
const updateRowIdx = providerKeys.length
|
|
125
124
|
const themeRowIdx = updateRowIdx + 1
|
|
126
|
-
const
|
|
125
|
+
const languageRowIdx = themeRowIdx + 1
|
|
126
|
+
const favoritesModeRowIdx = languageRowIdx + 1
|
|
127
127
|
const startupAiSpeedScanRowIdx = favoritesModeRowIdx + 1
|
|
128
128
|
const autoHideBrokenModelsRowIdx = startupAiSpeedScanRowIdx + 1
|
|
129
129
|
const cleanupLegacyProxyRowIdx = autoHideBrokenModelsRowIdx + 1
|
|
@@ -135,14 +135,14 @@ export function createOverlayRenderers(state, deps) {
|
|
|
135
135
|
|
|
136
136
|
// π Branding header
|
|
137
137
|
lines.push(` ${themeColors.accent('π')} ${themeColors.accentBold('free-coding-models')} ${themeColors.dim(`v${LOCAL_VERSION}`)}`)
|
|
138
|
-
lines.push(` ${themeColors.textBold(
|
|
138
|
+
lines.push(` ${themeColors.textBold(`β ${t('settings.title')}`)}`)
|
|
139
139
|
|
|
140
140
|
if (state.settingsErrorMsg) {
|
|
141
141
|
lines.push(` ${themeColors.errorBold(state.settingsErrorMsg)}`)
|
|
142
142
|
lines.push('')
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
lines.push(` ${themeColors.textBold(
|
|
145
|
+
lines.push(` ${themeColors.textBold(`π§© ${t('settings.providers')}`)}`)
|
|
146
146
|
// π Dynamic separator line using 100% terminal width
|
|
147
147
|
const separatorWidth = Math.max(20, state.terminalCols - 10)
|
|
148
148
|
lines.push(` ${themeColors.dim(' ' + 'β'.repeat(separatorWidth))}`)
|
|
@@ -174,25 +174,25 @@ export function createOverlayRenderers(state, deps) {
|
|
|
174
174
|
const extra = keyCount > 1 ? themeColors.info(` (+${keyCount - 1} more)`) : ''
|
|
175
175
|
keyDisplay = keyMasked + extra
|
|
176
176
|
} else {
|
|
177
|
-
keyDisplay = themeColors.dim(
|
|
177
|
+
keyDisplay = themeColors.dim(t('settings.noApiKeySet'))
|
|
178
178
|
}
|
|
179
179
|
|
|
180
180
|
// π Test result badge
|
|
181
181
|
const testResult = state.settingsTestResults[pk]
|
|
182
182
|
// π Default badge reflects configuration first: a saved key should look
|
|
183
183
|
// π ready to test even before the user has run the probe once.
|
|
184
|
-
let testBadge = keyCount > 0 ? themeColors.info('
|
|
185
|
-
if (testResult === 'pending') testBadge = themeColors.warning('
|
|
186
|
-
else if (testResult === 'ok') testBadge = themeColors.successBold('
|
|
187
|
-
else if (testResult === 'missing_key') testBadge = themeColors.dim('
|
|
188
|
-
else if (testResult === 'auth_error') testBadge = themeColors.error('
|
|
189
|
-
else if (testResult === 'rate_limited') testBadge = themeColors.warning('
|
|
190
|
-
else if (testResult === 'no_callable_model') testBadge = chalk.rgb(...getProviderRgb('openrouter'))('
|
|
191
|
-
else if (testResult === 'fail') testBadge = themeColors.error('
|
|
184
|
+
let testBadge = keyCount > 0 ? themeColors.info(t('settings.badge.test')) : themeColors.dim(t('settings.badge.missingKey'))
|
|
185
|
+
if (testResult === 'pending') testBadge = themeColors.warning(t('settings.badge.testing'))
|
|
186
|
+
else if (testResult === 'ok') testBadge = themeColors.successBold(t('settings.badge.testOk'))
|
|
187
|
+
else if (testResult === 'missing_key') testBadge = themeColors.dim(t('settings.badge.missingKey'))
|
|
188
|
+
else if (testResult === 'auth_error') testBadge = themeColors.error(t('settings.badge.authError'))
|
|
189
|
+
else if (testResult === 'rate_limited') testBadge = themeColors.warning(t('settings.badge.rateLimited'))
|
|
190
|
+
else if (testResult === 'no_callable_model') testBadge = chalk.rgb(...getProviderRgb('openrouter'))(t('settings.badge.noCallableModel'))
|
|
191
|
+
else if (testResult === 'fail') testBadge = themeColors.error(t('settings.badge.failed'))
|
|
192
192
|
// π No truncation of rate limits - overlay now uses 100% terminal width.
|
|
193
193
|
// π Paid/credits-required providers get an explicit money marker + parenthesized detail.
|
|
194
194
|
const billingNote = getProviderBillingNote(pk)
|
|
195
|
-
const rateSummary = themeColors.dim(`${meta.rateLimits || '
|
|
195
|
+
const rateSummary = themeColors.dim(`${meta.rateLimits || t('settings.noLimitInfo')}${billingNote ? ` ${billingNote}` : ''}`)
|
|
196
196
|
|
|
197
197
|
const enabledBadge = enabled ? themeColors.successBold('β
') : themeColors.errorBold('β')
|
|
198
198
|
// π Color provider names the same way as in the main table
|
|
@@ -211,17 +211,17 @@ export function createOverlayRenderers(state, deps) {
|
|
|
211
211
|
const selectedMeta = PROVIDER_METADATA[selectedProviderKey] || {}
|
|
212
212
|
if (selectedSource && state.settingsCursor < providerKeys.length) {
|
|
213
213
|
const selectedKey = getApiKey(state.config, selectedProviderKey)
|
|
214
|
-
const setupStatus = selectedKey ? themeColors.success('
|
|
214
|
+
const setupStatus = selectedKey ? themeColors.success(t('settings.keyDetected')) : themeColors.warning(t('settings.keyMissing'))
|
|
215
215
|
// π Color the provider name in the setup instructions header
|
|
216
216
|
const selectedProviderRgb = PROVIDER_COLOR[selectedProviderKey] ?? [105, 190, 245]
|
|
217
217
|
const selectedProviderLabel = getProviderLabelWithBilling(selectedProviderKey, selectedSource.name || selectedProviderKey)
|
|
218
218
|
const selectedBillingNote = getProviderBillingNote(selectedProviderKey)
|
|
219
219
|
const coloredProviderName = chalk.bold.rgb(...selectedProviderRgb)(selectedProviderLabel)
|
|
220
|
-
lines.push(` ${themeColors.textBold('
|
|
221
|
-
lines.push(themeColors.dim(` 1)
|
|
220
|
+
lines.push(` ${themeColors.textBold(t('settings.setupInstructions'))} β ${coloredProviderName}${selectedBillingNote ? ' ' + themeColors.warning(selectedBillingNote) : ''}`)
|
|
221
|
+
lines.push(themeColors.dim(` 1) ${t('settings.createProviderAccount')} (${selectedMeta.label || selectedSource.name}): ${selectedMeta.signupUrl || 'signup link missing'}`))
|
|
222
222
|
if (selectedBillingNote) lines.push(themeColors.warning(` π° Paid provider note: ${selectedBillingNote}`))
|
|
223
|
-
lines.push(themeColors.dim(` 2) ${selectedMeta.signupHint || '
|
|
224
|
-
lines.push(themeColors.dim(` 3)
|
|
223
|
+
lines.push(themeColors.dim(` 2) ${selectedMeta.signupHint || t('settings.generateApiKeyHint')}`))
|
|
224
|
+
lines.push(themeColors.dim(` 3) ${t('settings.pressToTestKey', { key: themeColors.hotkey('T') })} ${setupStatus}`))
|
|
225
225
|
if (selectedProviderKey === 'cloudflare') {
|
|
226
226
|
// π Account id resolution has fallbacks since issue #181: env var,
|
|
227
227
|
// π stored config setting, or zero-setup auto-discovery from the API key.
|
|
@@ -235,7 +235,7 @@ export function createOverlayRenderers(state, deps) {
|
|
|
235
235
|
const testDetail = state.settingsTestDetails?.[selectedProviderKey]
|
|
236
236
|
if (testDetail) {
|
|
237
237
|
lines.push('')
|
|
238
|
-
lines.push(themeColors.errorBold(
|
|
238
|
+
lines.push(themeColors.errorBold(` ${t('settings.testDiagnostics')}`))
|
|
239
239
|
for (const detailLine of wrapPlainText(testDetail)) {
|
|
240
240
|
lines.push(themeColors.error(` ${detailLine}`))
|
|
241
241
|
}
|
|
@@ -252,38 +252,44 @@ export function createOverlayRenderers(state, deps) {
|
|
|
252
252
|
const updateState = state.settingsUpdateState
|
|
253
253
|
const latestFound = state.settingsUpdateLatestVersion
|
|
254
254
|
const updateActionLabel = updateState === 'available' && latestFound
|
|
255
|
-
?
|
|
256
|
-
: '
|
|
257
|
-
let updateStatus = themeColors.dim('
|
|
258
|
-
if (updateState === 'checking') updateStatus = themeColors.warning('
|
|
259
|
-
if (updateState === 'available' && latestFound) updateStatus = themeColors.successBold(
|
|
260
|
-
if (updateState === 'up-to-date') updateStatus = themeColors.success('
|
|
261
|
-
if (updateState === 'error') updateStatus = themeColors.error(
|
|
262
|
-
if (updateState === 'installing') updateStatus = themeColors.info('
|
|
263
|
-
const updateRow = `${bullet(updateCursor)}${themeColors.textBold(updateActionLabel)
|
|
255
|
+
? t('settings.installUpdate', { version: latestFound })
|
|
256
|
+
: t('settings.checkUpdatesManually')
|
|
257
|
+
let updateStatus = themeColors.dim(t('settings.pressToCheckUpdates'))
|
|
258
|
+
if (updateState === 'checking') updateStatus = themeColors.warning(t('update.checking'))
|
|
259
|
+
if (updateState === 'available' && latestFound) updateStatus = themeColors.successBold(t('settings.updateAvailable', { version: latestFound }))
|
|
260
|
+
if (updateState === 'up-to-date') updateStatus = themeColors.success(t('settings.upToDate'))
|
|
261
|
+
if (updateState === 'error') updateStatus = themeColors.error(t('settings.updateCheckFailed'))
|
|
262
|
+
if (updateState === 'installing') updateStatus = themeColors.info(t('settings.installingUpdate'))
|
|
263
|
+
const updateRow = `${bullet(updateCursor)}${padEndDisplay(themeColors.textBold(updateActionLabel), 44)} ${updateStatus}`
|
|
264
264
|
cursorLineByRow[updateRowIdx] = lines.length
|
|
265
265
|
lines.push(updateCursor ? themeColors.bgCursor(updateRow) : updateRow)
|
|
266
266
|
const themeStatus = getThemeStatusLabel(activeThemeSetting())
|
|
267
267
|
const themeStatusColor = themeStatus.includes('Dark') ? themeColors.warningBold : themeColors.info
|
|
268
|
-
const themeRow = `${bullet(state.settingsCursor === themeRowIdx)}${themeColors.textBold('
|
|
268
|
+
const themeRow = `${bullet(state.settingsCursor === themeRowIdx)}${padEndDisplay(themeColors.textBold(t('settings.globalTheme')), 44)} ${themeStatusColor(themeStatus)}`
|
|
269
269
|
cursorLineByRow[themeRowIdx] = lines.length
|
|
270
270
|
lines.push(state.settingsCursor === themeRowIdx ? themeColors.bgCursor(themeRow) : themeRow)
|
|
271
271
|
|
|
272
|
+
const activeLanguage = state.config.settings?.language === 'zh-CN' ? 'zh-CN' : 'en'
|
|
273
|
+
const languageLabel = activeLanguage === 'zh-CN' ? t('common.language.simplifiedChinese') : t('common.language.english')
|
|
274
|
+
const languageRow = `${bullet(state.settingsCursor === languageRowIdx)}${padEndDisplay(themeColors.textBold(t('settings.language')), 44)} ${themeColors.info(languageLabel)}`
|
|
275
|
+
cursorLineByRow[languageRowIdx] = lines.length
|
|
276
|
+
lines.push(state.settingsCursor === languageRowIdx ? themeColors.bgCursorSettingsList(languageRow) : languageRow)
|
|
277
|
+
|
|
272
278
|
// π Favorites mode row mirrors Y-key behavior from the main table.
|
|
273
279
|
const favoritesModeEnabled = state.favoritesPinnedAndSticky === true
|
|
274
280
|
const favoritesModeStatus = favoritesModeEnabled
|
|
275
|
-
? themeColors.warningBold('
|
|
276
|
-
: themeColors.info(
|
|
277
|
-
const favoritesModeRow = `${bullet(state.settingsCursor === favoritesModeRowIdx)}${themeColors.textBold('
|
|
281
|
+
? themeColors.warningBold(t('settings.favoritesPinned'))
|
|
282
|
+
: themeColors.info(t('settings.favoritesNormal'))
|
|
283
|
+
const favoritesModeRow = `${bullet(state.settingsCursor === favoritesModeRowIdx)}${padEndDisplay(themeColors.textBold(t('settings.favoritesMode')), 44)} ${favoritesModeStatus}`
|
|
278
284
|
cursorLineByRow[favoritesModeRowIdx] = lines.length
|
|
279
285
|
lines.push(state.settingsCursor === favoritesModeRowIdx ? themeColors.bgCursorSettingsList(favoritesModeRow) : favoritesModeRow)
|
|
280
286
|
|
|
281
287
|
// π Startup AI Speed Scan row controls the opt-in Ctrl+U auto-run at launch.
|
|
282
288
|
const startupAiSpeedScanEnabled = state.config.settings?.runAiSpeedTestOnStartup === true
|
|
283
289
|
const startupAiSpeedScanStatus = startupAiSpeedScanEnabled
|
|
284
|
-
? themeColors.successBold('
|
|
285
|
-
: themeColors.dim('
|
|
286
|
-
const startupAiSpeedScanRow = `${bullet(state.settingsCursor === startupAiSpeedScanRowIdx)}${themeColors.textBold('
|
|
290
|
+
? themeColors.successBold(t('settings.startupSpeedTestEnabled'))
|
|
291
|
+
: themeColors.dim(t('settings.startupSpeedTestDisabled'))
|
|
292
|
+
const startupAiSpeedScanRow = `${bullet(state.settingsCursor === startupAiSpeedScanRowIdx)}${padEndDisplay(themeColors.textBold(t('settings.startupSpeedTest')), 44)} ${startupAiSpeedScanStatus}`
|
|
287
293
|
cursorLineByRow[startupAiSpeedScanRowIdx] = lines.length
|
|
288
294
|
lines.push(state.settingsCursor === startupAiSpeedScanRowIdx ? themeColors.bgCursorSettingsList(startupAiSpeedScanRow) : startupAiSpeedScanRow)
|
|
289
295
|
|
|
@@ -292,8 +298,8 @@ export function createOverlayRenderers(state, deps) {
|
|
|
292
298
|
const hiddenCount = state.config.hiddenModels instanceof Set ? state.config.hiddenModels.size : 0
|
|
293
299
|
const autoHideStatus = autoHideEnabled
|
|
294
300
|
? themeColors.successBold(`β
Enabled (${hiddenCount} hidden)`)
|
|
295
|
-
: themeColors.errorBold('
|
|
296
|
-
const autoHideRow = `${bullet(state.settingsCursor === autoHideBrokenModelsRowIdx)}${themeColors.textBold('
|
|
301
|
+
: themeColors.errorBold(t('common.disabled'))
|
|
302
|
+
const autoHideRow = `${bullet(state.settingsCursor === autoHideBrokenModelsRowIdx)}${padEndDisplay(themeColors.textBold(t('settings.autoHideBrokenModels')), 44)} ${autoHideStatus}`
|
|
297
303
|
cursorLineByRow[autoHideBrokenModelsRowIdx] = lines.length
|
|
298
304
|
lines.push(state.settingsCursor === autoHideBrokenModelsRowIdx ? themeColors.bgCursorSettingsList(autoHideRow) : autoHideRow)
|
|
299
305
|
|
|
@@ -302,23 +308,23 @@ export function createOverlayRenderers(state, deps) {
|
|
|
302
308
|
}
|
|
303
309
|
|
|
304
310
|
// π Cleanup row removes stale proxy-era config left behind by older builds.
|
|
305
|
-
const cleanupLegacyProxyRow = `${bullet(state.settingsCursor === cleanupLegacyProxyRowIdx)}${themeColors.textBold('
|
|
311
|
+
const cleanupLegacyProxyRow = `${bullet(state.settingsCursor === cleanupLegacyProxyRowIdx)}${padEndDisplay(themeColors.textBold(t('settings.cleanupLegacy')), 44)} ${themeColors.warning(t('settings.pressEnterCleanup'))}`
|
|
306
312
|
cursorLineByRow[cleanupLegacyProxyRowIdx] = lines.length
|
|
307
313
|
lines.push(state.settingsCursor === cleanupLegacyProxyRowIdx ? themeColors.bgCursorLegacy(cleanupLegacyProxyRow) : cleanupLegacyProxyRow)
|
|
308
314
|
|
|
309
315
|
// π Changelog viewer row
|
|
310
|
-
const changelogViewRow = `${bullet(state.settingsCursor === changelogViewRowIdx)}${themeColors.textBold('
|
|
316
|
+
const changelogViewRow = `${bullet(state.settingsCursor === changelogViewRowIdx)}${padEndDisplay(themeColors.textBold(t('settings.viewChangelog')), 44)} ${themeColors.dim(t('settings.pressEnterChangelog'))}`
|
|
311
317
|
cursorLineByRow[changelogViewRowIdx] = lines.length
|
|
312
318
|
lines.push(state.settingsCursor === changelogViewRowIdx ? themeColors.bgCursorSettingsList(changelogViewRow) : changelogViewRow)
|
|
313
319
|
|
|
314
320
|
// π Shell env toggle β expose API keys as shell environment variables
|
|
315
321
|
const shellEnvSetting = state.config.settings?.shellEnvEnabled
|
|
316
322
|
const shellEnvStatus = shellEnvSetting === true
|
|
317
|
-
? themeColors.successBold('
|
|
323
|
+
? themeColors.successBold(t('settings.shellEnvEnabled'))
|
|
318
324
|
: shellEnvSetting === false
|
|
319
|
-
? themeColors.dim('
|
|
320
|
-
: themeColors.warning('
|
|
321
|
-
const shellEnvRow = `${bullet(state.settingsCursor === shellEnvRowIdx)}${themeColors.textBold('
|
|
325
|
+
? themeColors.dim(t('common.disabled'))
|
|
326
|
+
: themeColors.warning(t('settings.shellEnvNotConfigured'))
|
|
327
|
+
const shellEnvRow = `${bullet(state.settingsCursor === shellEnvRowIdx)}${padEndDisplay(themeColors.textBold(t('settings.shellEnv')), 44)} ${shellEnvStatus}`
|
|
322
328
|
cursorLineByRow[shellEnvRowIdx] = lines.length
|
|
323
329
|
lines.push(state.settingsCursor === shellEnvRowIdx ? themeColors.bgCursorSettingsList(shellEnvRow) : shellEnvRow)
|
|
324
330
|
|
|
@@ -326,9 +332,9 @@ export function createOverlayRenderers(state, deps) {
|
|
|
326
332
|
|
|
327
333
|
lines.push('')
|
|
328
334
|
if (state.settingsEditMode) {
|
|
329
|
-
lines.push(themeColors.dim('
|
|
335
|
+
lines.push(themeColors.dim(` ${t('settings.typeApiKey')} β’ Enter ${t('common.save')} β’ Esc ${t('common.cancel')}`))
|
|
330
336
|
} else {
|
|
331
|
-
lines.push(themeColors.dim(
|
|
337
|
+
lines.push(themeColors.dim(` ββ ${t('settings.navigate')} β’ Enter ${t('settings.editRunCycle')} β’ + ${t('settings.addKey')} β’ - ${t('settings.removeKey')} β’ Space ${t('settings.toggleCycle')} β’ T ${t('settings.testKey')} β’ U ${t('settings.updates')} β’ G ${t('settings.theme')} β’ Y ${t('settings.favorites')} β’ Esc ${t('common.close')}`))
|
|
332
338
|
}
|
|
333
339
|
// π Show sync/restore status message if set
|
|
334
340
|
if (state.settingsSyncStatus) {
|
|
@@ -390,13 +396,13 @@ export function createOverlayRenderers(state, deps) {
|
|
|
390
396
|
const scopeChoices = [
|
|
391
397
|
{
|
|
392
398
|
key: 'all',
|
|
393
|
-
label: '
|
|
394
|
-
hint: '
|
|
399
|
+
label: t('install.scope.all.label'),
|
|
400
|
+
hint: t('install.scope.all.hint'),
|
|
395
401
|
},
|
|
396
402
|
{
|
|
397
403
|
key: 'selected',
|
|
398
|
-
label: '
|
|
399
|
-
hint: '
|
|
404
|
+
label: t('install.scope.selected.label'),
|
|
405
|
+
hint: t('install.scope.selected.hint'),
|
|
400
406
|
},
|
|
401
407
|
]
|
|
402
408
|
const selectedProviderLabel = state.installEndpointsProviderKey === 'fcm_router'
|
|
@@ -414,40 +420,40 @@ export function createOverlayRenderers(state, deps) {
|
|
|
414
420
|
})()
|
|
415
421
|
: 'β'
|
|
416
422
|
|
|
417
|
-
const selectedConnectionLabel = '
|
|
423
|
+
const selectedConnectionLabel = t('install.directProvider')
|
|
418
424
|
|
|
419
425
|
lines.push('')
|
|
420
426
|
// π Branding header
|
|
421
427
|
lines.push(` ${themeColors.accent('π')} ${themeColors.accentBold('free-coding-models')} ${themeColors.dim(`v${LOCAL_VERSION}`)}`)
|
|
422
|
-
lines.push(` ${themeColors.textBold(
|
|
428
|
+
lines.push(` ${themeColors.textBold(`π ${t('install.title')}`)}`)
|
|
423
429
|
lines.push('')
|
|
424
|
-
lines.push(themeColors.dim(
|
|
430
|
+
lines.push(themeColors.dim(` β ${t('install.tuiSubtitle')}`))
|
|
425
431
|
if (state.installEndpointsErrorMsg) {
|
|
426
432
|
lines.push(` ${themeColors.warning(state.installEndpointsErrorMsg)}`)
|
|
427
433
|
}
|
|
428
434
|
lines.push('')
|
|
429
435
|
|
|
430
436
|
if (state.installEndpointsPhase === 'providers') {
|
|
431
|
-
lines.push(` ${themeColors.textBold(
|
|
437
|
+
lines.push(` ${themeColors.textBold(t('common.stepNumber', { step: 1, total: totalSteps }))} ${themeColors.info(t('install.chooseConfiguredProvider'))}`)
|
|
432
438
|
lines.push('')
|
|
433
439
|
|
|
434
440
|
if (providerChoices.length === 0) {
|
|
435
|
-
lines.push(themeColors.dim('
|
|
436
|
-
lines.push(themeColors.dim('
|
|
441
|
+
lines.push(themeColors.dim(t('install.noProvidersConfigured')))
|
|
442
|
+
lines.push(themeColors.dim(t('install.keyRequired')))
|
|
437
443
|
} else {
|
|
438
444
|
providerChoices.forEach((provider, idx) => {
|
|
439
445
|
const isCursor = idx === state.installEndpointsCursor
|
|
440
|
-
const row = `${bullet(isCursor)}${themeColors.textBold(provider.label.padEnd(24))} ${themeColors.dim(
|
|
446
|
+
const row = `${bullet(isCursor)}${themeColors.textBold(provider.label.padEnd(24))} ${themeColors.dim(t('install.modelsCount', { count: provider.modelCount }))}`
|
|
441
447
|
cursorLineByRow[idx] = lines.length
|
|
442
448
|
lines.push(isCursor ? themeColors.bgCursorInstall(row) : row)
|
|
443
449
|
})
|
|
444
450
|
}
|
|
445
451
|
|
|
446
452
|
lines.push('')
|
|
447
|
-
lines.push(themeColors.dim(
|
|
453
|
+
lines.push(themeColors.dim(` ββ ${t('settings.navigate')} β’ Enter ${t('install.selectProvider')} β’ Esc ${t('common.close')}`))
|
|
448
454
|
} else if (state.installEndpointsPhase === 'tools') {
|
|
449
|
-
lines.push(` ${themeColors.textBold(
|
|
450
|
-
lines.push(themeColors.dim(`
|
|
455
|
+
lines.push(` ${themeColors.textBold(t('common.stepNumber', { step: 2, total: totalSteps }))} ${themeColors.info(t('install.chooseTargetTool'))}`)
|
|
456
|
+
lines.push(themeColors.dim(` ${t('install.provider')}: ${selectedProviderLabel}`))
|
|
451
457
|
lines.push('')
|
|
452
458
|
|
|
453
459
|
// π Use getToolMeta for labels instead of hard-coded ternary chains
|
|
@@ -456,20 +462,20 @@ export function createOverlayRenderers(state, deps) {
|
|
|
456
462
|
const meta = getToolMeta(toolMode)
|
|
457
463
|
const label = `${meta.emoji} ${meta.label}`
|
|
458
464
|
const note = toolMode.startsWith('opencode')
|
|
459
|
-
? themeColors.dim('
|
|
465
|
+
? themeColors.dim(t('install.sharedConfigFile'))
|
|
460
466
|
: toolMode === 'openhands'
|
|
461
|
-
? themeColors.dim(
|
|
462
|
-
: themeColors.dim('
|
|
467
|
+
? themeColors.dim(t('install.envFile'))
|
|
468
|
+
: themeColors.dim(t('install.managedConfig'))
|
|
463
469
|
const row = `${bullet(isCursor)}${themeColors.textBold(label.padEnd(26))} ${note}`
|
|
464
470
|
cursorLineByRow[idx] = lines.length
|
|
465
471
|
lines.push(isCursor ? themeColors.bgCursorInstall(row) : row)
|
|
466
472
|
})
|
|
467
473
|
|
|
468
474
|
lines.push('')
|
|
469
|
-
lines.push(themeColors.dim(
|
|
475
|
+
lines.push(themeColors.dim(` ββ ${t('settings.navigate')} β’ Enter ${t('install.selectTool')} β’ Esc ${t('common.back')}`))
|
|
470
476
|
} else if (state.installEndpointsPhase === 'scope') {
|
|
471
|
-
lines.push(` ${themeColors.textBold(
|
|
472
|
-
lines.push(themeColors.dim(`
|
|
477
|
+
lines.push(` ${themeColors.textBold(t('common.stepNumber', { step: 3, total: totalSteps }))} ${themeColors.info(t('install.chooseInstallScope'))}`)
|
|
478
|
+
lines.push(themeColors.dim(` ${t('install.provider')}: ${selectedProviderLabel} β’ ${t('install.tool')}: ${selectedToolLabel} β’ ${selectedConnectionLabel}`))
|
|
473
479
|
lines.push('')
|
|
474
480
|
|
|
475
481
|
scopeChoices.forEach((scope, idx) => {
|
|
@@ -481,14 +487,14 @@ export function createOverlayRenderers(state, deps) {
|
|
|
481
487
|
lines.push('')
|
|
482
488
|
})
|
|
483
489
|
|
|
484
|
-
lines.push(themeColors.dim(
|
|
490
|
+
lines.push(themeColors.dim(` Enter ${t('install.next')} β’ Esc ${t('common.back')}`))
|
|
485
491
|
} else if (state.installEndpointsPhase === 'models') {
|
|
486
492
|
const models = getProviderCatalogModels(state.installEndpointsProviderKey)
|
|
487
493
|
const selectedCount = state.installEndpointsSelectedModelIds.size
|
|
488
494
|
|
|
489
|
-
lines.push(` ${themeColors.textBold(
|
|
490
|
-
lines.push(themeColors.dim(`
|
|
491
|
-
lines.push(themeColors.dim(`
|
|
495
|
+
lines.push(` ${themeColors.textBold(t('common.stepNumber', { step: 4, total: totalSteps }))} ${themeColors.info(t('install.chooseModelsToInstall'))}`)
|
|
496
|
+
lines.push(themeColors.dim(` ${t('install.provider')}: ${selectedProviderLabel} β’ ${t('install.tool')}: ${selectedToolLabel} β’ ${selectedConnectionLabel}`))
|
|
497
|
+
lines.push(themeColors.dim(` ${t('install.selectedCount', { selected: selectedCount, total: models.length })}`))
|
|
492
498
|
lines.push('')
|
|
493
499
|
|
|
494
500
|
models.forEach((model, idx) => {
|
|
@@ -502,11 +508,11 @@ export function createOverlayRenderers(state, deps) {
|
|
|
502
508
|
})
|
|
503
509
|
|
|
504
510
|
lines.push('')
|
|
505
|
-
lines.push(themeColors.dim(
|
|
511
|
+
lines.push(themeColors.dim(` ββ ${t('settings.navigate')} β’ Space ${t('install.selectModels')} β’ A ${t('install.scope.all')} β’ Enter ${t('install.install')} β’ Esc ${t('common.back')}`))
|
|
506
512
|
} else if (state.installEndpointsPhase === 'result') {
|
|
507
513
|
const result = state.installEndpointsResult
|
|
508
514
|
const accent = result?.type === 'success' ? themeColors.successBold : themeColors.errorBold
|
|
509
|
-
lines.push(` ${themeColors.textBold('
|
|
515
|
+
lines.push(` ${themeColors.textBold(t('install.result'))} ${accent(result?.title || t('install.resultUnavailable'))}`)
|
|
510
516
|
lines.push('')
|
|
511
517
|
|
|
512
518
|
for (const detail of result?.lines || []) {
|
|
@@ -515,11 +521,11 @@ export function createOverlayRenderers(state, deps) {
|
|
|
515
521
|
|
|
516
522
|
if (result?.type === 'success') {
|
|
517
523
|
lines.push('')
|
|
518
|
-
lines.push(themeColors.dim('
|
|
524
|
+
lines.push(themeColors.dim(` ${t('install.futureRefresh')}`))
|
|
519
525
|
}
|
|
520
526
|
|
|
521
527
|
lines.push('')
|
|
522
|
-
lines.push(themeColors.dim(
|
|
528
|
+
lines.push(themeColors.dim(` Enter / Esc ${t('common.close')}`))
|
|
523
529
|
}
|
|
524
530
|
|
|
525
531
|
const targetLine = cursorLineByRow[state.installEndpointsCursor] ?? 0
|
|
@@ -815,7 +821,7 @@ export function createOverlayRenderers(state, deps) {
|
|
|
815
821
|
}
|
|
816
822
|
|
|
817
823
|
if (allResults.length === 0) {
|
|
818
|
-
panelLines.push(sty.dim('
|
|
824
|
+
panelLines.push(sty.dim(` ${t('palette.noResults')}`))
|
|
819
825
|
} else {
|
|
820
826
|
for (let idx = 0; idx < allResults.length; idx++) {
|
|
821
827
|
const entry = allResults[idx]
|
|
@@ -883,7 +889,7 @@ export function createOverlayRenderers(state, deps) {
|
|
|
883
889
|
const query = state.commandPaletteQuery || ''
|
|
884
890
|
const queryWithCursor = query.length > 0
|
|
885
891
|
? `${query}${sty.accentBold('β')}`
|
|
886
|
-
: sty.accentBold('β') + sty.dim('
|
|
892
|
+
: sty.accentBold('β') + sty.dim(t('palette.searchPlaceholder'))
|
|
887
893
|
|
|
888
894
|
// π Header: title left, Esc hint right, search input, separator. Each piece is
|
|
889
895
|
// π width-budgeted so the header can never push past the panel on tiny screens.
|
|
@@ -891,17 +897,17 @@ export function createOverlayRenderers(state, deps) {
|
|
|
891
897
|
const titleBudget = Math.max(1, panelInnerWidth - 1 - displayWidth('Esc'))
|
|
892
898
|
const queryBudget = Math.max(1, panelInnerWidth - 1)
|
|
893
899
|
const headerLines = [
|
|
894
|
-
` ${padEndDisplay(truncateAnsiWidth(sty.headerBold(
|
|
900
|
+
` ${padEndDisplay(truncateAnsiWidth(sty.headerBold(`β‘οΈ ${t('palette.title')}`), titleBudget, { ellipsis: 'β¦' }), titleBudget)} ${escHint}`,
|
|
895
901
|
` ${padEndDisplay(truncateAnsiWidth(`> ${queryWithCursor}`, queryBudget, { ellipsis: 'β¦' }), queryBudget)}`,
|
|
896
902
|
sty.dim(` ${'β'.repeat(Math.max(1, panelInnerWidth - 1))}`),
|
|
897
903
|
]
|
|
898
904
|
|
|
899
905
|
const sepLine = sty.dim(` ${'β'.repeat(Math.max(1, panelInnerWidth - 1))}`)
|
|
900
906
|
const footerLines = caps.compact
|
|
901
|
-
? [sepLine, ` ${sty.dim(truncateAnsiWidth('
|
|
907
|
+
? [sepLine, ` ${sty.dim(truncateAnsiWidth(`β΅ ${t('common.select')} β’ ββ ${t('settings.navigate')} β’ ${t('common.search')}`, panelInnerWidth - 1))}`]
|
|
902
908
|
: [
|
|
903
909
|
sepLine,
|
|
904
|
-
` ${padEndDisplay(sty.dim('
|
|
910
|
+
` ${padEndDisplay(sty.dim(`β΅ ${t('common.select')} β’ β β ${t('common.expand')}`), panelInnerWidth - 1)}`,
|
|
905
911
|
` ${padEndDisplay(sty.dim('ββ Navigate β’ Type search'), panelInnerWidth - 1)}`,
|
|
906
912
|
]
|
|
907
913
|
|
|
@@ -964,11 +970,11 @@ export function createOverlayRenderers(state, deps) {
|
|
|
964
970
|
|
|
965
971
|
// π Branding header
|
|
966
972
|
lines.push(` ${themeColors.accent('π')} ${themeColors.accentBold('free-coding-models')} ${themeColors.dim(`v${LOCAL_VERSION}`)}`)
|
|
967
|
-
lines.push(` ${heading('
|
|
973
|
+
lines.push(` ${heading(`β ${t('help.title')} Β· ${t('help.shortcuts')}`)}`)
|
|
968
974
|
lines.push(` ${themeColors.successBold('π Yellow = active key')}`)
|
|
969
975
|
lines.push('')
|
|
970
976
|
lines.push(` ${hint('β ββ / PgUp / PgDn / Home / End scroll β’ K or ')}${themeColors.successBold('Esc close')}`)
|
|
971
|
-
lines.push(` ${heading('
|
|
977
|
+
lines.push(` ${heading(t('help.columns'))}`)
|
|
972
978
|
lines.push('')
|
|
973
979
|
lines.push(` ${label('Rank')} SWE-bench rank (1 = best coding score) ${hint('Sort:')} ${key('R')}`)
|
|
974
980
|
lines.push(` ${hint('Quick glance at which model is objectively the best coder right now.')}`)
|
|
@@ -1012,13 +1018,13 @@ export function createOverlayRenderers(state, deps) {
|
|
|
1012
1018
|
|
|
1013
1019
|
|
|
1014
1020
|
lines.push('')
|
|
1015
|
-
lines.push(` ${heading('
|
|
1016
|
-
lines.push(` ${heading('
|
|
1021
|
+
lines.push(` ${heading(t('help.mainTui'))}`)
|
|
1022
|
+
lines.push(` ${heading(t('help.navigation'))}`)
|
|
1017
1023
|
lines.push(` ${key('ββ / J/K')} Navigate rows ${hint('(J/K = vim-style scroll)')}`)
|
|
1018
1024
|
lines.push(` ${key('Enter')} Select model and launch`)
|
|
1019
1025
|
lines.push(` ${hint('If the active CLI is missing, FCM offers a one-click install prompt first.')}`)
|
|
1020
1026
|
lines.push('')
|
|
1021
|
-
lines.push(` ${heading('
|
|
1027
|
+
lines.push(` ${heading(t('help.controls'))}`)
|
|
1022
1028
|
lines.push(` ${key('W')} Toggle ping mode ${hint('(speed 2s β normal 10s β slow 30s β forced 4s)')}`)
|
|
1023
1029
|
lines.push(` ${key('Ctrl+P')} Open β‘οΈ command palette ${hint('(search and run actions quickly)')}`)
|
|
1024
1030
|
lines.push(` ${key('Ctrl+A')} AI Speed Test ${hint('(benchmark selected model β time + TPS)')}`)
|
|
@@ -1044,7 +1050,7 @@ export function createOverlayRenderers(state, deps) {
|
|
|
1044
1050
|
lines.push(` ${key('I')} / ${key('Esc')} Show/hide this help`)
|
|
1045
1051
|
lines.push(` ${key('Ctrl+C')} Exit`)
|
|
1046
1052
|
lines.push('')
|
|
1047
|
-
lines.push(` ${heading('
|
|
1053
|
+
lines.push(` ${heading(`${t('settings.title')} (P)`)}`)
|
|
1048
1054
|
lines.push(` ${key('ββ')} Navigate rows`)
|
|
1049
1055
|
lines.push(` ${key('PgUp/PgDn')} Jump by page`)
|
|
1050
1056
|
lines.push(` ${key('Home/End')} Jump first/last row`)
|
|
@@ -1077,27 +1083,27 @@ export function createOverlayRenderers(state, deps) {
|
|
|
1077
1083
|
// π Branding header
|
|
1078
1084
|
lines.push('')
|
|
1079
1085
|
lines.push(` ${themeColors.accent('π')} ${themeColors.accentBold('free-coding-models')} ${themeColors.dim(`v${LOCAL_VERSION}`)}`)
|
|
1080
|
-
lines.push(` ${themeColors.textBold(
|
|
1086
|
+
lines.push(` ${themeColors.textBold(`π― ${t('recommend.title')}`)}`)
|
|
1081
1087
|
lines.push('')
|
|
1082
|
-
lines.push(themeColors.dim(
|
|
1088
|
+
lines.push(themeColors.dim(` β ${t('recommend.subtitle')}`))
|
|
1083
1089
|
lines.push('')
|
|
1084
1090
|
|
|
1085
1091
|
if (state.recommendPhase === 'questionnaire') {
|
|
1086
1092
|
// π Question definitions β each has a title, options array, and answer key
|
|
1087
1093
|
const questions = [
|
|
1088
1094
|
{
|
|
1089
|
-
title: '
|
|
1090
|
-
options: Object.entries(TASK_TYPES).map(([key, val]) => ({ key, label: val.label })),
|
|
1095
|
+
title: t('recommend.questionTask'),
|
|
1096
|
+
options: Object.entries(TASK_TYPES).map(([key, val]) => ({ key, label: t(`recommend.taskType.${key}`) || val.label })),
|
|
1091
1097
|
answerKey: 'taskType',
|
|
1092
1098
|
},
|
|
1093
1099
|
{
|
|
1094
|
-
title: '
|
|
1095
|
-
options: Object.entries(PRIORITY_TYPES).map(([key, val]) => ({ key, label: val.label })),
|
|
1100
|
+
title: t('recommend.questionPriority'),
|
|
1101
|
+
options: Object.entries(PRIORITY_TYPES).map(([key, val]) => ({ key, label: t(`recommend.priority.${key}`) || val.label })),
|
|
1096
1102
|
answerKey: 'priority',
|
|
1097
1103
|
},
|
|
1098
1104
|
{
|
|
1099
|
-
title: '
|
|
1100
|
-
options: Object.entries(CONTEXT_BUDGETS).map(([key, val]) => ({ key, label: val.label })),
|
|
1105
|
+
title: t('recommend.questionContext'),
|
|
1106
|
+
options: Object.entries(CONTEXT_BUDGETS).map(([key, val]) => ({ key, label: t(`recommend.contextBudget.${key}`) || val.label })),
|
|
1101
1107
|
answerKey: 'contextBudget',
|
|
1102
1108
|
},
|
|
1103
1109
|
]
|
|
@@ -1120,7 +1126,7 @@ export function createOverlayRenderers(state, deps) {
|
|
|
1120
1126
|
lines.push('')
|
|
1121
1127
|
}
|
|
1122
1128
|
|
|
1123
|
-
lines.push(` ${themeColors.textBold(
|
|
1129
|
+
lines.push(` ${themeColors.textBold(t('recommend.questionProgress', { step: qNum, total: qTotal }))}: ${themeColors.info(q.title)}`)
|
|
1124
1130
|
lines.push('')
|
|
1125
1131
|
|
|
1126
1132
|
for (let i = 0; i < q.options.length; i++) {
|
|
@@ -1145,7 +1151,7 @@ export function createOverlayRenderers(state, deps) {
|
|
|
1145
1151
|
const empty = barWidth - filled
|
|
1146
1152
|
const bar = themeColors.successBold('β'.repeat(filled)) + themeColors.dim('β'.repeat(empty))
|
|
1147
1153
|
|
|
1148
|
-
lines.push(` ${themeColors.textBold('
|
|
1154
|
+
lines.push(` ${themeColors.textBold(t('recommend.analyzing'))}`)
|
|
1149
1155
|
lines.push('')
|
|
1150
1156
|
lines.push(` ${bar} ${themeColors.textBold(String(pct) + '%')}`)
|
|
1151
1157
|
lines.push('')
|
|
@@ -1172,9 +1178,9 @@ export function createOverlayRenderers(state, deps) {
|
|
|
1172
1178
|
lines.push('')
|
|
1173
1179
|
|
|
1174
1180
|
if (state.recommendResults.length === 0) {
|
|
1175
|
-
lines.push(` ${themeColors.warning('
|
|
1181
|
+
lines.push(` ${themeColors.warning(t('recommend.noResults'))}`)
|
|
1176
1182
|
} else {
|
|
1177
|
-
lines.push(` ${themeColors.textBold('
|
|
1183
|
+
lines.push(` ${themeColors.textBold(`${t('recommend.topPicks')}:`)}`)
|
|
1178
1184
|
lines.push('')
|
|
1179
1185
|
|
|
1180
1186
|
for (let i = 0; i < state.recommendResults.length; i++) {
|
|
@@ -1367,8 +1373,8 @@ export function createOverlayRenderers(state, deps) {
|
|
|
1367
1373
|
let displayText = item.replace(/\*\*([^*]+)\*\*/g, '$1').replace(/`([^`]+)`/g, '$1')
|
|
1368
1374
|
// π Wrap long lines
|
|
1369
1375
|
const maxWidth = state.terminalCols - 16
|
|
1370
|
-
if (displayText
|
|
1371
|
-
displayText = displayText
|
|
1376
|
+
if (displayWidth(displayText) > maxWidth) {
|
|
1377
|
+
displayText = truncateAnsiWidth(displayText, maxWidth, { ellipsis: 'β¦' })
|
|
1372
1378
|
}
|
|
1373
1379
|
lines.push(` β’ ${displayText}`)
|
|
1374
1380
|
}
|
|
@@ -213,12 +213,13 @@ export function truncateAnsiWidth(input, maxWidth, { ellipsis = 'β¦' } = {}) {
|
|
|
213
213
|
// π Applies bgColor to each line and pads to terminalCols for full-width panel look.
|
|
214
214
|
// π If terminalCols is not provided, falls back to OVERLAY_PANEL_WIDTH for compatibility.
|
|
215
215
|
export function tintOverlayLines(lines, bgColor, terminalCols = null) {
|
|
216
|
-
const panelWidth = terminalCols || OVERLAY_PANEL_WIDTH
|
|
216
|
+
const panelWidth = Math.max(1, terminalCols || OVERLAY_PANEL_WIDTH)
|
|
217
217
|
return lines.map((line) => {
|
|
218
218
|
const text = String(line)
|
|
219
|
-
const
|
|
219
|
+
const fitted = truncateAnsiWidth(text, panelWidth, { ellipsis: '' })
|
|
220
|
+
const visibleWidth = displayWidth(fitted)
|
|
220
221
|
const padding = ' '.repeat(Math.max(0, panelWidth - visibleWidth))
|
|
221
|
-
return bgColor(
|
|
222
|
+
return bgColor(fitted + padding)
|
|
222
223
|
})
|
|
223
224
|
}
|
|
224
225
|
|