free-coding-models 0.1.75 β†’ 0.1.77

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 CHANGED
@@ -78,6 +78,8 @@
78
78
  - **πŸ”€ Startup mode menu** β€” Choose between OpenCode and OpenClaw before the TUI launches
79
79
  - **πŸ’» OpenCode integration** β€” Auto-detects NIM setup, sets model as default, launches OpenCode
80
80
  - **🦞 OpenClaw integration** β€” Sets selected model as default provider in `~/.openclaw/openclaw.json`
81
+ - **πŸ“ Feature Request (J key)** β€” Send anonymous feedback directly to the project team via a full-screen overlay with multi-line input (includes anonymous OS/terminal metadata in message footer only)
82
+ - **πŸ› Bug Report (I key)** β€” Send anonymous bug reports directly to the project team via a full-screen overlay with multi-line input (includes anonymous OS/terminal metadata in message footer only)
81
83
  - **🎨 Clean output** β€” Zero scrollback pollution, interface stays open until Ctrl+C
82
84
  - **πŸ“Ά Status indicators** β€” UP βœ… Β· No Key πŸ”‘ Β· Timeout ⏳ Β· Overloaded πŸ”₯ Β· Not Found 🚫
83
85
  - **πŸ” Keyless latency** β€” Models are pinged even without an API key β€” a `πŸ”‘ NO KEY` status confirms the server is reachable with real latency shown, so you can compare providers before committing to a key
