free-coding-models 0.2.1 β†’ 0.2.2

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,8 @@
4
4
  *
5
5
  * @details
6
6
  * This module centralizes all overlay rendering in one place:
7
- * - Settings, Help, Log, Smart Recommend, Feature Request, Bug Report
7
+ * - Settings, Install Endpoints, Help, Log, Smart Recommend, Feature Request, Bug Report
8
+ * - Settings diagnostics for provider key tests, including wrapped retry/error details
8
9
  * - Recommend analysis timer orchestration and progress updates
9
10
  *
10
11
  * The factory pattern keeps stateful UI logic isolated while still
@@ -48,8 +49,32 @@ export function createOverlayRenderers(state, deps) {
48
49
  getTopRecommendations,
49
50
  adjustScrollOffset,
50
51
  getPingModel,
52
+ getConfiguredInstallableProviders,
53
+ getInstallTargetModes,
54
+ getProviderCatalogModels,
51
55
  } = deps
52
56
 
57
+ // πŸ“– Wrap plain diagnostic text so long Settings messages stay readable inside
58
+ // πŸ“– the overlay instead of turning into one truncated red line.
59
+ const wrapPlainText = (text, width = 104) => {
60
+ const normalized = typeof text === 'string' ? text.trim() : ''
61
+ if (!normalized) return []
62
+ const words = normalized.split(/\s+/)
63
+ const lines = []
64
+ let current = ''
65
+ for (const word of words) {
66
+ const next = current ? `${current} ${word}` : word
67
+ if (next.length > width && current) {
68
+ lines.push(current)
69
+ current = word
70
+ } else {
71
+ current = next
72
+ }
73
+ }
74
+ if (current) lines.push(current)
75
+ return lines
76
+ }
77
+
53
78
  // πŸ“– Keep log token formatting aligned with the main table so the same totals
54
79
  // πŸ“– read the same everywhere in the TUI.
