free-coding-models 0.5.5 → 0.5.8

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.
Files changed (63) hide show
  1. package/README.md +19 -11
  2. package/bin/free-coding-models.js +6 -0
  3. package/changelog/v0.5.5.md +1 -0
  4. package/changelog/v0.5.6.md +24 -0
  5. package/changelog/v0.5.7.md +20 -0
  6. package/package.json +4 -4
  7. package/src/core/changelog-loader.js +5 -1
  8. package/src/core/endpoint-installer.js +3 -3
  9. package/src/core/router-daemon.js +11 -0
  10. package/src/core/tool-launchers.js +2 -2
  11. package/web/dist/assets/index-DKmmiDip.css +1 -0
  12. package/web/dist/assets/index-DzVt42Nf.js +39 -0
  13. package/web/dist/index.html +2 -2
  14. package/web/server.js +532 -2
  15. package/web/src/App.jsx +235 -73
  16. package/web/src/components/analytics/AnalyticsView.jsx +4 -0
  17. package/web/src/components/analytics/TokenUsagePanel.jsx +105 -0
  18. package/web/src/components/analytics/TokenUsagePanel.module.css +126 -0
  19. package/web/src/components/atoms/TierBadge.module.css +3 -3
  20. package/web/src/components/changelog/ChangelogView.jsx +135 -0
  21. package/web/src/components/changelog/ChangelogView.module.css +160 -0
  22. package/web/src/components/dashboard/DetailPanel.jsx +29 -4
  23. package/web/src/components/dashboard/DetailPanel.module.css +26 -0
  24. package/web/src/components/dashboard/FilterBar.jsx +17 -2
  25. package/web/src/components/dashboard/FilterBar.module.css +17 -0
  26. package/web/src/components/dashboard/ModelTable.jsx +17 -5
  27. package/web/src/components/dashboard/ModelTable.module.css +14 -1
  28. package/web/src/components/help/HelpView.jsx +188 -0
  29. package/web/src/components/help/HelpView.module.css +157 -0
  30. package/web/src/components/install/InstallEndpointsView.jsx +278 -0
  31. package/web/src/components/install/InstallEndpointsView.module.css +351 -0
  32. package/web/src/components/installed/InstalledModelsView.jsx +89 -0
  33. package/web/src/components/installed/InstalledModelsView.module.css +200 -0
  34. package/web/src/components/launch/IncompatibleFallbackModal.jsx +69 -0
  35. package/web/src/components/launch/LaunchButton.jsx +30 -0
  36. package/web/src/components/launch/LaunchButton.module.css +35 -0
  37. package/web/src/components/launch/LaunchModal.module.css +125 -0
  38. package/web/src/components/layout/Header.jsx +78 -14
  39. package/web/src/components/layout/Header.module.css +62 -2
  40. package/web/src/components/palette/CommandPalette.jsx +228 -74
  41. package/web/src/components/recommend/RecommendView.jsx +121 -0
  42. package/web/src/components/recommend/RecommendView.module.css +55 -0
  43. package/web/src/components/router/RouterView.jsx +286 -0
  44. package/web/src/components/router/RouterView.module.css +357 -0
  45. package/web/src/components/settings/SettingsView.jsx +281 -8
  46. package/web/src/components/settings/SettingsView.module.css +174 -0
  47. package/web/src/components/tools/ToolPicker.jsx +86 -0
  48. package/web/src/components/tools/ToolPicker.module.css +84 -0
  49. package/web/src/components/update/UpdateChip.jsx +104 -0
  50. package/web/src/components/update/UpdateChip.module.css +146 -0
  51. package/web/src/global.css +23 -0
  52. package/web/src/hooks/urlState.constants.js +28 -0
  53. package/web/src/hooks/useChangelog.js +51 -0
  54. package/web/src/hooks/useInstalledModels.js +42 -0
  55. package/web/src/hooks/useRecommend.js +67 -0
  56. package/web/src/hooks/useRouterDashboard.js +133 -0
  57. package/web/src/hooks/useTokenUsage.js +103 -0
  58. package/web/src/hooks/useToolMode.js +77 -0
  59. package/web/src/hooks/useUpdateChecker.js +91 -0
  60. package/web/src/hooks/useUrlState.js +123 -62
  61. package/web/src/utils/m3.js +55 -0
  62. package/web/dist/assets/index-uD3faN3G.js +0 -39
  63. package/web/dist/assets/index-uTifVKX1.css +0 -1
