free-coding-models 0.5.28 → 0.5.29

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.
@@ -9,7 +9,7 @@ import {
9
9
  IconRoute, IconPlayerPlay, IconPlayerStop, IconRefresh,
10
10
  IconCopy, IconCheck, IconChevronDown, IconChevronUp,
11
11
  IconActivity, IconServer, IconPlus, IconX, IconGripVertical,
12
- IconArrowUp, IconArrowDown, IconTrash, IconList, IconWand,
12
+ IconArrowRight, IconArrowUp, IconArrowDown, IconTrash, IconList, IconWand,
13
13
  } from '@tabler/icons-react'
14
14
  import styles from './RouterView.module.css'
15
15
 
@@ -72,6 +72,7 @@ export default function RouterView({ onClose, onToast }) {
72
72
  const [pickerProvider, setPickerProvider] = useState('')
73
73
  const [saveStatus, setSaveStatus] = useState(SAVE_STATUS_IDLE)
74
74
  const saveTimerRef = useRef(null)
75
+ const hasAutoExpandedLog = useRef(false)
75
76
 
76
77
  const fetchStatus = useCallback(async () => {
77
78
  try {
@@ -133,10 +134,15 @@ export default function RouterView({ onClose, onToast }) {
133
134
  await fetchStatus()
134
135
  await fetchSets()
135
136
  } else {
136
- onToast?.(`Failed to start: ${data.error || 'unknown'}`, 'error')
137
+ onToast?.(`Failed to start: ${data.error || data.message || 'unknown'}`, 'error')
137
138
  }
138
139
  } catch (err) {
139
- onToast?.(`Start failed: ${err.message}`, 'error')
140
+ // 📖 Distinguish between "daemon not running" (fetch itself fails) and
141
+ // 📖 other network errors so the user gets an actionable error message.
142
+ const msg = err.name === 'TypeError' && err.message?.includes('fetch')
143
+ ? 'Cannot reach daemon — it may not be installed or the port is blocked'
144
+ : err.message
145
+ onToast?.(`Start failed: ${msg}`, 'error')
140
146
  } finally { setActionLoading(false) }
141
147
  }
142
148
 
@@ -418,6 +424,29 @@ export default function RouterView({ onClose, onToast }) {
418
424
  const circuitBreakers = stats?.circuitBreakers || {}
419
425
  const requestLog = stats?.requestLog || []
420
426
 
427
+ // 📖 routingOrder — the exact attempt order the daemon will use for the next
428
+ // 📖 request (priority-first among healthy models). routingOrder[0] is the
429
+ // 📖 model that will serve the next chat. Used to mark the "next" row and
430
+ // 📖 to label Primary vs Fallback semantics. Falls back to set order when the
431
+ // 📖 daemon isn't running or hasn't reported yet (so stopped state still shows
432
+ // 📖 a sensible priority chain). See issue #120.
433
+ const routingOrder = stats?.routingOrder || []
434
+ const nextToServeKey = routingOrder.length > 0 ? routingOrder[0].key : null
435
+
436
+ // 📖 Auto-expand the request log the first time requests appear.
437
+ useEffect(() => {
438
+ if (requestLog.length > 0 && !hasAutoExpandedLog.current) {
439
+ hasAutoExpandedLog.current = true
440
+ setLogExpanded(true)
441
+ }
442
+ }, [requestLog.length])
443
+
444
+ // 📖 Computed quick-setup display values: actual when running, defaults when stopped.
445
+ const qsBaseUrl = running && quickSetup?.baseUrl ? quickSetup.baseUrl : 'http://localhost:19280/v1'
446
+ const qsModel = quickSetup?.model || 'fcm'
447
+ const qsApiKey = 'fcm-local'
448
+ const qsAllText = `Base URL: ${qsBaseUrl}\nModel: ${qsModel}\nAPI Key: ${qsApiKey}`
449
+
421
450
  const sets = setsData?.sets || {}
422
451
  const setNames = Object.keys(sets).sort()
423
452
 
@@ -464,6 +493,41 @@ export default function RouterView({ onClose, onToast }) {
464
493
  </div>
465
494
  )}
466
495
 
496
+ {/* Quick Setup — ALWAYS visible, front and center */}
497
+ <div className={`${styles.quickSetup} ${running ? styles.quickSetupHero : ''}`}>
498
+ <h3 className={styles.sectionTitle}>
499
+ <IconCopy size={14} />
500
+ Quick Setup
501
+ <button className={styles.copyAllBtn} onClick={() => handleCopy(qsAllText, 'all')}>
502
+ {copied === 'all' ? <IconCheck size={12} /> : <IconCopy size={12} />}
503
+ {copied === 'all' ? 'Copied!' : 'Copy all'}
504
+ </button>
505
+ </h3>
506
+ <div className={styles.quickRows}>
507
+ <div className={styles.quickRow}>
508
+ <span className={styles.quickLabel}>Base URL</span>
509
+ <code className={styles.quickValue}>{qsBaseUrl}</code>
510
+ <button className={styles.copyBtn} onClick={() => handleCopy(qsBaseUrl, 'url')}>
511
+ {copied === 'url' ? <IconCheck size={12} /> : <IconCopy size={12} />}
512
+ </button>
513
+ </div>
514
+ <div className={styles.quickRow}>
515
+ <span className={styles.quickLabel}>Model</span>
516
+ <code className={styles.quickValue}>{qsModel}</code>
517
+ <button className={styles.copyBtn} onClick={() => handleCopy(qsModel, 'model')}>
518
+ {copied === 'model' ? <IconCheck size={12} /> : <IconCopy size={12} />}
519
+ </button>
520
+ </div>
521
+ <div className={styles.quickRow}>
522
+ <span className={styles.quickLabel}>API Key</span>
523
+ <code className={styles.quickValue}>{qsApiKey}</code>
524
+ <button className={styles.copyBtn} onClick={() => handleCopy(qsApiKey, 'key')}>
525
+ {copied === 'key' ? <IconCheck size={12} /> : <IconCopy size={12} />}
526
+ </button>
527
+ </div>
528
+ </div>
529
+ </div>
530
+
467
531
  {/* Hero Card */}
468
532
  <div className={`${styles.heroCard} ${running ? styles.heroRunning : styles.heroStopped}`}>
469
533
  <div className={styles.heroLeft}>
@@ -488,8 +552,8 @@ export default function RouterView({ onClose, onToast }) {
488
552
  </div>
489
553
  <div className={styles.heroActions}>
490
554
  {!running ? (
491
- <button className={styles.startBtn} onClick={handleStart} disabled={actionLoading}>
492
- <IconPlayerPlay size={14} />
555
+ <button className={`${styles.startBtn} ${styles.startBtnBig}`} onClick={handleStart} disabled={actionLoading}>
556
+ <IconPlayerPlay size={16} />
493
557
  {actionLoading ? 'Starting…' : 'Start Router'}
494
558
  </button>
495
559
  ) : (
@@ -504,34 +568,6 @@ export default function RouterView({ onClose, onToast }) {
504
568
  </div>
505
569
  </div>
506
570
 
507
- {/* Quick Setup */}
508
- {running && quickSetup && (
509
- <div className={styles.quickSetup}>
510
- <h3 className={styles.sectionTitle}>
511
- <IconCopy size={14} />
512
- Quick Setup
513
- </h3>
514
- <div className={styles.quickRows}>
515
- {quickSetup.baseUrl && (
516
- <div className={styles.quickRow}>
517
- <span className={styles.quickLabel}>Base URL</span>
518
- <code className={styles.quickValue}>{quickSetup.baseUrl}</code>
519
- <button className={styles.copyBtn} onClick={() => handleCopy(quickSetup.baseUrl, 'url')}>
520
- {copied === 'url' ? <IconCheck size={12} /> : <IconCopy size={12} />}
521
- </button>
522
- </div>
523
- )}
524
- <div className={styles.quickRow}>
525
- <span className={styles.quickLabel}>Model</span>
526
- <code className={styles.quickValue}>{quickSetup.model}</code>
527
- <button className={styles.copyBtn} onClick={() => handleCopy(quickSetup.model, 'model')}>
528
- {copied === 'model' ? <IconCheck size={12} /> : <IconCopy size={12} />}
529
- </button>
530
- </div>
531
- </div>
532
- </div>
533
- )}
534
-
535
571
  {/* Active Set Manager */}
536
572
  {running && (
537
573
  <div className={styles.section}>
@@ -580,20 +616,42 @@ export default function RouterView({ onClose, onToast }) {
580
616
 
581
617
  {localModels.length === 0 ? (
582
618
  <div className={styles.setEmpty}>
583
- The active set is empty. Add models with the button above to start routing.
619
+ <div className={styles.setEmptyTitle}>No models in the active set</div>
620
+ <div className={styles.setEmptyHint}>
621
+ Add models from the picker below, or click <strong>Sync best</strong> above to auto-pick working models.
622
+ </div>
584
623
  </div>
585
624
  ) : (
586
- <div className={styles.setList} onDragLeave={handleDragLeave}>
625
+ <>
626
+ {/* 📖 Priority legend — explains the fallback chain so users
627
+ understand WHY the top model serves every request (issue #120).
628
+ Higher priority = tried first; the rest are failover targets. */}
629
+ <div className={styles.priorityLegend}>
630
+ <span className={styles.legendPrimary}><IconArrowRight size={11} /> Primary</span>
631
+ <span className={styles.legendSeparator}>tries first</span>
632
+ <span className={styles.legendFallback}>Fallback</span>
633
+ <span className={styles.legendSeparator}>on failure / rate-limit</span>
634
+ {running && nextToServeKey && (
635
+ <span className={styles.legendNext}>Next up: <code>{nextToServeKey}</code></span>
636
+ )}
637
+ </div>
638
+ <div className={styles.setList} onDragLeave={handleDragLeave}>
587
639
  {localModels.map((m, idx) => {
588
640
  const key = `${m.provider}/${m.model}`
589
641
  const cb = circuitBreakers[key] || {}
590
642
  const isDragging = draggingKey === key
591
643
  const dropAbove = dropPosition?.key === key && dropPosition.side === 'above'
592
644
  const dropBelow = dropPosition?.key === key && dropPosition.side === 'below'
645
+ // 📖 Priority semantics for the UI: #1 is the Primary model the
646
+ // 📖 router tries first; everyone else is a Fallback. `isNext` is
647
+ // 📖 true for the exact model the daemon will serve next request —
648
+ // 📖 derived from /stats.routingOrder (priority-first, see #120).
649
+ const isPrimary = idx === 0
650
+ const isNext = running && nextToServeKey === key
593
651
  return (
594
652
  <div
595
653
  key={key}
596
- className={`${styles.setRow} ${isDragging ? styles.setRowDragging : ''} ${dropAbove ? `${styles.setRowDropTarget} ${styles.setRowDropTargetAbove}` : ''} ${dropBelow ? `${styles.setRowDropTarget} ${styles.setRowDropTargetBelow}` : ''}`}
654
+ className={`${styles.setRow} ${isDragging ? styles.setRowDragging : ''} ${isNext ? styles.setRowNext : ''} ${dropAbove ? `${styles.setRowDropTarget} ${styles.setRowDropTargetAbove}` : ''} ${dropBelow ? `${styles.setRowDropTarget} ${styles.setRowDropTargetBelow}` : ''}`}
597
655
  draggable
598
656
  onDragStart={(e) => handleDragStart(e, idx)}
599
657
  onDragOver={(e) => handleDragOver(e, idx)}
@@ -604,7 +662,12 @@ export default function RouterView({ onClose, onToast }) {
604
662
  <span className={styles.setDragHandle} aria-hidden>
605
663
  <IconGripVertical size={14} />
606
664
  </span>
607
- <span className={styles.setPriority}>#{idx + 1}</span>
665
+ <span
666
+ className={`${styles.setPriority} ${isPrimary ? styles.setPriorityPrimary : styles.setPriorityFallback}`}
667
+ title={isPrimary ? 'Primary model — tried first on every request' : `Fallback #${idx + 1} — used when higher-priority models fail or rate-limit`}
668
+ >
669
+ {isPrimary ? 'Primary' : `#${idx + 1}`}
670
+ </span>
608
671
  <span className={styles.setKey}>{key}</span>
609
672
  {m.tier && <span className={styles.setTier}>{m.tier}</span>}
610
673
  <CircuitBadge state={cb.state || m.state} />
@@ -641,6 +704,7 @@ export default function RouterView({ onClose, onToast }) {
641
704
  )
642
705
  })}
643
706
  </div>
707
+ </>
644
708
  )}
645
709
 
646
710
  {pickerOpen && (
@@ -720,8 +784,8 @@ export default function RouterView({ onClose, onToast }) {
720
784
  </div>
721
785
  )}
722
786
 
723
- {/* Request Log */}
724
- {running && requestLog.length > 0 && (
787
+ {/* Request Log — always visible when running */}
788
+ {running && (
725
789
  <div className={styles.section}>
726
790
  <h3 className={styles.sectionTitle} onClick={() => setLogExpanded(!logExpanded)} style={{ cursor: 'pointer' }}>
727
791
  <IconActivity size={14} />
@@ -729,23 +793,27 @@ export default function RouterView({ onClose, onToast }) {
729
793
  {logExpanded ? <IconChevronUp size={12} /> : <IconChevronDown size={12} />}
730
794
  </h3>
731
795
  {logExpanded && (
732
- <div className={styles.logList}>
733
- {requestLog.map((entry, i) => (
734
- <div key={i} className={styles.logRow}>
735
- <span className={entry.error ? styles.logErr : styles.logOk}>
736
- {entry.status || '—'}
737
- </span>
738
- <span className={styles.logModel}>{entry.model}</span>
739
- <span className={styles.logLatency}>
740
- {entry.latency_ms != null ? `${entry.latency_ms}ms` : '—'}
741
- </span>
742
- <span className={styles.logTokens}>
743
- {entry.tokens > 0 ? formatNumber(entry.tokens) + ' tok' : ''}
744
- </span>
745
- {entry.failover && <span className={styles.logFailover}>failover</span>}
746
- </div>
747
- ))}
748
- </div>
796
+ requestLog.length === 0 ? (
797
+ <div className={styles.logEmpty}>No requests yet. Start coding to see traffic here.</div>
798
+ ) : (
799
+ <div className={styles.logList}>
800
+ {requestLog.map((entry, i) => (
801
+ <div key={i} className={styles.logRow}>
802
+ <span className={entry.error ? styles.logErr : styles.logOk}>
803
+ {entry.status || '—'}
804
+ </span>
805
+ <span className={styles.logModel}>{entry.model}</span>
806
+ <span className={styles.logLatency}>
807
+ {entry.latency_ms != null ? `${entry.latency_ms}ms` : ''}
808
+ </span>
809
+ <span className={styles.logTokens}>
810
+ {entry.tokens > 0 ? formatNumber(entry.tokens) + ' tok' : ''}
811
+ </span>
812
+ {entry.failover && <span className={styles.logFailover}>failover</span>}
813
+ </div>
814
+ ))}
815
+ </div>
816
+ )
749
817
  )}
750
818
  </div>
751
819
  )}
@@ -140,6 +140,13 @@
140
140
  &:disabled { opacity: 0.5; cursor: not-allowed; }
141
141
  }
142
142
 
143
+ /* 📖 Bigger start button for the stopped state — more prominent CTA. */
144
+ .startBtnBig {
145
+ padding: 10px 24px;
146
+ font-size: 15px;
147
+ border-radius: 10px;
148
+ }
149
+
143
150
  .stopBtn {
144
151
  display: flex;
145
152
  align-items: center;
@@ -175,6 +182,33 @@
175
182
  margin-bottom: 16px;
176
183
  }
177
184
 
185
+ /* 📖 Hero glow when running — makes the quick setup card visually prominent. */
186
+ .quickSetupHero {
187
+ border: 1px solid rgba(0, 200, 100, 0.25);
188
+ box-shadow: 0 0 16px rgba(0, 200, 100, 0.08);
189
+ border-radius: 10px;
190
+ padding: 12px;
191
+ background: linear-gradient(135deg, rgba(0, 200, 100, 0.05), transparent);
192
+ }
193
+
194
+ /* 📖 "Copy all" button — sits at the right of the Quick Setup title. */
195
+ .copyAllBtn {
196
+ display: inline-flex;
197
+ align-items: center;
198
+ gap: 4px;
199
+ margin-left: auto;
200
+ padding: 3px 10px;
201
+ background: var(--bg-secondary, #14141f);
202
+ border: 1px solid var(--border, #1e1e2e);
203
+ border-radius: 6px;
204
+ color: var(--text-secondary, #aaa);
205
+ font-size: 11px;
206
+ font-weight: 500;
207
+ cursor: pointer;
208
+ font-family: inherit;
209
+ &:hover { color: var(--text-primary, #e0e0e0); border-color: var(--accent, #00c864); }
210
+ }
211
+
178
212
  .sectionTitle {
179
213
  display: flex;
180
214
  align-items: center;
@@ -351,6 +385,16 @@
351
385
  text-transform: uppercase;
352
386
  }
353
387
 
388
+ .logEmpty {
389
+ padding: 16px;
390
+ text-align: center;
391
+ color: var(--text-muted, #888);
392
+ font-size: 12px;
393
+ background: var(--bg-secondary, #14141f);
394
+ border: 1px dashed var(--border, #1e1e2e);
395
+ border-radius: 8px;
396
+ }
397
+
354
398
  @keyframes fadeIn {
355
399
  from { opacity: 0; }
356
400
  to { opacity: 1; }
@@ -421,7 +465,7 @@
421
465
  }
422
466
 
423
467
  .setEmpty {
424
- padding: 16px;
468
+ padding: 20px 16px;
425
469
  text-align: center;
426
470
  color: var(--text-muted, #888);
427
471
  font-size: 12px;
@@ -430,6 +474,23 @@
430
474
  border-radius: 8px;
431
475
  }
432
476
 
477
+ .setEmptyTitle {
478
+ font-size: 13px;
479
+ font-weight: 600;
480
+ color: var(--text-secondary, #aaa);
481
+ margin-bottom: 4px;
482
+ }
483
+
484
+ .setEmptyHint {
485
+ font-size: 11px;
486
+ color: var(--text-muted, #888);
487
+ line-height: 1.5;
488
+ }
489
+
490
+ .setEmptyHint strong {
491
+ color: var(--text-secondary, #aaa);
492
+ }
493
+
433
494
  .setRow {
434
495
  display: flex;
435
496
  align-items: center;
@@ -510,6 +571,79 @@
510
571
  text-transform: uppercase;
511
572
  }
512
573
 
574
+ /* 📖 Priority badge variants — make the fallback chain semantics explicit
575
+ (issue #120). Primary gets the accent color so users instantly see which
576
+ model serves every request; fallbacks stay muted. */
577
+ .setPriorityPrimary {
578
+ min-width: 52px;
579
+ padding: 2px 8px;
580
+ border-radius: 4px;
581
+ background: rgba(0, 200, 100, 0.14);
582
+ color: var(--accent, #00c864);
583
+ border: 1px solid rgba(0, 200, 100, 0.3);
584
+ text-transform: uppercase;
585
+ letter-spacing: 0.3px;
586
+ text-align: center;
587
+ }
588
+
589
+ .setPriorityFallback {
590
+ text-transform: none;
591
+ opacity: 0.85;
592
+ }
593
+
594
+ /* 📖 Next-to-serve row — subtle accent left border + tint so the user can
595
+ see at a glance which model will handle the next chat completion.
596
+ Only applied when the daemon is running and routingOrder is known. */
597
+ .setRowNext {
598
+ background: linear-gradient(90deg, rgba(0, 200, 100, 0.07), transparent 60%);
599
+ border-left: 2px solid var(--accent, #00c864);
600
+ padding-left: 8px;
601
+ }
602
+
603
+ /* 📖 Priority legend — one-line explainer above the set list. */
604
+ .priorityLegend {
605
+ display: flex;
606
+ align-items: center;
607
+ gap: 8px;
608
+ flex-wrap: wrap;
609
+ padding: 6px 10px;
610
+ margin-bottom: 8px;
611
+ font-size: 11px;
612
+ color: var(--text-muted, #888);
613
+ background: var(--bg-secondary, #14141f);
614
+ border: 1px solid var(--border, #1e1e2e);
615
+ border-radius: 6px;
616
+ }
617
+
618
+ .legendPrimary {
619
+ display: inline-flex;
620
+ align-items: center;
621
+ gap: 3px;
622
+ font-weight: 600;
623
+ color: var(--accent, #00c864);
624
+ }
625
+
626
+ .legendFallback {
627
+ font-weight: 600;
628
+ color: var(--text-secondary, #aaa);
629
+ }
630
+
631
+ .legendSeparator {
632
+ color: var(--text-muted, #888);
633
+ opacity: 0.7;
634
+ }
635
+
636
+ .legendNext {
637
+ margin-left: auto;
638
+ color: var(--text-secondary, #aaa);
639
+ }
640
+
641
+ .legendNext code {
642
+ color: var(--accent, #00c864);
643
+ font-family: 'SF Mono', monospace;
644
+ font-size: 10px;
645
+ }
646
+
513
647
  .setRowBtns {
514
648
  display: flex;
515
649
  gap: 2px;
@@ -1,9 +0,0 @@
1
- /**
2
- * @file src/product-flags.js
3
- * @description Product-level flags and feature gates.
4
- *
5
- * @details
6
- * 📖 Previously held PROXY_DISABLED_NOTICE for the proxy bridge rebuild.
7
- * 📖 That notice was removed in 0.3.22 after the proxy surface was fully
8
- * 📖 retired. File kept as a home for future product-level flags.
9
- */