55
80
  const formatLogTokens = (totalTokens) => {
@@ -118,9 +143,13 @@ export function createOverlayRenderers(state, deps) {
118
143
 
119
144
  // πŸ“– Test result badge
120
145
  const testResult = state.settingsTestResults[pk]
121
- let testBadge = chalk.dim('[Test β€”]')
146
+ // πŸ“– Default badge reflects configuration first: a saved key should look
147
+ // πŸ“– ready to test even before the user has run the probe once.
148
+ let testBadge = keyCount > 0 ? chalk.cyan('[Test]') : chalk.dim('[Missing Key πŸ”‘]')
122
149
  if (testResult === 'pending') testBadge = chalk.yellow('[Testing…]')
123
150
  else if (testResult === 'ok') testBadge = chalk.greenBright('[Test βœ…]')
151
+ else if (testResult === 'missing_key') testBadge = chalk.dim('[Missing Key πŸ”‘]')
152
+ else if (testResult === 'auth_error') testBadge = chalk.red('[Auth ❌]')
124
153
  else if (testResult === 'rate_limited') testBadge = chalk.yellow('[Rate limit ⏳]')
125
154
  else if (testResult === 'no_callable_model') testBadge = chalk.magenta('[No model ⚠]')
126
155
  else if (testResult === 'fail') testBadge = chalk.red('[Test ❌]')
@@ -151,6 +180,14 @@ export function createOverlayRenderers(state, deps) {
151
180
  const accountIdStatus = hasAccountId ? chalk.green('CLOUDFLARE_ACCOUNT_ID detected βœ…') : chalk.yellow('Set CLOUDFLARE_ACCOUNT_ID ⚠')
152
181
  lines.push(chalk.dim(` 4) Export ${chalk.yellow('CLOUDFLARE_ACCOUNT_ID')} in your shell. Status: ${accountIdStatus}`))
153
182
  }
183
+ const testDetail = state.settingsTestDetails?.[selectedProviderKey]
184
+ if (testDetail) {
185
+ lines.push('')
186
+ lines.push(chalk.red.bold(' Test Diagnostics'))
187
+ for (const detailLine of wrapPlainText(testDetail)) {
188
+ lines.push(chalk.red(` ${detailLine}`))
189
+ }
190
+ }
154
191
  lines.push('')
155
192
  }
156
193
 
@@ -266,6 +303,168 @@ export function createOverlayRenderers(state, deps) {
266
303
  return cleared.join('\n')
267
304
  }
268
305
 
306
+ // ─── Install Endpoints overlay renderer ───────────────────────────────────
307
+ // πŸ“– renderInstallEndpoints drives the provider β†’ tool β†’ scope β†’ model flow
308
+ // πŸ“– behind the `Y` hotkey. It deliberately reuses the same overlay viewport
309
+ // πŸ“– helpers as Settings so long provider/model lists stay navigable.
310
+ function renderInstallEndpoints() {
311
+ const EL = '\x1b[K'
312
+ const lines = []
313
+ const cursorLineByRow = {}
314
+ const providerChoices = getConfiguredInstallableProviders(state.config)
315
+ const toolChoices = getInstallTargetModes()
316
+ const scopeChoices = [
317
+ {
318
+ key: 'all',
319
+ label: 'Install all models',
320
+ hint: 'Recommended β€” FCM will refresh this provider catalog automatically later.',
321
+ },
322
+ {
323
+ key: 'selected',
324
+ label: 'Install selected models only',
325
+ hint: 'Choose a smaller curated subset for a cleaner model picker.',
326
+ },
327
+ ]
328
+ const selectedProviderLabel = state.installEndpointsProviderKey
329
+ ? (sources[state.installEndpointsProviderKey]?.name || state.installEndpointsProviderKey)
330
+ : 'β€”'
331
+ const selectedToolLabel = state.installEndpointsToolMode
332
+ ? (state.installEndpointsToolMode === 'opencode-desktop'
333
+ ? 'OpenCode Desktop (shared opencode.json)'
334
+ : (state.installEndpointsToolMode === 'opencode'
335
+ ? 'OpenCode CLI (shared opencode.json)'
336
+ : state.installEndpointsToolMode === 'openclaw'
337
+ ? 'OpenClaw'
338
+ : state.installEndpointsToolMode === 'crush'
339
+ ? 'Crush'
340
+ : 'Goose'))
341
+ : 'β€”'
342
+
343
+ lines.push('')
344
+ lines.push(` ${chalk.bold('πŸ”Œ Install Endpoints')} ${chalk.dim('β€” install provider catalogs into supported coding tools')}`)
345
+ if (state.installEndpointsErrorMsg) {
346
+ lines.push(` ${chalk.yellow(state.installEndpointsErrorMsg)}`)
347
+ }
348
+ lines.push('')
349
+
350
+ if (state.installEndpointsPhase === 'providers') {
351
+ lines.push(` ${chalk.bold('Step 1/4')} ${chalk.cyan('Choose a configured provider')}`)
352
+ lines.push('')
353
+
354
+ if (providerChoices.length === 0) {
355
+ lines.push(chalk.dim(' No configured providers can be installed directly right now.'))
356
+ lines.push(chalk.dim(' Add an API key in Settings (`P`) first, then reopen this screen.'))
357
+ } else {
358
+ providerChoices.forEach((provider, idx) => {
359
+ const isCursor = idx === state.installEndpointsCursor
360
+ const bullet = isCursor ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
361
+ const row = `${bullet}${chalk.bold(provider.label.padEnd(24))} ${chalk.dim(`${provider.modelCount} models`)}`
362
+ cursorLineByRow[idx] = lines.length
363
+ lines.push(isCursor ? chalk.bgRgb(24, 44, 62)(row) : row)
364
+ })
365
+ }
366
+
367
+ lines.push('')
368
+ lines.push(chalk.dim(' ↑↓ Navigate β€’ Enter Choose provider β€’ Esc Close'))
369
+ } else if (state.installEndpointsPhase === 'tools') {
370
+ lines.push(` ${chalk.bold('Step 2/4')} ${chalk.cyan('Choose the target tool')}`)
371
+ lines.push(chalk.dim(` Provider: ${selectedProviderLabel}`))
372
+ lines.push('')
373
+
374
+ toolChoices.forEach((toolMode, idx) => {
375
+ const isCursor = idx === state.installEndpointsCursor
376
+ const label = toolMode === 'opencode-desktop'
377
+ ? 'OpenCode Desktop'
378
+ : toolMode === 'opencode'
379
+ ? 'OpenCode CLI'
380
+ : toolMode === 'openclaw'
381
+ ? 'OpenClaw'
382
+ : toolMode === 'crush'
383
+ ? 'Crush'
384
+ : 'Goose'
385
+ const note = toolMode.startsWith('opencode')
386
+ ? chalk.dim('shared config file')
387
+ : chalk.dim('managed provider install')
388
+ const bullet = isCursor ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
389
+ const row = `${bullet}${chalk.bold(label.padEnd(22))} ${note}`
390
+ cursorLineByRow[idx] = lines.length
391
+ lines.push(isCursor ? chalk.bgRgb(24, 44, 62)(row) : row)
392
+ })
393
+
394
+ lines.push('')
395
+ lines.push(chalk.dim(' ↑↓ Navigate β€’ Enter Choose tool β€’ Esc Back'))
396
+ } else if (state.installEndpointsPhase === 'scope') {
397
+ lines.push(` ${chalk.bold('Step 3/4')} ${chalk.cyan('Choose the install scope')}`)
398
+ lines.push(chalk.dim(` Provider: ${selectedProviderLabel} β€’ Tool: ${selectedToolLabel}`))
399
+ lines.push('')
400
+
401
+ scopeChoices.forEach((scope, idx) => {
402
+ const isCursor = idx === state.installEndpointsCursor
403
+ const bullet = isCursor ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
404
+ const row = `${bullet}${chalk.bold(scope.label)}`
405
+ cursorLineByRow[idx] = lines.length
406
+ lines.push(isCursor ? chalk.bgRgb(24, 44, 62)(row) : row)
407
+ lines.push(chalk.dim(` ${scope.hint}`))
408
+ lines.push('')
409
+ })
410
+
411
+ lines.push(chalk.dim(' Enter Continue β€’ Esc Back'))
412
+ } else if (state.installEndpointsPhase === 'models') {
413
+ const models = getProviderCatalogModels(state.installEndpointsProviderKey)
414
+ const selectedCount = state.installEndpointsSelectedModelIds.size
415
+
416
+ lines.push(` ${chalk.bold('Step 4/4')} ${chalk.cyan('Choose which models to install')}`)
417
+ lines.push(chalk.dim(` Provider: ${selectedProviderLabel} β€’ Tool: ${selectedToolLabel}`))
418
+ lines.push(chalk.dim(` Selected: ${selectedCount}/${models.length}`))
419
+ lines.push('')
420
+
421
+ models.forEach((model, idx) => {
422
+ const isCursor = idx === state.installEndpointsCursor
423
+ const selected = state.installEndpointsSelectedModelIds.has(model.modelId)
424
+ const bullet = isCursor ? chalk.bold.cyan(' ❯ ') : chalk.dim(' ')
425
+ const checkbox = selected ? chalk.greenBright('[βœ“]') : chalk.dim('[ ]')
426
+ const tier = chalk.cyan(model.tier.padEnd(2))
427
+ const row = `${bullet}${checkbox} ${chalk.bold(model.label.padEnd(26))} ${tier} ${chalk.dim(model.ctx.padEnd(6))} ${chalk.dim(model.modelId)}`
428
+ cursorLineByRow[idx] = lines.length
429
+ lines.push(isCursor ? chalk.bgRgb(24, 44, 62)(row) : row)
430
+ })
431
+
432
+ lines.push('')
433
+ lines.push(chalk.dim(' ↑↓ Navigate β€’ Space Toggle model β€’ A All/None β€’ Enter Install β€’ Esc Back'))
434
+ } else if (state.installEndpointsPhase === 'result') {
435
+ const result = state.installEndpointsResult
436
+ const accent = result?.type === 'success' ? chalk.greenBright : chalk.redBright
437
+ lines.push(` ${chalk.bold('Result')} ${accent(result?.title || 'Install result unavailable')}`)
438
+ lines.push('')
439
+
440
+ for (const detail of result?.lines || []) {
441
+ lines.push(` ${detail}`)
442
+ }
443
+
444
+ if (result?.type === 'success') {
445
+ lines.push('')
446
+ lines.push(chalk.dim(' Future FCM launches will refresh this catalog automatically when the provider list evolves.'))
447
+ }
448
+
449
+ lines.push('')
450
+ lines.push(chalk.dim(' Enter or Esc Close'))
451
+ }
452
+
453
+ const targetLine = cursorLineByRow[state.installEndpointsCursor] ?? 0
454
+ state.installEndpointsScrollOffset = keepOverlayTargetVisible(
455
+ state.installEndpointsScrollOffset,
456
+ targetLine,
457
+ lines.length,
458
+ state.terminalRows
459
+ )
460
+ const { visible, offset } = sliceOverlayLines(lines, state.installEndpointsScrollOffset, state.terminalRows)
461
+ state.installEndpointsScrollOffset = offset
462
+
463
+ const tintedLines = tintOverlayLines(visible, SETTINGS_OVERLAY_BG)
464
+ const cleared = tintedLines.map((line) => line + EL)
465
+ return cleared.join('\n')
466
+ }
467
+
269
468
  // ─── Help overlay renderer ────────────────────────────────────────────────
270
469
  // πŸ“– renderHelp: Draw the help overlay listing all key bindings.
271
470
  // πŸ“– Toggled with K key. Gives users a quick reference without leaving the TUI.
@@ -280,7 +479,7 @@ export function createOverlayRenderers(state, deps) {
280
479
  lines.push(` ${chalk.cyan('Rank')} SWE-bench rank (1 = best coding score) ${chalk.dim('Sort:')} ${chalk.yellow('R')}`)
281
480
  lines.push(` ${chalk.dim('Quick glance at which model is objectively the best coder right now.')}`)
282
481
  lines.push('')
283
- lines.push(` ${chalk.cyan('Tier')} S+ / S / A+ / A / A- / B+ / B / C based on SWE-bench score ${chalk.dim('Sort:')} ${chalk.yellow('Y')} ${chalk.dim('Cycle:')} ${chalk.yellow('T')}`)
482
+ lines.push(` ${chalk.cyan('Tier')} S+ / S / A+ / A / A- / B+ / B / C based on SWE-bench score ${chalk.dim('Cycle:')} ${chalk.yellow('T')}`)
284
483
  lines.push(` ${chalk.dim('Skip the noise β€” S/S+ models solve real GitHub issues, C models are for light tasks.')}`)
285
484
  lines.push('')
286
485
  lines.push(` ${chalk.cyan('SWE%')} SWE-bench score β€” coding ability benchmark (color-coded) ${chalk.dim('Sort:')} ${chalk.yellow('S')}`)
@@ -331,6 +530,7 @@ export function createOverlayRenderers(state, deps) {
331
530
  lines.push(` ${chalk.yellow('X')} Toggle token log page ${chalk.dim('(shows recent request usage from request-log.jsonl)')}`)
332
531
  lines.push(` ${chalk.yellow('Z')} Cycle tool mode ${chalk.dim('(OpenCode β†’ Desktop β†’ OpenClaw β†’ Crush β†’ Goose)')}`)
333
532
  lines.push(` ${chalk.yellow('F')} Toggle favorite on selected row ${chalk.dim('(⭐ pinned at top, persisted)')}`)
533
+ lines.push(` ${chalk.yellow('Y')} Install endpoints ${chalk.dim('(provider catalog β†’ OpenCode/OpenClaw/Crush/Goose, no proxy)')}`)
334
534
  lines.push(` ${chalk.yellow('Q')} Smart Recommend ${chalk.dim('(🎯 find the best model for your task β€” questionnaire + live analysis)')}`)
335
535
  lines.push(` ${chalk.rgb(57, 255, 20).bold('J')} Request Feature ${chalk.dim('(πŸ“ send anonymous feedback to the project team)')}`)
336
536
  lines.push(` ${chalk.rgb(255, 87, 51).bold('I')} Report Bug ${chalk.dim('(πŸ› send anonymous bug report to the project team)')}`)
@@ -907,6 +1107,7 @@ export function createOverlayRenderers(state, deps) {
907
1107
 
908
1108
  return {
909
1109
  renderSettings,
1110
+ renderInstallEndpoints,
910
1111
  renderHelp,
911
1112
  renderLog,
912
1113
  renderRecommend,
@@ -114,7 +114,9 @@ export const PROVIDER_METADATA = {
114
114
  label: 'Hugging Face Inference',
115
115
  color: chalk.rgb(255, 245, 157),
116
116
  signupUrl: 'https://huggingface.co/settings/tokens',
117
- signupHint: 'Settings β†’ Access Tokens',
117
+ // πŸ“– Hugging Face serverless inference now expects a fine-grained token with
118
+ // πŸ“– the dedicated Inference Providers permission, not a generic read token.
119
+ signupHint: 'Settings β†’ Access Tokens β†’ Fine-grained β†’ enable "Make calls to Inference Providers"',
118
120
  rateLimits: 'Free monthly credits (~$0.10)',
119
121
  },
120
122
  replicate: {
@@ -14,6 +14,8 @@
14
14
  * - Viewport clipping with above/below indicators
15
15
  * - Smart badges (mode, tier filter, origin filter, profile)
16
16
  * - Proxy status line integrated in footer
17
+ * - Install-endpoints shortcut surfaced directly in the footer hints
18
+ * - Distinct auth-failure vs missing-key health labels so configured providers stay honest
17
19
  *
18
20
  * β†’ Functions:
19
21
  * - `setActiveProxy` β€” Provide the active proxy instance for footer status rendering
@@ -434,6 +436,11 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
434
436
  // πŸ“– Server responded but needs an API key β€” shown dimly since it IS reachable
435
437
  statusText = `πŸ”‘ NO KEY`
436
438
  statusColor = (s) => chalk.dim(s)
439
+ } else if (r.status === 'auth_error') {
440
+ // πŸ“– A key is configured but the provider rejected it β€” keep this distinct
441
+ // πŸ“– from "no key" so configured-only mode does not look misleading.
442
+ statusText = `πŸ” AUTH FAIL`
443
+ statusColor = (s) => chalk.redBright(s)
437
444
  } else if (r.status === 'pending') {
438
445
  statusText = `${FRAMES[frame % FRAMES.length]} wait`
439
446
  statusColor = (s) => chalk.dim.yellow(s)
@@ -635,8 +642,8 @@ export function renderTable(results, pendingPings, frame, cursor = null, sortCol
635
642
  chalk.dim(` β€’ `) +
636
643
  hotkey('K', ' Help')
637
644
  )
638
- // πŸ“– Line 2: profiles, recommend, feature request, bug report, and extended hints β€” gives visibility to less-obvious features
639
- lines.push(chalk.dim(` `) + hotkey('⇧P', ' Cycle profile') + chalk.dim(` β€’ `) + hotkey('⇧S', ' Save profile') + chalk.dim(` β€’ `) + hotkey('Q', ' Smart Recommend') + chalk.dim(` β€’ `) + hotkey('J', ' Request feature') + chalk.dim(` β€’ `) + hotkey('I', ' Report bug'))
645
+ // πŸ“– Line 2: profiles, install flow, recommend, feature request, bug report, and extended hints.
646
+ lines.push(chalk.dim(` `) + hotkey('⇧P', ' Cycle profile') + chalk.dim(` β€’ `) + hotkey('⇧S', ' Save profile') + chalk.dim(` β€’ `) + hotkey('Y', ' Install endpoints') + chalk.dim(` β€’ `) + hotkey('Q', ' Smart Recommend') + chalk.dim(` β€’ `) + hotkey('J', ' Request feature') + chalk.dim(` β€’ `) + hotkey('I', ' Report bug'))
640
647
  // πŸ“– Proxy status line β€” always rendered with explicit state (starting/running/failed/stopped)
641
648
  lines.push(renderProxyStatusLine(proxyStartupStatus, activeProxyRef, proxyEnabled))
642
649
  if (versionStatus.isOutdated) {
package/src/utils.js CHANGED
@@ -218,9 +218,11 @@ export const getStabilityScore = (r) => {
218
218
  // πŸ“– sortResults: Sort the results array by any column the user can click/press in the TUI.
219
219
  // πŸ“– Returns a NEW array β€” never mutates the original (important for React-style re-renders).
220
220
  //
221
- // πŸ“– Supported columns (matching the keyboard shortcuts in the TUI):
221
+ // πŸ“– Supported columns in the sorter.
222
+ // πŸ“– Most map directly to visible TUI sort hotkeys; `tier` remains available internally
223
+ // πŸ“– even though the live TUI now reserves `Y` for the install-endpoints flow.
222
224
  // - 'rank' (R key) β€” original index from sources.js
223
- // - 'tier' (T key) β€” tier hierarchy (S+ first, C last)
225
+ // - 'tier' (internal) β€” tier hierarchy (S+ first, C last)
224
226
  // - 'origin' (O key) β€” provider name (all NIM for now, future-proofed)
225
227
  // - 'model' (M key) β€” alphabetical by display label
226
228
  // - 'ping' (L key) β€” last ping latency (only successful ones count)