@@ -0,0 +1,126 @@
1
+ .card {
2
+ margin-top: 16px;
3
+ }
4
+
5
+ .cardTitle {
6
+ font-size: 13px;
7
+ font-weight: 600;
8
+ color: var(--text-secondary, #aaa);
9
+ margin: 0 0 10px 0;
10
+ text-transform: uppercase;
11
+ letter-spacing: 0.5px;
12
+ }
13
+
14
+ .empty {
15
+ color: var(--text-muted, #666);
16
+ font-size: 13px;
17
+ padding: 20px 0;
18
+ text-align: center;
19
+ }
20
+
21
+ /* Summary Grid */
22
+ .summaryGrid {
23
+ display: grid;
24
+ grid-template-columns: 1fr 1fr;
25
+ gap: 12px;
26
+ margin-top: 16px;
27
+ }
28
+
29
+ .summaryCard {
30
+ background: var(--bg-secondary, #14141f);
31
+ border: 1px solid var(--border, #1e1e2e);
32
+ border-radius: 8px;
33
+ padding: 14px 16px;
34
+ display: flex;
35
+ flex-direction: column;
36
+ gap: 2px;
37
+ }
38
+
39
+ .summaryLabel {
40
+ font-size: 11px;
41
+ font-weight: 600;
42
+ color: var(--text-muted, #888);
43
+ text-transform: uppercase;
44
+ letter-spacing: 0.5px;
45
+ }
46
+
47
+ .summaryValue {
48
+ font-size: 22px;
49
+ font-weight: 700;
50
+ color: var(--text-primary, #e0e0e0);
51
+ }
52
+
53
+ .summarySub {
54
+ font-size: 11px;
55
+ color: var(--text-muted, #888);
56
+ }
57
+
58
+ /* 7-Day Chart */
59
+ .chart {
60
+ display: flex;
61
+ align-items: flex-end;
62
+ gap: 6px;
63
+ height: 100px;
64
+ padding: 0 4px;
65
+ }
66
+
67
+ .chartBar {
68
+ flex: 1;
69
+ display: flex;
70
+ flex-direction: column;
71
+ align-items: center;
72
+ height: 100%;
73
+ justify-content: flex-end;
74
+ gap: 4px;
75
+ }
76
+
77
+ .chartFill {
78
+ width: 100%;
79
+ min-height: 2px;
80
+ background: linear-gradient(180deg, #00c864 0%, rgba(0, 200, 100, 0.3) 100%);
81
+ border-radius: 3px 3px 0 0;
82
+ transition: height 0.3s ease;
83
+ }
84
+
85
+ .chartLabel {
86
+ font-size: 10px;
87
+ color: var(--text-muted, #888);
88
+ text-transform: uppercase;
89
+ }
90
+
91
+ /* Top Lists */
92
+ .topList {
93
+ display: flex;
94
+ flex-direction: column;
95
+ gap: 4px;
96
+ }
97
+
98
+ .topRow {
99
+ display: flex;
100
+ align-items: center;
101
+ gap: 8px;
102
+ padding: 4px 0;
103
+ font-size: 12px;
104
+ }
105
+
106
+ .topKey {
107
+ flex: 1;
108
+ color: var(--text-primary, #e0e0e0);
109
+ font-family: 'SF Mono', monospace;
110
+ overflow: hidden;
111
+ text-overflow: ellipsis;
112
+ white-space: nowrap;
113
+ }
114
+
115
+ .topVal {
116
+ color: var(--text-muted, #888);
117
+ min-width: 80px;
118
+ text-align: right;
119
+ }
120
+
121
+ .topReq {
122
+ color: var(--text-muted, #666);
123
+ min-width: 50px;
124
+ text-align: right;
125
+ font-size: 11px;
126
+ }
@@ -13,9 +13,9 @@
13
13
  .tier_aplus { background: rgba(0,200,255,0.1); color: #00c8ff; border: 1px solid rgba(0,200,255,0.2); }
14
14
  .tier_a { background: rgba(61,220,132,0.1); color: #3ddc84; border: 1px solid rgba(61,220,132,0.2); }
15
15
  .tier_aminus{ background: rgba(126,207,126,0.1); color: #7ecf7e; border: 1px solid rgba(126,207,126,0.2); }
16
- .tier_bplus { background: rgba(168,168,200,0.1); color: #a8a8c8; border: 1px solid rgba(168,168,200,0.15); }
17
- .tier_b { background: rgba(128,128,152,0.1); color: #808098; border: 1px solid rgba(128,128,152,0.15); }
18
- .tier_c { background: rgba(96,96,120,0.1); color: #606078; border: 1px solid rgba(96,96,120,0.15); }
16
+ .tier_bplus { background: rgba(168,168,200,0.1); color: #b8b8d8; border: 1px solid rgba(168,168,200,0.15); }
17
+ .tier_b { background: rgba(128,128,152,0.1); color: #a0a0b8; border: 1px solid rgba(128,128,152,0.15); }
18
+ .tier_c { background: rgba(96,96,120,0.1); color: #909098; border: 1px solid rgba(96,96,120,0.15); }
19
19
 
20
20
  /* 📖 Light theme: darker, AA-contrast tier text colors. Borders stay the same
21
21
  📖 hue so the badge still feels "tier-colored", just more legible. */
@@ -0,0 +1,135 @@
1
+ /**
2
+ * @file web/src/components/changelog/ChangelogView.jsx
3
+ * @description Changelog modal — M2 parity with the TUI's Changelog overlay (N key / Settings link).
4
+ * 📖 Two-phase UI: index of versions on the left, details on the right. Same
5
+ * 📖 content as the TUI's renderChangelog (which uses src/core/changelog-loader.js
6
+ * 📖 — we hit the same data through `/api/changelog`).
7
+ *
8
+ * @functions
9
+ * → ChangelogView → main modal component
10
+ */
11
+ import { useState } from 'react'
12
+ import { IconArrowLeft, IconX, IconCalendar } from '@tabler/icons-react'
13
+ import { useChangelog } from '../../hooks/useChangelog.js'
14
+ import styles from './ChangelogView.module.css'
15
+
16
+ // 📖 Section order matches the changelog files (`### Added` / `### Fixed` /
17
+ // 📖 `### Changed` / `### Updated`). The TUI uses the same order in
18
+ // 📖 formatChangelogForDisplay.
19
+ const SECTION_LABELS = [
20
+ { key: 'added', label: '✨ Added', icon: '✨' },
21
+ { key: 'fixed', label: '🐛 Fixed', icon: '🐛' },
22
+ { key: 'changed', label: '🔄 Changed', icon: '🔄' },
23
+ { key: 'updated', label: '📝 Updated', icon: '📝' },
24
+ ]
25
+
26
+ export default function ChangelogView({ onClose, defaultVersion = null }) {
27
+ const { sortedVersions, getVersion, loading, error } = useChangelog()
28
+ // 📖 Two-phase navigation: 'index' (version list) or 'details' (one version).
29
+ // 📖 `selectedVersion` is null on the index and a version string on details.
30
+ const [phase, setPhase] = useState(defaultVersion ? 'details' : 'index')
31
+ const [selectedVersion, setSelectedVersion] = useState(defaultVersion)
32
+
33
+ const openDetails = (version) => {
34
+ setSelectedVersion(version)
35
+ setPhase('details')
36
+ }
37
+ const backToIndex = () => {
38
+ setSelectedVersion(null)
39
+ setPhase('index')
40
+ }
41
+
42
+ const details = selectedVersion ? getVersion(selectedVersion) : null
43
+
44
+ return (
45
+ <div className={styles.backdrop} onClick={onClose}>
46
+ <div className={styles.modal} onClick={(e) => e.stopPropagation()}>
47
+ <div className={styles.header}>
48
+ <div className={styles.titleRow}>
49
+ <h2 className={styles.title}>
50
+ {phase === 'index' ? '📋 Changelog' : `📋 v${selectedVersion}`}
51
+ </h2>
52
+ {phase === 'details' && (
53
+ <button className={styles.backBtn} onClick={backToIndex} title="Back to index (TUI: B)">
54
+ <IconArrowLeft size={14} stroke={1.5} /> Index
55
+ </button>
56
+ )}
57
+ <button className={styles.closeBtn} onClick={onClose} aria-label="Close changelog">
58
+ <IconX size={18} stroke={1.5} />
59
+ </button>
60
+ </div>
61
+ </div>
62
+
63
+ <div className={styles.body}>
64
+ {loading && <div className={styles.empty}>Loading changelog…</div>}
65
+ {error && !loading && <div className={styles.empty}>Failed to load changelog: {error}</div>}
66
+
67
+ {!loading && !error && phase === 'index' && (
68
+ <div className={styles.indexWrap}>
69
+ <p className={styles.indexHint}>
70
+ {sortedVersions.length} versions. Click any to read the release notes.
71
+ </p>
72
+ <ul className={styles.versionList}>
73
+ {sortedVersions.map((version) => {
74
+ const changes = getVersion(version)
75
+ const summary = []
76
+ if (changes?.added?.length) summary.push(`${changes.added.length} added`)
77
+ if (changes?.fixed?.length) summary.push(`${changes.fixed.length} fixed`)
78
+ if (changes?.changed?.length) summary.push(`${changes.changed.length} changed`)
79
+ return (
80
+ <li key={version}>
81
+ <button
82
+ className={styles.versionBtn}
83
+ onClick={() => openDetails(version)}
84
+ >
85
+ <span className={styles.versionLabel}>v{version}</span>
86
+ <span className={styles.versionSummary}>
87
+ {summary.length > 0 ? summary.join(' · ') : '—'}
88
+ </span>
89
+ </button>
90
+ </li>
91
+ )
92
+ })}
93
+ </ul>
94
+ </div>
95
+ )}
96
+
97
+ {!loading && !error && phase === 'details' && details && (
98
+ <div className={styles.details}>
99
+ {SECTION_LABELS.map(({ key, label }) => {
100
+ const items = details[key]
101
+ if (!items || items.length === 0) return null
102
+ return (
103
+ <section key={key} className={styles.section}>
104
+ <h3 className={styles.sectionTitle}>{label}</h3>
105
+ <ul className={styles.itemList}>
106
+ {items.map((item, idx) => (
107
+ <li key={idx} className={styles.item}>{formatItem(item)}</li>
108
+ ))}
109
+ </ul>
110
+ </section>
111
+ )
112
+ })}
113
+ {SECTION_LABELS.every(({ key }) => !details[key]?.length) && (
114
+ <div className={styles.empty}>No release notes for v{selectedVersion}.</div>
115
+ )}
116
+ </div>
117
+ )}
118
+
119
+ {!loading && !error && phase === 'details' && !details && (
120
+ <div className={styles.empty}>No notes for v{selectedVersion}.</div>
121
+ )}
122
+ </div>
123
+ </div>
124
+ </div>
125
+ )
126
+ }
127
+
128
+ // 📖 Strip **bold** and `code` markdown markers for the Web view. The TUI does
129
+ // 📖 the same in formatChangelogForDisplay, so the two surfaces stay in sync.
130
+ function formatItem(text) {
131
+ return text
132
+ .replace(/\*\*([^*]+)\*\*/g, '$1')
133
+ .replace(/`([^`]+)`/g, '$1')
134
+ .trim()
135
+ }
@@ -0,0 +1,160 @@
1
+ /**
2
+ * @file web/src/components/changelog/ChangelogView.module.css
3
+ * @description Two-phase changelog modal styles (TUI parity).
4
+ */
5
+
6
+ .backdrop {
7
+ position: fixed;
8
+ inset: 0;
9
+ background: rgba(0, 0, 0, 0.55);
10
+ backdrop-filter: blur(4px);
11
+ -webkit-backdrop-filter: blur(4px);
12
+ z-index: 500;
13
+ display: flex;
14
+ align-items: center;
15
+ justify-content: center;
16
+ padding: 5vh 4vw;
17
+ }
18
+
19
+ .modal {
20
+ width: min(720px, 95vw);
21
+ max-height: 88vh;
22
+ background: var(--color-bg-elevated);
23
+ border: 1px solid var(--color-border-hover);
24
+ border-radius: 14px;
25
+ box-shadow: 0 24px 64px rgba(0, 0, 0, 0.5);
26
+ display: flex;
27
+ flex-direction: column;
28
+ overflow: hidden;
29
+ }
30
+
31
+ .header {
32
+ padding: 16px 20px;
33
+ border-bottom: 1px solid var(--color-border);
34
+ }
35
+
36
+ .titleRow {
37
+ display: flex;
38
+ align-items: center;
39
+ gap: 12px;
40
+ }
41
+
42
+ .title {
43
+ font-size: 16px;
44
+ font-weight: 700;
45
+ margin: 0;
46
+ color: var(--color-text);
47
+ flex: 1;
48
+ min-width: 0;
49
+ }
50
+
51
+ .backBtn {
52
+ display: inline-flex;
53
+ align-items: center;
54
+ gap: 4px;
55
+ padding: 4px 10px;
56
+ background: var(--color-surface);
57
+ color: var(--color-text);
58
+ border: 1px solid var(--color-border);
59
+ border-radius: 6px;
60
+ font-size: 11px;
61
+ font-weight: 600;
62
+ cursor: pointer;
63
+ font-family: var(--font-sans);
64
+ white-space: nowrap;
65
+ }
66
+ .backBtn:hover { background: var(--color-bg-hover); border-color: var(--color-text-muted); }
67
+
68
+ .closeBtn {
69
+ background: transparent;
70
+ border: none;
71
+ color: var(--color-text-muted);
72
+ cursor: pointer;
73
+ padding: 4px;
74
+ border-radius: 4px;
75
+ display: inline-flex;
76
+ align-items: center;
77
+ justify-content: center;
78
+ flex-shrink: 0;
79
+ }
80
+ .closeBtn:hover { color: var(--color-text); background: var(--color-bg-hover); }
81
+
82
+ .body {
83
+ flex: 1;
84
+ overflow-y: auto;
85
+ padding: 16px 20px 20px;
86
+ }
87
+
88
+ .indexWrap {}
89
+ .indexHint {
90
+ margin: 0 0 12px;
91
+ font-size: 11px;
92
+ color: var(--color-text-muted);
93
+ }
94
+
95
+ .versionList {
96
+ list-style: none;
97
+ margin: 0;
98
+ padding: 0;
99
+ display: flex;
100
+ flex-direction: column;
101
+ gap: 4px;
102
+ }
103
+ .versionBtn {
104
+ display: flex;
105
+ align-items: center;
106
+ justify-content: space-between;
107
+ gap: 12px;
108
+ width: 100%;
109
+ padding: 8px 12px;
110
+ background: var(--color-surface);
111
+ border: 1px solid var(--color-border);
112
+ border-radius: 6px;
113
+ cursor: pointer;
114
+ text-align: left;
115
+ transition: all 100ms;
116
+ font-family: var(--font-sans);
117
+ }
118
+ .versionBtn:hover { background: var(--color-bg-hover); border-color: var(--color-text-muted); }
119
+ .versionLabel {
120
+ font-family: var(--font-mono);
121
+ font-size: 13px;
122
+ font-weight: 700;
123
+ color: var(--color-text);
124
+ }
125
+ .versionSummary {
126
+ font-size: 11px;
127
+ color: var(--color-text-muted);
128
+ }
129
+
130
+ .details { display: flex; flex-direction: column; gap: 14px; }
131
+ .section {}
132
+ .sectionTitle {
133
+ font-size: 12px;
134
+ font-weight: 700;
135
+ text-transform: uppercase;
136
+ letter-spacing: 0.6px;
137
+ color: var(--color-text-muted);
138
+ margin: 0 0 6px;
139
+ }
140
+ .itemList {
141
+ list-style: disc;
142
+ padding-left: 20px;
143
+ margin: 0;
144
+ display: flex;
145
+ flex-direction: column;
146
+ gap: 4px;
147
+ }
148
+ .item {
149
+ font-size: 12px;
150
+ color: var(--color-text);
151
+ line-height: 1.55;
152
+ word-break: break-word;
153
+ }
154
+
155
+ .empty {
156
+ text-align: center;
157
+ padding: 40px 20px;
158
+ color: var(--color-text-dim);
159
+ font-size: 13px;
160
+ }
@@ -7,14 +7,16 @@
7
7
  * 📖 action section (star + reorder + per-row AI Speed Test button).
8
8
  * @functions DetailPanel → main panel component, buildDetailChart → SVG chart builder
9
9
  */
10
- import { useMemo } from 'react'
11
- import { IconStar, IconStarFilled, IconChevronUp, IconChevronDown, IconPlayerPlayFilled } from '@tabler/icons-react'
10
+ import { IconStar, IconStarFilled, IconChevronUp, IconChevronDown, IconPlayerPlayFilled, IconAlertTriangle } from '@tabler/icons-react'
12
11
  import TierBadge from '../atoms/TierBadge.jsx'
13
12
  import VerdictBadge from '../atoms/VerdictBadge.jsx'
14
13
  import StatusDot from '../atoms/StatusDot.jsx'
15
14
  import StabilityCell from '../atoms/StabilityCell.jsx'
16
15
  import { formatPing, formatAvg, pingClass } from '../../utils/format.js'
17
16
  import { sweClass } from '../../utils/ranks.js'
17
+ import LaunchButton from '../launch/LaunchButton.jsx'
18
+ import ToolPicker from '../tools/ToolPicker.jsx'
19
+ import { getToolMeta, isModelCompatibleWithTool } from '../../../../src/core/tool-metadata.js'
18
20
  import styles from './DetailPanel.module.css'
19
21
 
20
22
  function buildDetailChart(history) {
@@ -70,7 +72,10 @@ function StatRow({ label, children }) {
70
72
  )
71
73
  }
72
74
 
73
- export default function DetailPanel({ model, onClose, favorites, onBenchmark, onToast }) {
75
+ export default function DetailPanel({
76
+ model, onClose, favorites, onBenchmark, onLaunch, onToast,
77
+ toolMode = 'opencode', onSetToolMode, onCycleToolMode, onOpenFallback,
78
+ }) {
74
79
  if (!model) return null
75
80
 
76
81
  const chartSvg = buildDetailChart(model.pingHistory)
@@ -78,6 +83,8 @@ export default function DetailPanel({ model, onClose, favorites, onBenchmark, on
78
83
  const favRank = favorites?.favoriteRank(model) ?? Number.MAX_SAFE_INTEGER
79
84
  const isFavAtTop = isFav && favRank === 0
80
85
  const isFavAtBottom = isFav && favRank === (favorites?.favorites.length ?? 0) - 1
86
+ const toolMeta = getToolMeta(toolMode)
87
+ const compatible = isModelCompatibleWithTool(model.providerKey, toolMode)
81
88
 
82
89
  return (
83
90
  <div className={styles.panel}>
@@ -127,8 +134,23 @@ export default function DetailPanel({ model, onClose, favorites, onBenchmark, on
127
134
  <StatRow label="API Key">
128
135
  {model.hasApiKey ? '✅ Configured' : '❌ Missing'}
129
136
  </StatRow>
137
+ <StatRow label="Tool">
138
+ <ToolPicker
139
+ compact
140
+ toolMode={toolMode}
141
+ onSetToolMode={onSetToolMode}
142
+ onCycleToolMode={onCycleToolMode}
143
+ />
144
+ </StatRow>
145
+ {!compatible && (
146
+ <div className={styles.compatWarning}>
147
+ <IconAlertTriangle size={14} />
148
+ <span>{model.label} is not compatible with {toolMeta.emoji} {toolMeta.label}.</span>
149
+ <button onClick={() => onOpenFallback?.(model)}>Install in compatible tool</button>
150
+ </div>
151
+ )}
130
152
 
131
- {/* ── M1: favorites + per-row benchmark action section ── */}
153
+ {/* ── M1/M3: favorites + benchmark + endpoint install action section ── */}
132
154
  {(favorites || onBenchmark) && (
133
155
  <div className={styles.actions}>
134
156
  {favorites && (
@@ -166,6 +188,9 @@ export default function DetailPanel({ model, onClose, favorites, onBenchmark, on
166
188
  )}
167
189
  </div>
168
190
  )}
191
+ {onLaunch && (
192
+ <LaunchButton model={model} toolMode={toolMode} onLaunch={onLaunch} />
193
+ )}
169
194
  {onBenchmark && (
170
195
  <button
171
196
  className={styles.benchBtn}
@@ -219,6 +219,32 @@
219
219
  background: rgba(180, 0, 255, 0.20);
220
220
  border-color: rgba(180, 0, 255, 0.7);
221
221
  }
222
+
223
+ .compatWarning {
224
+ display: flex;
225
+ align-items: center;
226
+ gap: 8px;
227
+ margin: 12px 0;
228
+ padding: 10px;
229
+ border: 1px solid rgba(255, 90, 122, 0.35);
230
+ border-radius: 8px;
231
+ background: rgba(255, 90, 122, 0.10);
232
+ color: #ff8da3;
233
+ font-size: 12px;
234
+ line-height: 1.35;
235
+ }
236
+ .compatWarning span { flex: 1; }
237
+ .compatWarning button {
238
+ border: 1px solid rgba(255, 90, 122, 0.45);
239
+ border-radius: 6px;
240
+ background: var(--color-bg-card);
241
+ color: #ff8da3;
242
+ padding: 5px 8px;
243
+ cursor: pointer;
244
+ font-size: 11px;
245
+ font-weight: 800;
246
+ }
247
+ .compatWarning button:hover { background: rgba(255, 90, 122, 0.16); }
222
248
  :global([data-theme="light"]) .benchBtn {
223
249
  background: rgba(140, 0, 200, 0.06);
224
250
  color: #6a00a0;
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import { useState, useEffect, useMemo } from 'react'
10
10
  import { IconRefresh, IconX, IconFilter, IconCircleCheck, IconAlertTriangle, IconActivity, IconEye, IconStar } from '@tabler/icons-react'
11
+ import { getToolMeta } from '../../../../src/core/tool-metadata.js'
11
12
  import styles from './FilterBar.module.css'
12
13
 
13
14
  // 📖 Chip sets match the TUI cycles 1:1 (see useFilter.js). Keep these in sync.
@@ -109,6 +110,7 @@ export default function FilterBar({
109
110
  globalBenchmarkRunning,
110
111
  globalBenchmarkTotal,
111
112
  globalBenchmarkCompleted,
113
+ toolMode = 'opencode',
112
114
  }) {
113
115
  const [countdown, setCountdown] = useState(null)
114
116
 
@@ -124,6 +126,7 @@ export default function FilterBar({
124
126
  }, [nextPingAt])
125
127
 
126
128
  const countdownDisplay = countdown !== null ? formatCountdown(countdown) : null
129
+ const activeTool = getToolMeta(toolMode)
127
130
 
128
131
  // 📖 Custom text filter chip — sticks to the right of the search bar, with
129
132
  // 📖 an "X" to clear (TUI's `X` key behavior).
@@ -170,12 +173,14 @@ export default function FilterBar({
170
173
  <ChipRow label="Health" items={HEALTHS} value={filterHealth} onChange={setFilterHealth} />
171
174
 
172
175
  <div className={styles.group}>
173
- <label className={styles.filterLabel}>Visibility</label>
176
+ <label className={styles.filterLabel} htmlFor="visibility-select">Visibility</label>
174
177
  <select
178
+ id="visibility-select"
175
179
  className={styles.select}
176
180
  value={visibilityMode}
177
181
  onChange={(e) => setVisibilityMode(e.target.value)}
178
182
  title={VISIBILITY_MODES.find((v) => v.key === visibilityMode)?.hint}
183
+ aria-label="Visibility mode"
179
184
  >
180
185
  {VISIBILITY_MODES.map((v) => (
181
186
  <option key={v.key} value={v.key}>{v.label}</option>
@@ -184,11 +189,13 @@ export default function FilterBar({
184
189
  </div>
185
190
 
186
191
  <div className={styles.group}>
187
- <label className={styles.filterLabel}>Provider</label>
192
+ <label className={styles.filterLabel} htmlFor="provider-select">Provider</label>
188
193
  <select
194
+ id="provider-select"
189
195
  className={styles.providerSelect}
190
196
  value={filterProvider}
191
197
  onChange={(e) => setFilterProvider(e.target.value)}
198
+ aria-label="Filter by provider"
192
199
  >
193
200
  <option value="all">All Providers</option>
194
201
  {providers.map((p) => (
@@ -197,6 +204,14 @@ export default function FilterBar({
197
204
  </select>
198
205
  </div>
199
206
 
207
+ <div className={styles.group}>
208
+ <label className={styles.filterLabel}>Endpoint target</label>
209
+ <div className={styles.toolStatus} title="Active endpoint install target from the Header picker">
210
+ <span>{activeTool.emoji}</span>
211
+ <strong>{activeTool.label}</strong>
212
+ </div>
213
+ </div>
214
+
200
215
  <div className={styles.spacer} />
201
216
 
202
217
  {/* ── Custom text filter chip (TUI's Ctrl+P "Apply text filter") ── */}
@@ -49,6 +49,23 @@
49
49
  }
50
50
  .select:focus, .providerSelect:focus { border-color: var(--color-accent); }
51
51
 
52
+ .toolStatus {
53
+ display: inline-flex;
54
+ align-items: center;
55
+ gap: 6px;
56
+ padding: 4px 8px;
57
+ border: 1px solid var(--color-border);
58
+ border-radius: 5px;
59
+ background: var(--color-surface);
60
+ color: var(--color-text);
61
+ font-size: 11px;
62
+ white-space: nowrap;
63
+ }
64
+ .toolStatus strong {
65
+ font-size: 11px;
66
+ font-weight: 800;
67
+ }
68
+
52
69
  /* ── Spacer + reset ── */
53
70
  .spacer { flex: 1; }
54
71