@@ -123,6 +123,102 @@ const TELEMETRY_CONSENT_ASCII = [
123
123
  const POSTHOG_PROJECT_KEY_DEFAULT = 'phc_5P1n8HaLof6nHM0tKJYt4bV5pj2XPb272fLVigwf1YQ'
124
124
  const POSTHOG_HOST_DEFAULT = 'https://eu.i.posthog.com'
125
125
 
126
+ // πŸ“– Discord feature request webhook configuration (anonymous feedback system)
127
+ const DISCORD_WEBHOOK_URL = 'https://discord.com/api/webhooks/1476709155992764427/hmnHNtpducvi5LClhv8DynENjUmmg9q8HI1Bx1lNix56UHqrqZf55rW95LGvNJ2W4j7D'
128
+ const DISCORD_BOT_NAME = 'TUI - Feature Requests'
129
+ const DISCORD_EMBED_COLOR = 0x39FF14 // Vert fluo (RGB: 57, 255, 20)
130
+
131
+ // πŸ“– sendFeatureRequest: Send anonymous feature request to Discord via webhook
132
+ // πŸ“– Called when user presses J key, types message, and presses Enter
133
+ // πŸ“– Returns success/error status for UI feedback
134
+ async function sendFeatureRequest(message) {
135
+ try {
136
+ // πŸ“– Collect anonymous telemetry for context (no personal data)
137
+ const system = getTelemetrySystem()
138
+ const terminal = getTelemetryTerminal()
139
+ const nodeVersion = process.version
140
+ const arch = process.arch
141
+ const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || 'Unknown'
142
+
143
+ // πŸ“– Build Discord embed with rich metadata in footer (compact format)
144
+ const embed = {
145
+ description: message,
146
+ color: DISCORD_EMBED_COLOR,
147
+ timestamp: new Date().toISOString(),
148
+ footer: {
149
+ text: `v${LOCAL_VERSION} β€’ ${system} β€’ ${terminal} β€’ ${nodeVersion} β€’ ${arch} β€’ ${timezone}`
150
+ }
151
+ }
152
+
153
+ const response = await fetch(DISCORD_WEBHOOK_URL, {
154
+ method: 'POST',
155
+ headers: { 'content-type': 'application/json' },
156
+ body: JSON.stringify({
157
+ username: DISCORD_BOT_NAME,
158
+ embeds: [embed]
159
+ }),
160
+ signal: AbortSignal.timeout(10000) // πŸ“– 10s timeout for webhook
161
+ })
162
+
163
+ if (!response.ok) {
164
+ throw new Error(`HTTP ${response.status}`)
165
+ }
166
+
167
+ return { success: true, error: null }
168
+ } catch (error) {
169
+ const message = error instanceof Error ? error.message : 'Unknown error'
170
+ return { success: false, error: message }
171
+ }
172
+ }
173
+
174
+ // πŸ“– Discord bug report webhook configuration (anonymous bug reports)
175
+ const DISCORD_BUG_WEBHOOK_URL = 'https://discord.com/api/webhooks/1476715954409963743/5cOLf7U_891f1jwxRBLIp2RIP9xYhr4rWtOhipzKKwVdFVl1Bj89X_fB6I_uGXZiGT9E'
176
+ const DISCORD_BUG_BOT_NAME = 'TUI Bug Report'
177
+ const DISCORD_BUG_EMBED_COLOR = 0xFF5733 // Rouge (RGB: 255, 87, 51)
178
+
179
+ // πŸ“– sendBugReport: Send anonymous bug report to Discord via webhook
180
+ // πŸ“– Called when user presses I key, types message, and presses Enter
181
+ // πŸ“– Returns success/error status for UI feedback
182
+ async function sendBugReport(message) {
183
+ try {
184
+ // πŸ“– Collect anonymous telemetry for context (no personal data)
185
+ const system = getTelemetrySystem()
186
+ const terminal = getTelemetryTerminal()
187
+ const nodeVersion = process.version
188
+ const arch = process.arch
189
+ const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || 'Unknown'
190
+
191
+ // πŸ“– Build Discord embed with rich metadata in footer (compact format)
192
+ const embed = {
193
+ description: message,
194
+ color: DISCORD_BUG_EMBED_COLOR,
195
+ timestamp: new Date().toISOString(),
196
+ footer: {
197
+ text: `v${LOCAL_VERSION} β€’ ${system} β€’ ${terminal} β€’ ${nodeVersion} β€’ ${arch} β€’ ${timezone}`
198
+ }
199
+ }
200
+
201
+ const response = await fetch(DISCORD_BUG_WEBHOOK_URL, {
202
+ method: 'POST',
203
+ headers: { 'content-type': 'application/json' },
204
+ body: JSON.stringify({
205
+ username: DISCORD_BUG_BOT_NAME,
206
+ embeds: [embed]
207
+ }),
208
+ signal: AbortSignal.timeout(10000) // πŸ“– 10s timeout for webhook
209
+ })
210
+
211
+ if (!response.ok) {
212
+ throw new Error(`HTTP ${response.status}`)
213
+ }
214
+
215
+ return { success: true, error: null }
216
+ } catch (error) {
217
+ const message = error instanceof Error ? error.message : 'Unknown error'
218
+ return { success: false, error: message }
219
+ }
220
+ }
221
+
126
222
  // πŸ“– parseTelemetryEnv: Convert env var strings into booleans.
127
223
  // πŸ“– Returns true/false when value is recognized, otherwise null.
128
224
  function parseTelemetryEnv(value) {
@@ -1290,8 +1386,8 @@ function renderTable(results, pendingPings, frame, cursor = null, sortColumn = '
1290
1386
  : chalk.rgb(0, 200, 255)('Enterβ†’OpenCode')
1291
1387
  // πŸ“– Line 1: core navigation + sorting shortcuts
1292
1388
  lines.push(chalk.dim(` ↑↓ Navigate β€’ `) + actionHint + chalk.dim(` β€’ `) + chalk.yellow('F') + chalk.dim(` Favorite β€’ R/Y/O/M/L/A/S/C/H/V/B/U Sort β€’ `) + chalk.yellow('T') + chalk.dim(` Tier β€’ `) + chalk.yellow('N') + chalk.dim(` Origin β€’ W↓/X↑ (${intervalSec}s) β€’ `) + chalk.rgb(255, 100, 50).bold('Z') + chalk.dim(` Mode β€’ `) + chalk.yellow('P') + chalk.dim(` Settings β€’ `) + chalk.rgb(0, 255, 80).bold('K') + chalk.dim(` Help`))
1293
- // πŸ“– Line 2: profiles, recommend, and extended hints β€” gives visibility to less-obvious features
1294
- lines.push(chalk.dim(` `) + chalk.rgb(200, 150, 255).bold('⇧P') + chalk.dim(` Cycle profile β€’ `) + chalk.rgb(200, 150, 255).bold('⇧S') + chalk.dim(` Save profile β€’ `) + chalk.rgb(0, 200, 180).bold('Q') + chalk.dim(` Smart Recommend β€’ `) + chalk.yellow('E') + chalk.dim(`/`) + chalk.yellow('D') + chalk.dim(` Tier ↑↓ β€’ `) + chalk.yellow('Esc') + chalk.dim(` Close overlay β€’ Ctrl+C Exit`))
1389
+ // πŸ“– Line 2: profiles, recommend, feature request, bug report, and extended hints β€” gives visibility to less-obvious features
1390
+ lines.push(chalk.dim(` `) + chalk.rgb(200, 150, 255).bold('⇧P') + chalk.dim(` Cycle profile β€’ `) + chalk.rgb(200, 150, 255).bold('⇧S') + chalk.dim(` Save profile β€’ `) + chalk.rgb(0, 200, 180).bold('Q') + chalk.dim(` Smart Recommend β€’ `) + chalk.rgb(57, 255, 20).bold('J') + chalk.dim(` Request feature β€’ `) + chalk.rgb(255, 87, 51).bold('I') + chalk.dim(` Report bug β€’ `) + chalk.yellow('E') + chalk.dim(`/`) + chalk.yellow('D') + chalk.dim(` Tier ↑↓ β€’ `) + chalk.yellow('Esc') + chalk.dim(` Close overlay β€’ Ctrl+C Exit`))
1295
1391
  lines.push('')
1296
1392
  lines.push(
1297
1393
  chalk.rgb(255, 150, 200)(' Made with πŸ’– & β˜• by \x1b]8;;https://github.com/vava-nessa\x1b\\vava-nessa\x1b]8;;\x1b\\') +
@@ -2817,6 +2913,16 @@ async function main() {
2817
2913
  activeProfile: getActiveProfileName(config), // πŸ“– Currently loaded profile name (or null)
2818
2914
  profileSaveMode: false, // πŸ“– Whether the inline "Save profile" name input is active
2819
2915
  profileSaveBuffer: '', // πŸ“– Typed characters for the profile name being saved
2916
+ // πŸ“– Feature Request state (J key opens it)
2917
+ featureRequestOpen: false, // πŸ“– Whether the feature request overlay is active
2918
+ featureRequestBuffer: '', // πŸ“– Typed characters for the feature request message
2919
+ featureRequestStatus: 'idle', // πŸ“– 'idle'|'sending'|'success'|'error' β€” webhook send status
2920
+ featureRequestError: null, // πŸ“– Last webhook error message
2921
+ // πŸ“– Bug Report state (I key opens it)
2922
+ bugReportOpen: false, // πŸ“– Whether the bug report overlay is active
2923
+ bugReportBuffer: '', // πŸ“– Typed characters for the bug report message
2924
+ bugReportStatus: 'idle', // πŸ“– 'idle'|'sending'|'success'|'error' β€” webhook send status
2925
+ bugReportError: null, // πŸ“– Last webhook error message
2820
2926
  }
2821
2927
 
2822
2928
  // πŸ“– Re-clamp viewport on terminal resize
@@ -3093,6 +3199,8 @@ async function main() {
3093
3199
  lines.push(` ${chalk.yellow('Z')} Cycle launch mode ${chalk.dim('(OpenCode CLI β†’ OpenCode Desktop β†’ OpenClaw)')}`)
3094
3200
  lines.push(` ${chalk.yellow('F')} Toggle favorite on selected row ${chalk.dim('(⭐ pinned at top, persisted)')}`)
3095
3201
  lines.push(` ${chalk.yellow('Q')} Smart Recommend ${chalk.dim('(🎯 find the best model for your task β€” questionnaire + live analysis)')}`)
3202
+ lines.push(` ${chalk.rgb(57, 255, 20).bold('J')} Request Feature ${chalk.dim('(πŸ“ send anonymous feedback to the project team)')}`)
3203
+ lines.push(` ${chalk.rgb(255, 87, 51).bold('I')} Report Bug ${chalk.dim('(πŸ› send anonymous bug report to the project team)')}`)
3096
3204
  lines.push(` ${chalk.yellow('P')} Open settings ${chalk.dim('(manage API keys, provider toggles, analytics, manual update)')}`)
3097
3205
  lines.push(` ${chalk.yellow('Shift+P')} Cycle config profile ${chalk.dim('(switch between saved profiles live)')}`)
3098
3206
  lines.push(` ${chalk.yellow('Shift+S')} Save current config as a named profile ${chalk.dim('(inline prompt β€” type name + Enter)')}`)
@@ -3331,6 +3439,218 @@ async function main() {
3331
3439
  }, PING_RATE)
3332
3440
  }
3333
3441
 
3442
+ // ─── Feature Request overlay renderer ─────────────────────────────────────
3443
+ // πŸ“– renderFeatureRequest: Draw the overlay for anonymous Discord feedback.
3444
+ // πŸ“– Shows an input field where users can type feature requests, then sends to Discord webhook.
3445
+ function renderFeatureRequest() {
3446
+ const EL = '\x1b[K'
3447
+ const lines = []
3448
+
3449
+ // πŸ“– Calculate available space for multi-line input
3450
+ const maxInputWidth = OVERLAY_PANEL_WIDTH - 8 // 8 = padding (4 spaces each side)
3451
+ const maxInputLines = 10 // Show up to 10 lines of input
3452
+
3453
+ // πŸ“– Split buffer into lines for display (with wrapping)
3454
+ const wrapText = (text, width) => {
3455
+ const words = text.split(' ')
3456
+ const lines = []
3457
+ let currentLine = ''
3458
+
3459
+ for (const word of words) {
3460
+ const testLine = currentLine ? currentLine + ' ' + word : word
3461
+ if (testLine.length <= width) {
3462
+ currentLine = testLine
3463
+ } else {
3464
+ if (currentLine) lines.push(currentLine)
3465
+ currentLine = word
3466
+ }
3467
+ }
3468
+ if (currentLine) lines.push(currentLine)
3469
+ return lines
3470
+ }
3471
+
3472
+ const inputLines = wrapText(state.featureRequestBuffer, maxInputWidth)
3473
+ const displayLines = inputLines.slice(0, maxInputLines)
3474
+
3475
+ // πŸ“– Header
3476
+ lines.push('')
3477
+ lines.push(` ${chalk.bold.rgb(57, 255, 20)('πŸ“ Feature Request')} ${chalk.dim('β€” send anonymous feedback to the project team')}`)
3478
+ lines.push('')
3479
+
3480
+ // πŸ“– Status messages (if any)
3481
+ if (state.featureRequestStatus === 'sending') {
3482
+ lines.push(` ${chalk.yellow('⏳ Sending...')}`)
3483
+ lines.push('')
3484
+ } else if (state.featureRequestStatus === 'success') {
3485
+ lines.push(` ${chalk.greenBright.bold('βœ… Successfully sent!')} ${chalk.dim('Closing overlay in 3 seconds...')}`)
3486
+ lines.push('')
3487
+ lines.push(` ${chalk.dim('Thank you for your feedback! Your feature request has been sent to the project team.')}`)
3488
+ lines.push('')
3489
+ } else if (state.featureRequestStatus === 'error') {
3490
+ lines.push(` ${chalk.red('❌ Error:')} ${chalk.yellow(state.featureRequestError || 'Failed to send')}`)
3491
+ lines.push(` ${chalk.dim('Press Backspace to edit, or Esc to close')}`)
3492
+ lines.push('')
3493
+ } else {
3494
+ lines.push(` ${chalk.dim('Type your feature request below. Press Enter to send, Esc to cancel.')}`)
3495
+ lines.push(` ${chalk.dim('Your message will be sent anonymously to the project team.')}`)
3496
+ lines.push('')
3497
+ }
3498
+
3499
+ // πŸ“– Input box with border
3500
+ lines.push(chalk.dim(` β”Œβ”€ ${chalk.cyan('Message')} ${chalk.dim(`(${state.featureRequestBuffer.length}/500 chars)`)} ─${'─'.repeat(maxInputWidth - 22)}┐`))
3501
+
3502
+ // πŸ“– Display input lines (or placeholder if empty)
3503
+ if (displayLines.length === 0 && state.featureRequestStatus === 'idle') {
3504
+ lines.push(chalk.dim(` β”‚${' '.repeat(maxInputWidth)}β”‚`))
3505
+ lines.push(chalk.dim(` β”‚ ${chalk.white.italic('Type your message here...')}${' '.repeat(Math.max(0, maxInputWidth - 28))}β”‚`))
3506
+ } else {
3507
+ for (const line of displayLines) {
3508
+ const padded = line.padEnd(maxInputWidth)
3509
+ lines.push(` β”‚ ${chalk.white(padded)} β”‚`)
3510
+ }
3511
+ }
3512
+
3513
+ // πŸ“– Fill remaining space if needed
3514
+ const linesToFill = Math.max(0, maxInputLines - Math.max(displayLines.length, 1))
3515
+ for (let i = 0; i < linesToFill; i++) {
3516
+ lines.push(chalk.dim(` β”‚${' '.repeat(maxInputWidth)}β”‚`))
3517
+ }
3518
+
3519
+ // πŸ“– Cursor indicator (only when not sending/success)
3520
+ if (state.featureRequestStatus === 'idle' || state.featureRequestStatus === 'error') {
3521
+ const cursorLine = inputLines.length > 0 ? inputLines.length - 1 : 0
3522
+ const lastDisplayLine = displayLines.length - 1
3523
+ // Add cursor indicator to the last line
3524
+ if (lines.length > 0 && displayLines.length > 0) {
3525
+ const lastLineIdx = lines.findIndex(l => l.includes('β”‚ ') && !l.includes('Message'))
3526
+ if (lastLineIdx >= 0 && lastLineIdx < lines.length) {
3527
+ // Add cursor blink
3528
+ const lastLine = lines[lastLineIdx]
3529
+ if (lastLine.includes('β”‚')) {
3530
+ lines[lastLineIdx] = lastLine.replace(/\s+β”‚$/, chalk.rgb(57, 255, 20).bold('▏') + ' β”‚')
3531
+ }
3532
+ }
3533
+ }
3534
+ }
3535
+
3536
+ lines.push(chalk.dim(` β””${'─'.repeat(maxInputWidth + 2)}β”˜`))
3537
+
3538
+ lines.push('')
3539
+ lines.push(chalk.dim(' Enter Send β€’ Esc Cancel β€’ Backspace Delete'))
3540
+
3541
+ // πŸ“– Apply overlay tint and return
3542
+ const FEATURE_REQUEST_OVERLAY_BG = chalk.bgRgb(26, 26, 46) // Dark blue-ish background (RGB: 26, 26, 46)
3543
+ const tintedLines = tintOverlayLines(lines, FEATURE_REQUEST_OVERLAY_BG)
3544
+ const cleared = tintedLines.map(l => l + EL)
3545
+ return cleared.join('\n')
3546
+ }
3547
+
3548
+ // ─── Bug Report overlay renderer ─────────────────────────────────────────
3549
+ // πŸ“– renderBugReport: Draw the overlay for anonymous Discord bug reports.
3550
+ // πŸ“– Shows an input field where users can type bug reports, then sends to Discord webhook.
3551
+ function renderBugReport() {
3552
+ const EL = '\x1b[K'
3553
+ const lines = []
3554
+
3555
+ // πŸ“– Calculate available space for multi-line input
3556
+ const maxInputWidth = OVERLAY_PANEL_WIDTH - 8 // 8 = padding (4 spaces each side)
3557
+ const maxInputLines = 10 // Show up to 10 lines of input
3558
+
3559
+ // πŸ“– Split buffer into lines for display (with wrapping)
3560
+ const wrapText = (text, width) => {
3561
+ const words = text.split(' ')
3562
+ const lines = []
3563
+ let currentLine = ''
3564
+
3565
+ for (const word of words) {
3566
+ const testLine = currentLine ? currentLine + ' ' + word : word
3567
+ if (testLine.length <= width) {
3568
+ currentLine = testLine
3569
+ } else {
3570
+ if (currentLine) lines.push(currentLine)
3571
+ currentLine = word
3572
+ }
3573
+ }
3574
+ if (currentLine) lines.push(currentLine)
3575
+ return lines
3576
+ }
3577
+
3578
+ const inputLines = wrapText(state.bugReportBuffer, maxInputWidth)
3579
+ const displayLines = inputLines.slice(0, maxInputLines)
3580
+
3581
+ // πŸ“– Header
3582
+ lines.push('')
3583
+ lines.push(` ${chalk.bold.rgb(255, 87, 51)('πŸ› Bug Report')} ${chalk.dim('β€” send anonymous bug reports to the project team')}`)
3584
+ lines.push('')
3585
+
3586
+ // πŸ“– Status messages (if any)
3587
+ if (state.bugReportStatus === 'sending') {
3588
+ lines.push(` ${chalk.yellow('⏳ Sending...')}`)
3589
+ lines.push('')
3590
+ } else if (state.bugReportStatus === 'success') {
3591
+ lines.push(` ${chalk.greenBright.bold('βœ… Successfully sent!')} ${chalk.dim('Closing overlay in 3 seconds...')}`)
3592
+ lines.push('')
3593
+ lines.push(` ${chalk.dim('Thank you for your feedback! Your bug report has been sent to the project team.')}`)
3594
+ lines.push('')
3595
+ } else if (state.bugReportStatus === 'error') {
3596
+ lines.push(` ${chalk.red('❌ Error:')} ${chalk.yellow(state.bugReportError || 'Failed to send')}`)
3597
+ lines.push(` ${chalk.dim('Press Backspace to edit, or Esc to close')}`)
3598
+ lines.push('')
3599
+ } else {
3600
+ lines.push(` ${chalk.dim('Describe the bug you encountered. Press Enter to send, Esc to cancel.')}`)
3601
+ lines.push(` ${chalk.dim('Your message will be sent anonymously to the project team.')}`)
3602
+ lines.push('')
3603
+ }
3604
+
3605
+ // πŸ“– Input box with border
3606
+ lines.push(chalk.dim(` β”Œβ”€ ${chalk.cyan('Bug Details')} ${chalk.dim(`(${state.bugReportBuffer.length}/500 chars)`)} ─${'─'.repeat(maxInputWidth - 24)}┐`))
3607
+
3608
+ // πŸ“– Display input lines (or placeholder if empty)
3609
+ if (displayLines.length === 0 && state.bugReportStatus === 'idle') {
3610
+ lines.push(chalk.dim(` β”‚${' '.repeat(maxInputWidth)}β”‚`))
3611
+ lines.push(chalk.dim(` β”‚ ${chalk.white.italic('Describe what happened...')}${' '.repeat(Math.max(0, maxInputWidth - 31))}β”‚`))
3612
+ } else {
3613
+ for (const line of displayLines) {
3614
+ const padded = line.padEnd(maxInputWidth)
3615
+ lines.push(` β”‚ ${chalk.white(padded)} β”‚`)
3616
+ }
3617
+ }
3618
+
3619
+ // πŸ“– Fill remaining space if needed
3620
+ const linesToFill = Math.max(0, maxInputLines - Math.max(displayLines.length, 1))
3621
+ for (let i = 0; i < linesToFill; i++) {
3622
+ lines.push(chalk.dim(` β”‚${' '.repeat(maxInputWidth)}β”‚`))
3623
+ }
3624
+
3625
+ // πŸ“– Cursor indicator (only when not sending/success)
3626
+ if (state.bugReportStatus === 'idle' || state.bugReportStatus === 'error') {
3627
+ const cursorLine = inputLines.length > 0 ? inputLines.length - 1 : 0
3628
+ const lastDisplayLine = displayLines.length - 1
3629
+ // Add cursor indicator to the last line
3630
+ if (lines.length > 0 && displayLines.length > 0) {
3631
+ const lastLineIdx = lines.findIndex(l => l.includes('β”‚ ') && !l.includes('Bug Details'))
3632
+ if (lastLineIdx >= 0 && lastLineIdx < lines.length) {
3633
+ // Add cursor blink
3634
+ const lastLine = lines[lastLineIdx]
3635
+ if (lastLine.includes('β”‚')) {
3636
+ lines[lastLineIdx] = lastLine.replace(/\s+β”‚$/, chalk.rgb(255, 87, 51).bold('▏') + ' β”‚')
3637
+ }
3638
+ }
3639
+ }
3640
+ }
3641
+
3642
+ lines.push(chalk.dim(` β””${'─'.repeat(maxInputWidth + 2)}β”˜`))
3643
+
3644
+ lines.push('')
3645
+ lines.push(chalk.dim(' Enter Send β€’ Esc Cancel β€’ Backspace Delete'))
3646
+
3647
+ // πŸ“– Apply overlay tint and return
3648
+ const BUG_REPORT_OVERLAY_BG = chalk.bgRgb(46, 20, 20) // Dark red-ish background (RGB: 46, 20, 20)
3649
+ const tintedLines = tintOverlayLines(lines, BUG_REPORT_OVERLAY_BG)
3650
+ const cleared = tintedLines.map(l => l + EL)
3651
+ return cleared.join('\n')
3652
+ }
3653
+
3334
3654
  // πŸ“– stopRecommendAnalysis: cleanup timers if user cancels during analysis
3335
3655
  function stopRecommendAnalysis() {
3336
3656
  if (state.recommendAnalysisTimer) { clearInterval(state.recommendAnalysisTimer); state.recommendAnalysisTimer = null }
@@ -3447,6 +3767,140 @@ async function main() {
3447
3767
  return
3448
3768
  }
3449
3769
 
3770
+ // πŸ“– Feature Request overlay: intercept ALL keys while overlay is active.
3771
+ // πŸ“– Enter β†’ send to Discord, Esc β†’ cancel, Backspace β†’ delete char, printable β†’ append to buffer.
3772
+ if (state.featureRequestOpen) {
3773
+ if (key.ctrl && key.name === 'c') { exit(0); return }
3774
+
3775
+ if (key.name === 'escape') {
3776
+ // πŸ“– Cancel feature request β€” close overlay
3777
+ state.featureRequestOpen = false
3778
+ state.featureRequestBuffer = ''
3779
+ state.featureRequestStatus = 'idle'
3780
+ state.featureRequestError = null
3781
+ return
3782
+ }
3783
+
3784
+ if (key.name === 'return') {
3785
+ // πŸ“– Send feature request to Discord webhook
3786
+ const message = state.featureRequestBuffer.trim()
3787
+ if (message.length > 0 && state.featureRequestStatus !== 'sending') {
3788
+ state.featureRequestStatus = 'sending'
3789
+ const result = await sendFeatureRequest(message)
3790
+ if (result.success) {
3791
+ // πŸ“– Success β€” show confirmation briefly, then close overlay after 3 seconds
3792
+ state.featureRequestStatus = 'success'
3793
+ setTimeout(() => {
3794
+ state.featureRequestOpen = false
3795
+ state.featureRequestBuffer = ''
3796
+ state.featureRequestStatus = 'idle'
3797
+ state.featureRequestError = null
3798
+ }, 3000)
3799
+ } else {
3800
+ // πŸ“– Error β€” show error message, keep overlay open
3801
+ state.featureRequestStatus = 'error'
3802
+ state.featureRequestError = result.error || 'Unknown error'
3803
+ }
3804
+ }
3805
+ return
3806
+ }
3807
+
3808
+ if (key.name === 'backspace') {
3809
+ // πŸ“– Don't allow editing while sending or after success
3810
+ if (state.featureRequestStatus === 'sending' || state.featureRequestStatus === 'success') return
3811
+ state.featureRequestBuffer = state.featureRequestBuffer.slice(0, -1)
3812
+ // πŸ“– Clear error status when user starts editing again
3813
+ if (state.featureRequestStatus === 'error') {
3814
+ state.featureRequestStatus = 'idle'
3815
+ state.featureRequestError = null
3816
+ }
3817
+ return
3818
+ }
3819
+
3820
+ // πŸ“– Append printable characters (str is the raw character typed)
3821
+ // πŸ“– Limit to 500 characters (Discord embed description limit)
3822
+ if (str && str.length === 1 && !key.ctrl && !key.meta) {
3823
+ // πŸ“– Don't allow editing while sending or after success
3824
+ if (state.featureRequestStatus === 'sending' || state.featureRequestStatus === 'success') return
3825
+ if (state.featureRequestBuffer.length < 500) {
3826
+ state.featureRequestBuffer += str
3827
+ // πŸ“– Clear error status when user starts editing again
3828
+ if (state.featureRequestStatus === 'error') {
3829
+ state.featureRequestStatus = 'idle'
3830
+ state.featureRequestError = null
3831
+ }
3832
+ }
3833
+ }
3834
+ return
3835
+ }
3836
+
3837
+ // πŸ“– Bug Report overlay: intercept ALL keys while overlay is active.
3838
+ // πŸ“– Enter β†’ send to Discord, Esc β†’ cancel, Backspace β†’ delete char, printable β†’ append to buffer.
3839
+ if (state.bugReportOpen) {
3840
+ if (key.ctrl && key.name === 'c') { exit(0); return }
3841
+
3842
+ if (key.name === 'escape') {
3843
+ // πŸ“– Cancel bug report β€” close overlay
3844
+ state.bugReportOpen = false
3845
+ state.bugReportBuffer = ''
3846
+ state.bugReportStatus = 'idle'
3847
+ state.bugReportError = null
3848
+ return
3849
+ }
3850
+
3851
+ if (key.name === 'return') {
3852
+ // πŸ“– Send bug report to Discord webhook
3853
+ const message = state.bugReportBuffer.trim()
3854
+ if (message.length > 0 && state.bugReportStatus !== 'sending') {
3855
+ state.bugReportStatus = 'sending'
3856
+ const result = await sendBugReport(message)
3857
+ if (result.success) {
3858
+ // πŸ“– Success β€” show confirmation briefly, then close overlay after 3 seconds
3859
+ state.bugReportStatus = 'success'
3860
+ setTimeout(() => {
3861
+ state.bugReportOpen = false
3862
+ state.bugReportBuffer = ''
3863
+ state.bugReportStatus = 'idle'
3864
+ state.bugReportError = null
3865
+ }, 3000)
3866
+ } else {
3867
+ // πŸ“– Error β€” show error message, keep overlay open
3868
+ state.bugReportStatus = 'error'
3869
+ state.bugReportError = result.error || 'Unknown error'
3870
+ }
3871
+ }
3872
+ return
3873
+ }
3874
+
3875
+ if (key.name === 'backspace') {
3876
+ // πŸ“– Don't allow editing while sending or after success
3877
+ if (state.bugReportStatus === 'sending' || state.bugReportStatus === 'success') return
3878
+ state.bugReportBuffer = state.bugReportBuffer.slice(0, -1)
3879
+ // πŸ“– Clear error status when user starts editing again
3880
+ if (state.bugReportStatus === 'error') {
3881
+ state.bugReportStatus = 'idle'
3882
+ state.bugReportError = null
3883
+ }
3884
+ return
3885
+ }
3886
+
3887
+ // πŸ“– Append printable characters (str is the raw character typed)
3888
+ // πŸ“– Limit to 500 characters (Discord embed description limit)
3889
+ if (str && str.length === 1 && !key.ctrl && !key.meta) {
3890
+ // πŸ“– Don't allow editing while sending or after success
3891
+ if (state.bugReportStatus === 'sending' || state.bugReportStatus === 'success') return
3892
+ if (state.bugReportBuffer.length < 500) {
3893
+ state.bugReportBuffer += str
3894
+ // πŸ“– Clear error status when user starts editing again
3895
+ if (state.bugReportStatus === 'error') {
3896
+ state.bugReportStatus = 'idle'
3897
+ state.bugReportError = null
3898
+ }
3899
+ }
3900
+ }
3901
+ return
3902
+ }
3903
+
3450
3904
  // πŸ“– Help overlay: full keyboard navigation + key swallowing while overlay is open.
3451
3905
  if (state.helpVisible) {
3452
3906
  const pageStep = Math.max(1, (state.terminalRows || 1) - 2)
@@ -3919,6 +4373,24 @@ async function main() {
3919
4373
  return
3920
4374
  }
3921
4375
 
4376
+ // πŸ“– J key: open Feature Request overlay (anonymous Discord feedback)
4377
+ if (key.name === 'j') {
4378
+ state.featureRequestOpen = true
4379
+ state.featureRequestBuffer = ''
4380
+ state.featureRequestStatus = 'idle'
4381
+ state.featureRequestError = null
4382
+ return
4383
+ }
4384
+
4385
+ // πŸ“– I key: open Bug Report overlay (anonymous Discord bug reports)
4386
+ if (key.name === 'i') {
4387
+ state.bugReportOpen = true
4388
+ state.bugReportBuffer = ''
4389
+ state.bugReportStatus = 'idle'
4390
+ state.bugReportError = null
4391
+ return
4392
+ }
4393
+
3922
4394
  // πŸ“– Interval adjustment keys: W=decrease (faster), X=increase (slower)
3923
4395
  // πŸ“– Minimum 1s, maximum 60s
3924
4396
  if (key.name === 'w') {
@@ -4052,11 +4524,11 @@ async function main() {
4052
4524
 
4053
4525
  process.stdin.on('keypress', onKeyPress)
4054
4526
 
4055
- // πŸ“– Animation loop: render settings overlay, recommend overlay, help overlay, OR main table
4527
+ // πŸ“– Animation loop: render settings overlay, recommend overlay, help overlay, feature request overlay, bug report overlay, OR main table
4056
4528
  const ticker = setInterval(() => {
4057
4529
  state.frame++
4058
4530
  // πŸ“– Cache visible+sorted models each frame so Enter handler always matches the display
4059
- if (!state.settingsOpen && !state.recommendOpen) {
4531
+ if (!state.settingsOpen && !state.recommendOpen && !state.featureRequestOpen && !state.bugReportOpen) {
4060
4532
  const visible = state.results.filter(r => !r.hidden)
4061
4533
  state.visibleSorted = sortResultsWithPinnedFavorites(visible, state.sortColumn, state.sortDirection)
4062
4534
  }
@@ -4064,9 +4536,13 @@ async function main() {
4064
4536
  ? renderSettings()
4065
4537
  : state.recommendOpen
4066
4538
  ? renderRecommend()
4067
- : state.helpVisible
4068
- ? renderHelp()
4069
- : renderTable(state.results, state.pendingPings, state.frame, state.cursor, state.sortColumn, state.sortDirection, state.pingInterval, state.lastPingTime, state.mode, tierFilterMode, state.scrollOffset, state.terminalRows, originFilterMode, state.activeProfile, state.profileSaveMode, state.profileSaveBuffer)
4539
+ : state.featureRequestOpen
4540
+ ? renderFeatureRequest()
4541
+ : state.bugReportOpen
4542
+ ? renderBugReport()
4543
+ : state.helpVisible
4544
+ ? renderHelp()
4545
+ : renderTable(state.results, state.pendingPings, state.frame, state.cursor, state.sortColumn, state.sortDirection, state.pingInterval, state.lastPingTime, state.mode, tierFilterMode, state.scrollOffset, state.terminalRows, originFilterMode, state.activeProfile, state.profileSaveMode, state.profileSaveBuffer)
4070
4546
  process.stdout.write(ALT_HOME + content)
4071
4547
  }, Math.round(1000 / FPS))
4072
4548
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "free-coding-models",
3
- "version": "0.1.75",
3
+ "version": "0.1.77",
4
4
  "description": "Find the fastest coding LLM models in seconds β€” ping free models from multiple providers, pick the best one for OpenCode, Cursor, or any AI coding assistant.",
5
5
  "keywords": [
6
6
  "nvidia",