agent-nuvira 2.7.0 → 2.7.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.
Files changed (114) hide show
  1. package/dist/agents/orchestrator.d.ts.map +1 -1
  2. package/dist/agents/orchestrator.js +17 -3
  3. package/dist/agents/orchestrator.js.map +1 -1
  4. package/dist/app.js +26 -0
  5. package/dist/cli/chat.d.ts.map +1 -1
  6. package/dist/cli/chat.js +108 -2
  7. package/dist/cli/chat.js.map +1 -1
  8. package/dist/config/auth.js +18 -0
  9. package/dist/config/jwt.js +24 -0
  10. package/dist/config/keys.js +14 -0
  11. package/dist/gateway/chat-store.d.ts +32 -5
  12. package/dist/gateway/chat-store.d.ts.map +1 -1
  13. package/dist/gateway/chat-store.js +52 -6
  14. package/dist/gateway/chat-store.js.map +1 -1
  15. package/dist/gateway/registry.d.ts.map +1 -1
  16. package/dist/gateway/registry.js +54 -10
  17. package/dist/gateway/registry.js.map +1 -1
  18. package/dist/inference/model-validator.d.ts.map +1 -1
  19. package/dist/inference/model-validator.js +11 -1
  20. package/dist/inference/model-validator.js.map +1 -1
  21. package/dist/inference/tool-call-utils.d.ts +31 -0
  22. package/dist/inference/tool-call-utils.d.ts.map +1 -1
  23. package/dist/inference/tool-call-utils.js +41 -0
  24. package/dist/inference/tool-call-utils.js.map +1 -1
  25. package/dist/learning/reasoning-trace.d.ts +5 -3
  26. package/dist/learning/reasoning-trace.d.ts.map +1 -1
  27. package/dist/learning/reasoning-trace.js +2 -2
  28. package/dist/learning/reasoning-trace.js.map +1 -1
  29. package/dist/middleware/auth.js +30 -0
  30. package/dist/nlu/conversation-gate.js +1 -1
  31. package/dist/nlu/conversation-gate.js.map +1 -1
  32. package/dist/nlu/entities.d.ts.map +1 -1
  33. package/dist/nlu/entities.js +12 -0
  34. package/dist/nlu/entities.js.map +1 -1
  35. package/dist/nlu/intent.d.ts.map +1 -1
  36. package/dist/nlu/intent.js +83 -15
  37. package/dist/nlu/intent.js.map +1 -1
  38. package/dist/passport.js +34 -0
  39. package/dist/routes/auth.js +42 -0
  40. package/dist/routes/user.js +31 -0
  41. package/dist/server.js +17 -0
  42. package/dist/tools/pipeline-tool.d.ts.map +1 -1
  43. package/dist/tools/pipeline-tool.js +14 -0
  44. package/dist/tools/pipeline-tool.js.map +1 -1
  45. package/dist/tools/release-sync.d.ts +52 -16
  46. package/dist/tools/release-sync.d.ts.map +1 -1
  47. package/dist/tools/release-sync.js +143 -79
  48. package/dist/tools/release-sync.js.map +1 -1
  49. package/dist/web-dashboard/src/App.js +85 -0
  50. package/dist/web-dashboard/src/admin-auth.test.js +186 -0
  51. package/dist/web-dashboard/src/ansi.js +23 -0
  52. package/dist/web-dashboard/src/ansi.test.js +31 -0
  53. package/dist/web-dashboard/src/api-admin-auth.test.js +172 -0
  54. package/dist/web-dashboard/src/api-admin.test.js +65 -0
  55. package/dist/web-dashboard/src/api-hub.test.js +117 -0
  56. package/dist/web-dashboard/src/api.js +1421 -0
  57. package/dist/web-dashboard/src/api.test.js +51 -0
  58. package/dist/web-dashboard/src/artifacts.js +128 -0
  59. package/dist/web-dashboard/src/artifacts.test.js +139 -0
  60. package/dist/web-dashboard/src/components/AdminPanel.js +567 -0
  61. package/dist/web-dashboard/src/components/AdminPanel.test.js +288 -0
  62. package/dist/web-dashboard/src/components/AgentHub.js +1580 -0
  63. package/dist/web-dashboard/src/components/AgentHub.test.js +343 -0
  64. package/dist/web-dashboard/src/components/BedrockOnboarding.js +320 -0
  65. package/dist/web-dashboard/src/components/BenchmarkCharts.js +228 -0
  66. package/dist/web-dashboard/src/components/ChatPage.js +1586 -0
  67. package/dist/web-dashboard/src/components/ChatPage.test.js +899 -0
  68. package/dist/web-dashboard/src/components/ContactsPage.js +141 -0
  69. package/dist/web-dashboard/src/components/CostDashboard.js +105 -0
  70. package/dist/web-dashboard/src/components/DAGView.js +477 -0
  71. package/dist/web-dashboard/src/components/DAGView.test.js +147 -0
  72. package/dist/web-dashboard/src/components/EnvVarEditor.js +138 -0
  73. package/dist/web-dashboard/src/components/EvalsPage.js +73 -0
  74. package/dist/web-dashboard/src/components/EvalsPage.test.js +120 -0
  75. package/dist/web-dashboard/src/components/ExecutionHistory.js +201 -0
  76. package/dist/web-dashboard/src/components/GatewayPage.js +40 -0
  77. package/dist/web-dashboard/src/components/GatewayPage.test.js +74 -0
  78. package/dist/web-dashboard/src/components/HealthPanel.js +68 -0
  79. package/dist/web-dashboard/src/components/HistoryBrowser.js +34 -0
  80. package/dist/web-dashboard/src/components/Layout.js +50 -0
  81. package/dist/web-dashboard/src/components/Markdown.js +152 -0
  82. package/dist/web-dashboard/src/components/Markdown.test.js +126 -0
  83. package/dist/web-dashboard/src/components/MarkdownZeroDep.js +237 -0
  84. package/dist/web-dashboard/src/components/MemoryPanel.js +81 -0
  85. package/dist/web-dashboard/src/components/ModelTimeline.js +298 -0
  86. package/dist/web-dashboard/src/components/ModelsPanel.js +1484 -0
  87. package/dist/web-dashboard/src/components/ModelsPanel.test.js +460 -0
  88. package/dist/web-dashboard/src/components/Overview.js +123 -0
  89. package/dist/web-dashboard/src/components/PhaseTimeline.js +359 -0
  90. package/dist/web-dashboard/src/components/PhaseTimeline.test.js +234 -0
  91. package/dist/web-dashboard/src/components/PlatformConfigSection.js +384 -0
  92. package/dist/web-dashboard/src/components/PlatformConfigSection.test.js +106 -0
  93. package/dist/web-dashboard/src/components/PlatformsPage.js +250 -0
  94. package/dist/web-dashboard/src/components/QuotaPanel.js +159 -0
  95. package/dist/web-dashboard/src/components/QuotaPanel.test.js +85 -0
  96. package/dist/web-dashboard/src/components/RequestsPanel.js +229 -0
  97. package/dist/web-dashboard/src/components/RequestsPanel.test.js +103 -0
  98. package/dist/web-dashboard/src/components/RoutingInsightsPanel.js +938 -0
  99. package/dist/web-dashboard/src/components/RoutingInsightsPanel.test.js +339 -0
  100. package/dist/web-dashboard/src/components/RoutingWalkthrough.js +408 -0
  101. package/dist/web-dashboard/src/components/RoutingWalkthrough.test.js +209 -0
  102. package/dist/web-dashboard/src/components/TaskConsole.js +119 -0
  103. package/dist/web-dashboard/src/components/TaskConsole.test.js +123 -0
  104. package/dist/web-dashboard/src/components/TasksPage.js +256 -0
  105. package/dist/web-dashboard/src/components/TasksPage.test.js +103 -0
  106. package/dist/web-dashboard/src/components/TracePanel.js +306 -0
  107. package/dist/web-dashboard/src/components/WhatsAppPanel.js +195 -0
  108. package/dist/web-dashboard/src/components/WhatsAppPanel.test.js +122 -0
  109. package/dist/web-dashboard/src/jsonOrNull.js +37 -0
  110. package/dist/web-dashboard/src/main.js +15 -0
  111. package/dist/web-dashboard/src/mask.js +11 -0
  112. package/dist/web-dashboard/vite.config.js +23 -0
  113. package/dist/web-dashboard/vitest.config.js +15 -0
  114. package/package.json +5 -3
@@ -0,0 +1,938 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.GovernanceSection = GovernanceSection;
7
+ exports.RbacSection = RbacSection;
8
+ exports.default = RoutingInsightsPanel;
9
+ const RoutingWalkthrough_1 = __importDefault(require("./RoutingWalkthrough"));
10
+ // ─── Helpers ────────────────────────────────────────────────────────────────
11
+ const PROVIDER_ICONS = {
12
+ local: '💻', groq: '🟢', nim: '🔶', gemini: '🔷', openrouter: '🟣',
13
+ };
14
+ const PROVIDER_LABELS = {
15
+ local: 'Ollama (Local)', groq: 'Groq', nim: 'NVIDIA NIM',
16
+ gemini: 'Gemini', openrouter: 'OpenRouter',
17
+ };
18
+ const COMPLEXITY_ICONS = {
19
+ trivial: '🟢', simple: '🔵', moderate: '🟡', complex: '🟠', critical: '🔴',
20
+ };
21
+ function providerIcon(provider) {
22
+ return PROVIDER_ICONS[provider] || '🔌';
23
+ }
24
+ function providerLabel(provider) {
25
+ return PROVIDER_LABELS[provider] || provider;
26
+ }
27
+ function pct(value, digits = 1) {
28
+ return `${(value * 100).toFixed(digits)}%`;
29
+ }
30
+ function usd(value) {
31
+ return `$${value.toFixed(6)}`;
32
+ }
33
+ // ─── Small Components ───────────────────────────────────────────────────────
34
+ function SectionCard({ icon, title, subtitle, children }) {
35
+ return (<div style={{
36
+ background: '#161b22', borderRadius: 12,
37
+ border: '1px solid #21262d', padding: '18px 20px', marginBottom: 16,
38
+ }}>
39
+ <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 4 }}>
40
+ <span style={{ fontSize: 20 }}>{icon}</span>
41
+ <h3 style={{ fontSize: 15, fontWeight: 600, color: '#e6edf3', margin: 0 }}>{title}</h3>
42
+ </div>
43
+ {subtitle && <p style={{ fontSize: 12, color: '#8b949e', margin: '2px 0 12px 0' }}>{subtitle}</p>}
44
+ {!subtitle && <div style={{ height: 6 }}/>}
45
+ {children}
46
+ </div>);
47
+ }
48
+ function ScoreBar({ value, color }) {
49
+ const pctWidth = Math.min(100, Math.max(0, value * 100));
50
+ return (<div style={{
51
+ flex: 1, background: '#0d1117', borderRadius: 4, height: 6,
52
+ overflow: 'hidden', border: '1px solid #21262d',
53
+ }}>
54
+ <div style={{
55
+ width: `${pctWidth}%`, background: color, height: '100%',
56
+ transition: 'width 0.4s ease',
57
+ }}/>
58
+ </div>);
59
+ }
60
+ function chipStyle(background, border, textColor = '#8b949e') {
61
+ return {
62
+ fontSize: 10,
63
+ padding: '1px 7px',
64
+ borderRadius: 9,
65
+ background,
66
+ border: `1px solid ${border}`,
67
+ color: textColor,
68
+ fontFamily: "'SFMono-Regular', Consolas, monospace",
69
+ whiteSpace: 'nowrap',
70
+ };
71
+ }
72
+ function EmptyNote() {
73
+ return (<div style={{
74
+ background: '#0d1117', border: '1px dashed #30363d', borderRadius: 10,
75
+ padding: '18px 20px', color: '#8b949e', fontSize: 13, textAlign: 'center',
76
+ }}>
77
+ 📊 No routing data yet — run <code style={{ color: '#58a6ff' }}>buff benchmark</code> to populate
78
+ provider quality scores, or use Auto routing (<code style={{ color: '#58a6ff' }}>buff model switch auto</code>)
79
+ to build per-agent best-model stats over time.
80
+ </div>);
81
+ }
82
+ const SOURCE_LABELS = {
83
+ chat: '💬 chat',
84
+ orchestrator: '🔀 orchestrator',
85
+ explain: '🔍 explain',
86
+ benchmark: '📈 benchmark',
87
+ eval: '🎯 eval',
88
+ };
89
+ const COMPLEXITY_LABELS = {
90
+ trivial: '🟢 trivial',
91
+ simple: '🔵 simple',
92
+ moderate: '🟡 moderate',
93
+ complex: '🟠 complex',
94
+ critical: '🔴 critical',
95
+ };
96
+ function sourceLabel(source) {
97
+ return SOURCE_LABELS[source] || source;
98
+ }
99
+ function timeAgo(ts) {
100
+ const diff = Date.now() - ts;
101
+ if (diff < 60_000)
102
+ return 'just now';
103
+ if (diff < 3_600_000)
104
+ return `${Math.floor(diff / 60_000)}m ago`;
105
+ if (diff < 86_400_000)
106
+ return `${Math.floor(diff / 3_600_000)}h ago`;
107
+ return `${Math.floor(diff / 86_400_000)}d ago`;
108
+ }
109
+ function fmtDuration(ms) {
110
+ if (ms <= 0)
111
+ return 'now';
112
+ const h = Math.floor(ms / 3_600_000);
113
+ const m = Math.floor((ms % 3_600_000) / 60_000);
114
+ if (h > 0)
115
+ return `${h}h ${m}m`;
116
+ if (m > 0)
117
+ return `${m}m`;
118
+ return `${Math.ceil(ms / 1000)}s`;
119
+ }
120
+ // ─── Routing Usage Stats (actual picks over time) ───────────────────────────
121
+ function UsageCountRow({ label, value, total, color }) {
122
+ const pct = total > 0 ? (value / total) * 100 : 0;
123
+ return (<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 5 }}>
124
+ <span style={{ width: 110, color: '#e6edf3', whiteSpace: 'nowrap', fontSize: 12 }}>{label}</span>
125
+ <div style={{ flex: 1, background: '#0d1117', borderRadius: 4, height: 6, overflow: 'hidden', border: '1px solid #21262d' }}>
126
+ <div style={{ width: `${Math.min(100, pct)}%`, background: color, height: '100%', transition: 'width 0.4s ease' }}/>
127
+ </div>
128
+ <span style={{ width: 34, textAlign: 'right', fontFamily: "'SFMono-Regular', Consolas, monospace", fontSize: 11, color: '#8b949e' }}>
129
+ {value}
130
+ </span>
131
+ </div>);
132
+ }
133
+ function UsageSection({ usage }) {
134
+ if (!usage.total)
135
+ return null;
136
+ const total = usage.total;
137
+ const topProviders = Object.entries(usage.byProvider).sort((a, b) => b[1] - a[1]).slice(0, 6);
138
+ const topModels = Object.entries(usage.byModel).sort((a, b) => b[1] - a[1]).slice(0, 5);
139
+ return (<SectionCard icon="🧮" title="Routing Usage — actual picks over time" subtitle="Recorded from live chat, orchestrator runs, explain snapshots, benchmark --routing, and eval --routing">
140
+ <div style={{ display: 'flex', gap: 20, flexWrap: 'wrap', marginBottom: 14 }}>
141
+ <div style={{ background: '#0d1117', border: '1px solid #21262d', borderRadius: 10, padding: '12px 18px', textAlign: 'center' }}>
142
+ <div style={{ fontSize: 22, fontWeight: 700, color: '#e6edf3', fontFamily: "'SFMono-Regular', Consolas, monospace" }}>{usage.total}</div>
143
+ <div style={{ fontSize: 11, color: '#8b949e', marginTop: 2 }}>total decisions</div>
144
+ </div>
145
+ <div style={{ background: '#0d1117', border: '1px solid #21262d', borderRadius: 10, padding: '12px 18px', textAlign: 'center' }}>
146
+ <div style={{ fontSize: 22, fontWeight: 700, color: usage.last24h > 0 ? '#3fb950' : '#8b949e', fontFamily: "'SFMono-Regular', Consolas, monospace" }}>{usage.last24h}</div>
147
+ <div style={{ fontSize: 11, color: '#8b949e', marginTop: 2 }}>last 24h</div>
148
+ </div>
149
+ </div>
150
+
151
+ <div style={{ fontSize: 12, color: '#8b949e', marginBottom: 6 }}>By provider</div>
152
+ {topProviders.map(([provider, count]) => (<UsageCountRow key={provider} label={`${providerIcon(provider)} ${providerLabel(provider).split(' ')[0]}`} value={count} total={total} color="#58a6ff"/>))}
153
+
154
+ {Object.keys(usage.bySource).length > 0 && (<div style={{ marginTop: 12, display: 'flex', gap: 6, flexWrap: 'wrap' }}>
155
+ {Object.entries(usage.bySource).sort((a, b) => b[1] - a[1]).map(([source, count]) => (<span key={source} style={{
156
+ background: '#0d1117', border: '1px solid #21262d', borderRadius: 20,
157
+ padding: '3px 10px', fontSize: 11, color: '#e6edf3',
158
+ }}>
159
+ {sourceLabel(source)} · {count}
160
+ </span>))}
161
+ </div>)}
162
+
163
+ {topModels.length > 0 && (<div style={{ marginTop: 14 }}>
164
+ <div style={{ fontSize: 12, color: '#8b949e', marginBottom: 6 }}>Most-picked models</div>
165
+ {topModels.map(([model, count]) => (<div key={model} style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 5 }}>
166
+ <span style={{ flex: 1, color: '#e6edf3', fontSize: 12, fontFamily: "'SFMono-Regular', Consolas, monospace", whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }} title={model}>
167
+ {model.length > 42 ? model.slice(0, 39) + '…' : model}
168
+ </span>
169
+ <span style={{ width: 34, textAlign: 'right', fontFamily: "'SFMono-Regular', Consolas, monospace", fontSize: 11, color: '#8b949e' }}>{count}</span>
170
+ </div>))}
171
+ </div>)}
172
+ </SectionCard>);
173
+ }
174
+ // ─── Audit Trail (explain snapshots timeline) ───────────────────────────────
175
+ function AuditTimelineSection({ history }) {
176
+ if (!history.length)
177
+ return null;
178
+ return (<SectionCard icon="🕓" title="Audit Trail — routing decision timeline" subtitle="Every explain snapshot (and routing-mode pick) persisted for transparency">
179
+ <div style={{ maxHeight: 320, overflowY: 'auto', paddingRight: 4 }}>
180
+ {history.map((h) => (<div key={h.id} style={{
181
+ display: 'flex', gap: 10, padding: '8px 4px',
182
+ borderBottom: '1px solid #21262d',
183
+ }}>
184
+ <div style={{ width: 52, flexShrink: 0, fontSize: 11, color: '#6e7681', paddingTop: 2 }}>
185
+ {timeAgo(h.timestamp)}
186
+ </div>
187
+ <div style={{ flex: 1, minWidth: 0 }}>
188
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
189
+ <span style={{
190
+ fontSize: 11, padding: '1px 8px', borderRadius: 10,
191
+ background: '#0d1117', border: '1px solid #30363d', color: '#8b949e',
192
+ }}>
193
+ {sourceLabel(h.source)}
194
+ </span>
195
+ <span style={{ fontSize: 12, fontWeight: 600, color: '#3fb950', fontFamily: "'SFMono-Regular', Consolas, monospace", whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }} title={`${h.provider}/${h.model}`}>
196
+ {providerIcon(h.provider)} {h.provider}/{h.model.length > 26 ? h.model.slice(0, 23) + '…' : h.model}
197
+ </span>
198
+ {h.complexity && (<span style={{ fontSize: 11, color: '#6e7681' }}>
199
+ {COMPLEXITY_LABELS[h.complexity] || h.complexity}
200
+ </span>)}
201
+ </div>
202
+ <div style={{ fontSize: 12, color: '#8b949e', marginTop: 3, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} title={h.task}>
203
+ {h.task || h.agentType}
204
+ </div>
205
+ </div>
206
+ </div>))}
207
+ </div>
208
+ </SectionCard>);
209
+ }
210
+ // ─── Auto Router Preference ─────────────────────────────────────────────────
211
+ function PreferenceSection({ routing }) {
212
+ if (!routing.preference.length)
213
+ return null;
214
+ return (<SectionCard icon="🤖" title="Auto Router — What the agent would pick" subtitle="Complexity-weighted scoring across reasoning, speed, cost, privacy, and reliability (real provider pricing)">
215
+ <div style={{ overflowX: 'auto' }}>
216
+ <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13 }}>
217
+ <thead>
218
+ <tr style={{ borderBottom: '1px solid #21262d', color: '#8b949e' }}>
219
+ <th style={{ padding: '8px 12px', textAlign: 'left', fontWeight: 500 }}>Complexity</th>
220
+ <th style={{ padding: '8px 12px', textAlign: 'left', fontWeight: 500 }}>Winner</th>
221
+ <th style={{ padding: '8px 12px', textAlign: 'left', fontWeight: 500 }}>Score</th>
222
+ <th style={{ padding: '8px 12px', textAlign: 'left', fontWeight: 500 }}>All providers</th>
223
+ </tr>
224
+ </thead>
225
+ <tbody>
226
+ {routing.preference.map((p) => (<tr key={p.complexity} style={{ borderBottom: '1px solid #21262d' }}>
227
+ <td style={{ padding: '10px 12px', color: '#e6edf3', whiteSpace: 'nowrap' }}>
228
+ {COMPLEXITY_ICONS[p.complexity] || '•'} {p.complexity}
229
+ </td>
230
+ <td style={{ padding: '10px 12px', whiteSpace: 'nowrap' }}>
231
+ <span style={{ color: '#3fb950', fontWeight: 600 }}>
232
+ {providerIcon(p.winner.split('/')[0])} {p.winner}
233
+ </span>
234
+ </td>
235
+ <td style={{ padding: '10px 12px', fontFamily: "'SFMono-Regular', Consolas, monospace", color: '#8b949e' }}>
236
+ {p.score.toFixed(3)}
237
+ </td>
238
+ <td style={{ padding: '10px 12px', minWidth: 300 }}>
239
+ {p.providers.map((prov, i) => (<div key={prov.provider} style={{ marginBottom: 5 }}>
240
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
241
+ <span style={{ width: 16, fontSize: 11, color: '#6e7681', textAlign: 'right' }}>{i + 1}.</span>
242
+ <span style={{ width: 90, color: '#e6edf3', whiteSpace: 'nowrap', fontSize: 12 }}>
243
+ {providerIcon(prov.provider)} {providerLabel(prov.provider).split(' ')[0]}
244
+ </span>
245
+ <ScoreBar value={prov.score} color={i === 0 ? '#3fb950' : '#58a6ff'}/>
246
+ <span style={{ width: 40, textAlign: 'right', fontFamily: "'SFMono-Regular', Consolas, monospace", fontSize: 11, color: '#8b949e' }}>
247
+ {prov.score.toFixed(2)}
248
+ </span>
249
+ </div>
250
+ {/* v1.58.0 M2.x chips — same guarantees as the CLI `model explain` output.
251
+ Rendered only when at least one chip applies, so rows under gates-off
252
+ configs don't leave an empty strip. */}
253
+ {(prov.capabilityFit !== undefined || prov.costSource || prov.contextUtilization !== undefined) && (<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', paddingLeft: 16, marginTop: 2 }}>
254
+ {prov.capabilityFit !== undefined && (<span style={chipStyle('#1c2128', '#6e7681')}>🎯 fit {prov.capabilityFit}%</span>)}
255
+ {prov.costSource === 'measured' && prov.costBasis ? (<span style={chipStyle('#12291a', '#3fb950', '#3fb950')}>
256
+ 📏 measured {prov.costBasis.inputTokens.toLocaleString()}→{prov.costBasis.outputTokens.toLocaleString()} tok
257
+ </span>) : (<span style={chipStyle('#1c2128', '#8b949e')}>📐 estimated</span>)}
258
+ {prov.contextUtilization !== undefined && prov.contextWindowTokens !== undefined && (<span style={chipStyle('#1c2128', '#58a6ff', '#58a6ff')}>
259
+ ⏳ ctx {prov.contextUtilization}% ({prov.contextWindowTokens.toLocaleString()} tok)
260
+ </span>)}
261
+ </div>)}
262
+ </div>))}
263
+ </td>
264
+ </tr>))}
265
+ </tbody>
266
+ </table>
267
+ </div>
268
+ </SectionCard>);
269
+ }
270
+ // ─── Provider Benchmark Quality ─────────────────────────────────────────────
271
+ function ProviderQualitySection({ routing }) {
272
+ if (!routing.providers.length)
273
+ return null;
274
+ return (<SectionCard icon="📈" title="Provider Benchmark Quality" subtitle="Measured from your own `buff benchmark` runs — blended into Auto routing decisions">
275
+ <div style={{ overflowX: 'auto' }}>
276
+ <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13 }}>
277
+ <thead>
278
+ <tr style={{ borderBottom: '1px solid #21262d', color: '#8b949e' }}>
279
+ <th style={{ padding: '8px 12px', textAlign: 'left', fontWeight: 500 }}>Provider</th>
280
+ <th style={{ padding: '8px 12px', textAlign: 'left', fontWeight: 500 }}>Runs</th>
281
+ <th style={{ padding: '8px 12px', textAlign: 'left', fontWeight: 500 }}>Avg Quality</th>
282
+ <th style={{ padding: '8px 12px', textAlign: 'left', fontWeight: 500 }}>Pass Rate</th>
283
+ <th style={{ padding: '8px 12px', textAlign: 'left', fontWeight: 500 }}>Total Cost</th>
284
+ </tr>
285
+ </thead>
286
+ <tbody>
287
+ {routing.providers.map((p) => (<tr key={p.provider} style={{ borderBottom: '1px solid #21262d' }}>
288
+ <td style={{ padding: '10px 12px', color: '#e6edf3', whiteSpace: 'nowrap' }}>
289
+ {providerIcon(p.provider)} {providerLabel(p.provider)}
290
+ </td>
291
+ <td style={{ padding: '10px 12px', color: '#8b949e' }}>{p.runs}</td>
292
+ <td style={{ padding: '10px 12px' }}>
293
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
294
+ <ScoreBar value={p.avgQuality} color="#58a6ff"/>
295
+ <span style={{ fontFamily: "'SFMono-Regular', Consolas, monospace", fontSize: 12, color: '#8b949e', width: 44, textAlign: 'right' }}>
296
+ {pct(p.avgQuality)}
297
+ </span>
298
+ </div>
299
+ </td>
300
+ <td style={{ padding: '10px 12px', color: p.passRate >= 0.8 ? '#3fb950' : '#d29922' }}>
301
+ {pct(p.passRate)}
302
+ </td>
303
+ <td style={{ padding: '10px 12px', fontFamily: "'SFMono-Regular', Consolas, monospace", color: '#8b949e' }}>
304
+ {usd(p.totalCostUsd)}
305
+ </td>
306
+ </tr>))}
307
+ </tbody>
308
+ </table>
309
+ </div>
310
+ </SectionCard>);
311
+ }
312
+ // ─── Bandit Learning (Thompson-sampling priors) ────────────────────────────
313
+ const BANDIT_BUCKETS = ['trivial', 'simple', 'moderate', 'complex', 'critical'];
314
+ function banditWinColor(rate) {
315
+ if (rate >= 0.7)
316
+ return '#3fb950';
317
+ if (rate >= 0.45)
318
+ return '#d29922';
319
+ return '#f85149';
320
+ }
321
+ function BanditSection({ bandit }) {
322
+ const providers = Object.keys(bandit.priors).sort();
323
+ if (!bandit.enabled || providers.length === 0)
324
+ return null;
325
+ const recentHistory = bandit.learningHistory.slice(-15).reverse();
326
+ return (<SectionCard icon="🎰" title="Bandit Learning — Thompson-sampling priors" subtitle="Beta(α, β) per provider × complexity bucket, learned from real task outcomes (routing.bandit = true). Higher expected win rate = more successful history.">
327
+ <div style={{ overflowX: 'auto' }}>
328
+ <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 12 }}>
329
+ <thead>
330
+ <tr style={{ borderBottom: '1px solid #21262d', color: '#8b949e' }}>
331
+ <th style={{ padding: '8px 10px', textAlign: 'left', fontWeight: 500 }}>Provider</th>
332
+ {BANDIT_BUCKETS.map((b) => (<th key={b} style={{ padding: '8px 10px', textAlign: 'center', fontWeight: 500 }}>
333
+ {COMPLEXITY_ICONS[b] || '•'} {b}
334
+ </th>))}
335
+ </tr>
336
+ </thead>
337
+ <tbody>
338
+ {providers.map((provider) => (<tr key={provider} style={{ borderBottom: '1px solid #21262d' }}>
339
+ <td style={{ padding: '8px 10px', color: '#e6edf3', whiteSpace: 'nowrap' }}>
340
+ {providerIcon(provider)} {providerLabel(provider).split(' ')[0]}
341
+ </td>
342
+ {BANDIT_BUCKETS.map((bucket) => {
343
+ const prior = bandit.priors[provider]?.[bucket];
344
+ if (!prior || (prior.alpha === 0 && prior.beta === 0)) {
345
+ return <td key={bucket} style={{ padding: '8px 10px', textAlign: 'center', color: '#6e7681' }}>·</td>;
346
+ }
347
+ return (<td key={bucket} style={{ padding: '8px 10px', textAlign: 'center' }}>
348
+ <div style={{ fontSize: 13, fontWeight: 600, color: banditWinColor(prior.expectedWinRate), fontFamily: "'SFMono-Regular', Consolas, monospace" }}>
349
+ {(prior.expectedWinRate * 100).toFixed(0)}%
350
+ </div>
351
+ <div style={{ fontSize: 10, color: '#8b949e', fontFamily: "'SFMono-Regular', Consolas, monospace" }}>
352
+ α{prior.alpha} β{prior.beta}
353
+ </div>
354
+ </td>);
355
+ })}
356
+ </tr>))}
357
+ </tbody>
358
+ </table>
359
+ </div>
360
+
361
+ {recentHistory.length > 0 && (<div style={{ marginTop: 16 }}>
362
+ <div style={{ fontSize: 12, color: '#8b949e', marginBottom: 8 }}>Recent learning history</div>
363
+ <div style={{ maxHeight: 180, overflowY: 'auto', paddingRight: 4 }}>
364
+ {recentHistory.map((h, i) => {
365
+ const icon = h.outcome === 'success' ? '✅' : h.outcome === 'escalated' ? '🔄' : '❌';
366
+ const ts = new Date(h.timestamp).toLocaleTimeString();
367
+ return (<div key={i} style={{ display: 'flex', gap: 8, alignItems: 'center', padding: '5px 4px', borderBottom: '1px solid #21262d' }}>
368
+ <span>{icon}</span>
369
+ <span style={{ color: '#e6edf3', fontSize: 12, width: 110, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }} title={h.provider}>
370
+ {h.provider}
371
+ </span>
372
+ <span style={{ fontSize: 11, color: '#6e7681', width: 80 }}>{h.complexity}</span>
373
+ <span style={{ fontSize: 11, color: '#8b949e', fontFamily: "'SFMono-Regular', Consolas, monospace" }}>
374
+ reward {h.reward.toFixed(2)}
375
+ </span>
376
+ <span style={{ fontSize: 11, color: '#6e7681', marginLeft: 'auto' }}>{ts}</span>
377
+ </div>);
378
+ })}
379
+ </div>
380
+ </div>)}
381
+
382
+ <div style={{ fontSize: 11, color: '#6e7681', marginTop: 12 }}>
383
+ Cold-start Beta(1,1) behaves like the heuristic router until outcomes accumulate.
384
+ Enable learning with <code style={{ color: '#58a6ff' }}>buff config set routing.bandit true</code>.
385
+ </div>
386
+ </SectionCard>);
387
+ }
388
+ // ─── Promotion Gate (bandit vs heuristic A/B verdict) ──────────────────────
389
+ function deltaPct(value, digits = 1) {
390
+ const sign = value > 0 ? '+' : '';
391
+ return `${sign}${(value * 100).toFixed(digits)}%`;
392
+ }
393
+ function PassChip({ state }) {
394
+ const styles = {
395
+ pass: { bg: '#12291a', border: '#238636', color: '#3fb950', label: '✓ pass' },
396
+ fail: { bg: '#2d1616', border: '#f85149', color: '#f85149', label: '✗ fail' },
397
+ neutral: { bg: '#1c2128', border: '#6e7681', color: '#8b949e', label: '○ neutral' },
398
+ }[state];
399
+ return (<span style={{
400
+ fontSize: 11, padding: '1px 8px', borderRadius: 10,
401
+ background: styles.bg,
402
+ border: `1px solid ${styles.border}`,
403
+ color: styles.color,
404
+ }}>
405
+ {styles.label}
406
+ </span>);
407
+ }
408
+ function PromotionGateSection({ promotion }) {
409
+ if (!promotion.decisionCount)
410
+ return null;
411
+ const verdict = promotion.promoted
412
+ ? { icon: '🎖️', label: 'Promoted — the bandit beats the heuristic', color: '#3fb950' }
413
+ : promotion.sufficient
414
+ ? { icon: '⚠️', label: 'Not promoted — the bandit is not (yet) better', color: '#d29922' }
415
+ : { icon: '⏳', label: 'Collecting data — need more diverged decisions', color: '#58a6ff' };
416
+ const progress = promotion.minDecisions > 0
417
+ ? Math.min(100, Math.round((promotion.divergedCount / promotion.minDecisions) * 100))
418
+ : 0;
419
+ const rows = [
420
+ {
421
+ key: 'quality', label: 'Quality ↑',
422
+ delta: promotion.qualityDelta,
423
+ state: promotion.criteria.quality ? 'pass' : 'fail',
424
+ note: 'needs > +2%',
425
+ },
426
+ {
427
+ key: 'cost', label: 'Cost ↓',
428
+ delta: promotion.costDelta,
429
+ state: promotion.criteria.cost ? 'pass' : 'fail',
430
+ note: 'regression < +1%',
431
+ },
432
+ {
433
+ key: 'latency', label: 'Latency ↓',
434
+ delta: promotion.latencyDelta,
435
+ // Unmeasured latency is treated as neutral by the gate (never a win,
436
+ // never a fail) — reflect that honestly instead of a green 'pass'.
437
+ state: promotion.latencyMeasured
438
+ ? (promotion.criteria.latency ? 'pass' : 'fail')
439
+ : 'neutral',
440
+ note: promotion.latencyMeasured ? 'regression < +5%' : 'no latency measurements yet',
441
+ },
442
+ ];
443
+ return (<SectionCard icon="🎖️" title="Promotion Gate — is the bandit better than the heuristic?" subtitle="A/B verdict from real trajectories (router-promotion.jsonl): quality must improve >2% while cost and latency don't regress (ruflo ADR-150)">
444
+ <div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 14 }}>
445
+ <span style={{ fontSize: 22 }}>{verdict.icon}</span>
446
+ <span style={{ fontSize: 14, fontWeight: 600, color: verdict.color }}>{verdict.label}</span>
447
+ </div>
448
+
449
+ <div style={{ marginBottom: 14 }}>
450
+ <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 11, color: '#8b949e', marginBottom: 5 }}>
451
+ <span>{promotion.divergedCount} diverged decisions</span>
452
+ <span>need {promotion.minDecisions} for a verdict</span>
453
+ </div>
454
+ <div style={{ background: '#0d1117', borderRadius: 4, height: 8, overflow: 'hidden', border: '1px solid #21262d' }}>
455
+ <div style={{ width: `${progress}%`, background: promotion.sufficient ? '#3fb950' : '#58a6ff', height: '100%', transition: 'width 0.4s ease' }}/>
456
+ </div>
457
+ <div style={{ fontSize: 11, color: '#6e7681', marginTop: 4 }}>
458
+ {promotion.decisionCount} total decisions logged
459
+ </div>
460
+ </div>
461
+
462
+ {rows.map((row) => (<div key={row.key} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '7px 0', borderBottom: '1px solid #21262d' }}>
463
+ <span style={{ width: 70, fontSize: 12, color: '#e6edf3' }}>{row.label}</span>
464
+ <span style={{ width: 92, fontFamily: "'SFMono-Regular', Consolas, monospace", fontSize: 12, color: '#8b949e' }}>
465
+ {deltaPct(row.delta)}
466
+ </span>
467
+ <span style={{ fontSize: 11, color: '#6e7681', flex: 1 }}>{row.note}</span>
468
+ <PassChip state={row.state}/>
469
+ </div>))}
470
+
471
+ <div style={{ fontSize: 11, color: '#6e7681', marginTop: 12 }}>
472
+ Run auto-routed tasks with <code style={{ color: '#58a6ff' }}>routing.bandit true</code> to accumulate A/B
473
+ decisions. The gate does not disable the bandit — it tells you whether it's actually winning.
474
+ </div>
475
+ </SectionCard>);
476
+ }
477
+ // ─── ML Task-Similarity Router (v1.71.0, ruflo neural-router analog) ────────
478
+ function MlSection({ ml }) {
479
+ if (!ml.enabled || ml.recordCount === 0)
480
+ return null;
481
+ const trusted = ml.providers.filter((p) => p.trusted);
482
+ const learning = ml.providers.filter((p) => !p.trusted);
483
+ return (<SectionCard icon="🧠" title="ML Router — learned from similar tasks" subtitle="Task-feature kNN (ruflo neural-router analog): outcomes of the most similar PAST tasks nudge each provider's score. Win rate is similarity-weighted; factor = 1 + 0.5 × (winRate − 0.5), trusted only after 5 similar-task samples.">
484
+ <div style={{ display: 'flex', gap: 20, flexWrap: 'wrap', marginBottom: 14 }}>
485
+ <div style={{ background: '#0d1117', border: '1px solid #21262d', borderRadius: 10, padding: '12px 18px', textAlign: 'center' }}>
486
+ <div style={{ fontSize: 22, fontWeight: 700, color: '#e6edf3', fontFamily: "'SFMono-Regular', Consolas, monospace" }}>
487
+ {ml.recordCount.toLocaleString()}
488
+ </div>
489
+ <div style={{ fontSize: 11, color: '#8b949e', marginTop: 2 }}>learned tasks</div>
490
+ </div>
491
+ <div style={{ background: '#0d1117', border: '1px solid #238636', borderRadius: 10, padding: '12px 18px', textAlign: 'center' }}>
492
+ <div style={{ fontSize: 22, fontWeight: 700, color: '#3fb950', fontFamily: "'SFMono-Regular', Consolas, monospace" }}>
493
+ {trusted.length}
494
+ </div>
495
+ <div style={{ fontSize: 11, color: '#8b949e', marginTop: 2 }}>trusted providers</div>
496
+ </div>
497
+ <div style={{ background: '#0d1117', border: '1px solid #d29922', borderRadius: 10, padding: '12px 18px', textAlign: 'center' }}>
498
+ <div style={{ fontSize: 22, fontWeight: 700, color: '#d29922', fontFamily: "'SFMono-Regular', Consolas, monospace" }}>
499
+ {learning.length}
500
+ </div>
501
+ <div style={{ fontSize: 11, color: '#8b949e', marginTop: 2 }}>still learning</div>
502
+ </div>
503
+ </div>
504
+
505
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
506
+ {ml.providers.map((p) => (<div key={p.provider} style={{ background: '#0d1117', border: '1px solid #21262d', borderRadius: 10, padding: '10px 14px' }}>
507
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6 }}>
508
+ <span>{providerIcon(p.provider)}</span>
509
+ <span style={{ fontSize: 13, fontWeight: 600, color: '#e6edf3' }}>{providerLabel(p.provider)}</span>
510
+ {p.trusted ? (<span style={chipStyle('#3fb95026', '#238636', '#3fb950')}>trusted · {p.samples} samples</span>) : (<span style={chipStyle('#d2992226', '#9e6a03', '#d29922')}>{p.samples}/5 samples</span>)}
511
+ {p.model && (<span style={{ fontSize: 11, color: '#6e7681', fontFamily: "'SFMono-Regular', Consolas, monospace", marginLeft: 'auto' }}>
512
+ {p.model.length > 30 ? p.model.slice(0, 27) + '…' : p.model}
513
+ </span>)}
514
+ </div>
515
+ <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
516
+ <div style={{ width: 70, fontSize: 11, color: '#8b949e' }}>
517
+ win {pct(p.winRate, 0)}
518
+ </div>
519
+ <ScoreBar value={p.winRate} color={p.winRate >= 0.5 ? '#3fb950' : '#f85149'}/>
520
+ <div style={{ width: 74, fontSize: 11, color: '#8b949e', textAlign: 'right' }}>
521
+ factor <span style={{ color: p.trusted && p.factor !== 1 ? '#58a6ff' : '#6e7681', fontFamily: "'SFMono-Regular', Consolas, monospace" }}>
522
+ {p.factor.toFixed(2)}×
523
+ </span>
524
+ </div>
525
+ </div>
526
+ </div>))}
527
+ </div>
528
+
529
+ <div style={{ fontSize: 11, color: '#6e7681', marginTop: 12 }}>
530
+ Enabled with <code style={{ color: '#58a6ff' }}>buff config set routing.mlRouter true</code> — the factor only
531
+ adjusts scores when the provider has ≥ 5 similar-task wins (cold start is neutral).
532
+ </div>
533
+ </SectionCard>);
534
+ }
535
+ // ─── Best Model per Agent ───────────────────────────────────────────────────
536
+ function BestModelsSection({ routing }) {
537
+ if (!routing.bestModels.length)
538
+ return null;
539
+ return (<SectionCard icon="🏆" title="Best Model per Agent (from your runs)" subtitle="Success-rate-ranked models from agent-stats — the Auto router boosts the proven winner">
540
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: 10 }}>
541
+ {routing.bestModels.map((b) => (<div key={`${b.agentType}-${b.model}`} style={{
542
+ background: '#0d1117', border: '1px solid #21262d', borderRadius: 10,
543
+ padding: '12px 14px', minWidth: 200, flex: '1 1 200px',
544
+ transition: 'border-color 0.2s, box-shadow 0.2s',
545
+ }} onMouseEnter={(e) => {
546
+ e.currentTarget.style.borderColor = '#3fb950';
547
+ e.currentTarget.style.boxShadow = '0 2px 10px #3fb95022';
548
+ }} onMouseLeave={(e) => {
549
+ e.currentTarget.style.borderColor = '#21262d';
550
+ e.currentTarget.style.boxShadow = 'none';
551
+ }}>
552
+ <div style={{ fontSize: 12, color: '#8b949e', marginBottom: 4 }}>
553
+ {providerIcon(b.model.split('/')[0])} {b.agentType}
554
+ </div>
555
+ <div style={{
556
+ fontSize: 13, fontWeight: 600, color: '#e6edf3',
557
+ fontFamily: "'SFMono-Regular', Consolas, monospace",
558
+ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
559
+ }} title={b.model}>
560
+ {b.model.length > 34 ? b.model.slice(0, 31) + '…' : b.model}
561
+ </div>
562
+ <div style={{ fontSize: 11, color: '#6e7681', marginTop: 6 }}>
563
+ <span style={{ color: b.successRate >= 0.8 ? '#3fb950' : '#d29922' }}>
564
+ {pct(b.successRate, 0)} success
565
+ </span>
566
+ {' · '}{b.runs} run{b.runs !== 1 ? 's' : ''}
567
+ </div>
568
+ </div>))}
569
+ </div>
570
+ </SectionCard>);
571
+ }
572
+ // ─── Quota Ledger (central quota tracking per provider × model) ─────────────
573
+ // ─── Vector Retrieval (token savings transparency) ─────────────────────────
574
+ function RetrievalSection({ retrieval }) {
575
+ // Render when there's any retrieval activity OR a non-empty repo index — the
576
+ // index may exist even before the first call (user ran `buff retrieval index`).
577
+ if (!retrieval.enabled && retrieval.repoChunks === 0)
578
+ return null;
579
+ const saved = retrieval.totalSavedTokens ?? 0;
580
+ const avgReduction = retrieval.avgPctReduced ?? 0;
581
+ const lastCall = retrieval.lastCall;
582
+ const totalCalls = retrieval.totalCalls ?? 0;
583
+ const totalRetrievals = retrieval.totalRetrievals ?? 0;
584
+ const failovers = retrieval.totalFailovers ?? 0;
585
+ return (<SectionCard icon="🧠" title="Vector Retrieval — token savings" subtitle="Large contexts are chunked, embedded locally (bge-small-en-v1.5), and reduced to the top-k relevant chunks before the LLM — saving tokens so free quotas stretch further. Complements the quota ledger: retrieval saves, the ledger manages.">
586
+ <div style={{ display: 'flex', gap: 20, flexWrap: 'wrap', marginBottom: 14 }}>
587
+ <div style={{ background: '#0d1117', border: '1px solid #238636', borderRadius: 10, padding: '12px 18px', textAlign: 'center' }}>
588
+ <div style={{ fontSize: 22, fontWeight: 700, color: '#3fb950', fontFamily: "'SFMono-Regular', Consolas, monospace" }}>
589
+ {saved.toLocaleString()}
590
+ </div>
591
+ <div style={{ fontSize: 11, color: '#8b949e', marginTop: 2 }}>tokens saved</div>
592
+ </div>
593
+ <div style={{ background: '#0d1117', border: '1px solid #21262d', borderRadius: 10, padding: '12px 18px', textAlign: 'center' }}>
594
+ <div style={{ fontSize: 22, fontWeight: 700, color: '#e6edf3', fontFamily: "'SFMono-Regular', Consolas, monospace" }}>
595
+ {avgReduction.toFixed(1)}%
596
+ </div>
597
+ <div style={{ fontSize: 11, color: '#8b949e', marginTop: 2 }}>avg context reduction</div>
598
+ </div>
599
+ <div style={{ background: '#0d1117', border: '1px solid #d29922', borderRadius: 10, padding: '12px 18px', textAlign: 'center' }}>
600
+ <div style={{ fontSize: 22, fontWeight: 700, color: '#d29922', fontFamily: "'SFMono-Regular', Consolas, monospace" }}>
601
+ {retrieval.repoChunks.toLocaleString()}
602
+ </div>
603
+ <div style={{ fontSize: 11, color: '#8b949e', marginTop: 2 }}>repo chunks indexed</div>
604
+ </div>
605
+ </div>
606
+
607
+ <div style={{ display: 'flex', gap: 20, flexWrap: 'wrap', fontSize: 12, color: '#8b949e', marginBottom: 14 }}>
608
+ <span>📞 {totalCalls} context calls · 🧠 {totalRetrievals} retrievals used · ⚠️ {failovers} failovers (full-context fallback)</span>
609
+ {retrieval.dimensions > 0 && <span>· {retrieval.dimensions}-dim embeddings</span>}
610
+ </div>
611
+
612
+ {lastCall && lastCall.used !== false && lastCall.hits?.length > 0 && (<div style={{ background: '#0d1117', border: '1px solid #21262d', borderRadius: 10, padding: '12px 16px', marginBottom: 12 }}>
613
+ <div style={{ fontSize: 12, fontWeight: 600, color: '#e6edf3', marginBottom: 8 }}>
614
+ Latest retrieval — {lastCall.originalTokens.toLocaleString()} → {lastCall.reducedTokens.toLocaleString()} tokens
615
+ <span style={{ color: '#3fb950', fontFamily: "'SFMono-Regular', Consolas, monospace" }}> (−{lastCall.pctReduced?.toFixed(0)}%)</span>
616
+ </div>
617
+ <div style={{ fontSize: 11, color: '#8b949e' }}>
618
+ {lastCall.hits.slice(0, 5).map((h) => (<div key={h.filePath} style={{ marginTop: 3 }}>
619
+ <span style={{ color: '#58a6ff' }}>▸</span> {h.filePath}{' '}
620
+ <span style={{ fontFamily: "'SFMono-Regular', Consolas, monospace" }}>(sim {h.similarity.toFixed(3)})</span>
621
+ </div>))}
622
+ </div>
623
+ </div>)}
624
+
625
+ <div style={{ fontSize: 11, color: '#6e7681' }}>
626
+ Pre-index your repo with <code style={{ color: '#58a6ff' }}>buff retrieval index &lt;dir&gt;</code>, query it with{' '}
627
+ <code style={{ color: '#58a6ff' }}>buff retrieval query "&lt;question&gt;"</code>, and see the same stats with{' '}
628
+ <code style={{ color: '#58a6ff' }}>buff retrieval stats</code>.
629
+ </div>
630
+ </SectionCard>);
631
+ }
632
+ function QuotaSection({ quota }) {
633
+ // Render the card when there are usage entries OR failover-timeline events —
634
+ // failovers can precede any successful call (auth/rate-limit on first use),
635
+ // so the timeline must be visible even on an empty ledger.
636
+ if ((!quota.enabled || quota.entries.length === 0) && (quota.events?.length ?? 0) === 0)
637
+ return null;
638
+ const totalTokens = quota.entries.reduce((s, e) => s + e.tokensConsumed, 0);
639
+ const totalRequests = quota.entries.reduce((s, e) => s + e.requests, 0);
640
+ const parkedCount = quota.entries.filter((e) => e.parked).length;
641
+ // Free/local-first transparency (assessment #7): free-tier tokens = money
642
+ // saved (would have cost on a paid provider); paid tokens = actual spend.
643
+ const freeTokens = quota.freeTokens ?? totalTokens - (quota.paidTokens ?? 0);
644
+ const paidTokens = quota.paidTokens ?? 0;
645
+ const estimatedSavedUsd = quota.estimatedSavedUsd ?? 0;
646
+ const freePct = totalTokens > 0 ? (freeTokens / totalTokens) * 100 : 0;
647
+ return (<SectionCard icon="📒" title="Quota Ledger — free-tier usage & auto re-enable" subtitle="Tokens/requests per provider × model with reset windows. Exhausted providers are parked (excluded from Auto routing) and re-enable when the window rolls.">
648
+ <div style={{ display: 'flex', gap: 20, flexWrap: 'wrap', marginBottom: 14 }}>
649
+ <div style={{ background: '#0d1117', border: '1px solid #21262d', borderRadius: 10, padding: '12px 18px', textAlign: 'center' }}>
650
+ <div style={{ fontSize: 22, fontWeight: 700, color: '#e6edf3', fontFamily: "'SFMono-Regular', Consolas, monospace" }}>
651
+ {totalTokens.toLocaleString()}
652
+ </div>
653
+ <div style={{ fontSize: 11, color: '#8b949e', marginTop: 2 }}>tokens tracked</div>
654
+ </div>
655
+ <div style={{ background: '#0d1117', border: '1px solid #21262d', borderRadius: 10, padding: '12px 18px', textAlign: 'center' }}>
656
+ <div style={{ fontSize: 22, fontWeight: 700, color: '#e6edf3', fontFamily: "'SFMono-Regular', Consolas, monospace" }}>{totalRequests}</div>
657
+ <div style={{ fontSize: 11, color: '#8b949e', marginTop: 2 }}>requests</div>
658
+ </div>
659
+ <div style={{ background: '#0d1117', border: '1px solid #21262d', borderRadius: 10, padding: '12px 18px', textAlign: 'center' }}>
660
+ <div style={{ fontSize: 22, fontWeight: 700, color: parkedCount > 0 ? '#d29922' : '#3fb950', fontFamily: "'SFMono-Regular', Consolas, monospace" }}>{parkedCount}</div>
661
+ <div style={{ fontSize: 11, color: '#8b949e', marginTop: 2 }}>parked</div>
662
+ </div>
663
+ </div>
664
+
665
+ {/* Cost transparency: free/local tokens saved vs paid spend triggered */}
666
+ <div style={{ display: 'flex', gap: 20, flexWrap: 'wrap', marginBottom: 16 }}>
667
+ <div style={{
668
+ flex: '1 1 200px', background: '#0d1117', borderRadius: 10, padding: '12px 16px',
669
+ border: '1px solid #238636',
670
+ }}>
671
+ <div style={{ fontSize: 20, fontWeight: 700, color: '#3fb950', fontFamily: "'SFMono-Regular', Consolas, monospace" }}>
672
+ {freeTokens.toLocaleString()}
673
+ </div>
674
+ <div style={{ fontSize: 11, color: '#8b949e', marginTop: 2 }}>
675
+ tokens on free/local · {freePct.toFixed(0)}% of usage
676
+ </div>
677
+ <div style={{ fontSize: 12, color: '#3fb950', marginTop: 4, fontFamily: "'SFMono-Regular', Consolas, monospace" }}>
678
+ 💰 est. ${estimatedSavedUsd.toFixed(4)} saved
679
+ </div>
680
+ </div>
681
+ <div style={{
682
+ flex: '1 1 200px', background: '#0d1117', borderRadius: 10, padding: '12px 16px',
683
+ border: '1px solid #d29922',
684
+ }}>
685
+ <div style={{ fontSize: 20, fontWeight: 700, color: paidTokens > 0 ? '#d29922' : '#8b949e', fontFamily: "'SFMono-Regular', Consolas, monospace" }}>
686
+ {paidTokens.toLocaleString()}
687
+ </div>
688
+ <div style={{ fontSize: 11, color: '#8b949e', marginTop: 2 }}>
689
+ tokens on paid providers (spend triggered)
690
+ </div>
691
+ <div style={{ fontSize: 12, color: paidTokens > 0 ? '#d29922' : '#8b949e', marginTop: 4 }}>
692
+ {quota.paidRequests ?? 0} paid request{quota.paidRequests === 1 ? '' : 's'}
693
+ </div>
694
+ </div>
695
+ </div>
696
+
697
+ <div style={{ overflowX: 'auto' }}>
698
+ <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 12 }}>
699
+ <thead>
700
+ <tr style={{ borderBottom: '1px solid #21262d', color: '#8b949e' }}>
701
+ <th style={{ padding: '8px 10px', textAlign: 'left', fontWeight: 500 }}>Provider / Model</th>
702
+ <th style={{ padding: '8px 10px', textAlign: 'right', fontWeight: 500 }}>Tokens</th>
703
+ <th style={{ padding: '8px 10px', textAlign: 'right', fontWeight: 500 }}>Requests</th>
704
+ <th style={{ padding: '8px 10px', textAlign: 'right', fontWeight: 500 }}>Resets in</th>
705
+ <th style={{ padding: '8px 10px', textAlign: 'center', fontWeight: 500 }}>State</th>
706
+ </tr>
707
+ </thead>
708
+ <tbody>
709
+ {quota.entries.map((e) => (<tr key={`${e.provider}|${e.model}`} style={{ borderBottom: '1px solid #21262d' }}>
710
+ <td style={{ padding: '8px 10px', whiteSpace: 'nowrap' }}>
711
+ <span style={{ color: '#e6edf3' }}>{providerIcon(e.provider)} {providerLabel(e.provider).split(' ')[0]}</span>
712
+ <span style={{ color: '#8b949e', fontFamily: "'SFMono-Regular', Consolas, monospace" }}>
713
+ {' / '}{e.model.length > 24 ? e.model.slice(0, 21) + '…' : e.model}
714
+ </span>
715
+ </td>
716
+ <td style={{ padding: '8px 10px', textAlign: 'right', fontFamily: "'SFMono-Regular', Consolas, monospace", color: '#8b949e' }}>
717
+ {e.tokensConsumed.toLocaleString()}
718
+ </td>
719
+ <td style={{ padding: '8px 10px', textAlign: 'right', fontFamily: "'SFMono-Regular', Consolas, monospace", color: '#8b949e' }}>
720
+ {e.requests}
721
+ </td>
722
+ <td style={{ padding: '8px 10px', textAlign: 'right', color: '#8b949e', whiteSpace: 'nowrap' }}>
723
+ {fmtDuration(e.resetsInMs)}
724
+ </td>
725
+ <td style={{ padding: '8px 10px', textAlign: 'center' }}>
726
+ {e.parked ? (<span style={{ fontSize: 11, padding: '1px 8px', borderRadius: 10, background: '#2d1616', border: '1px solid #f85149', color: '#f85149' }}>
727
+ ⏸ parked
728
+ </span>) : (<span style={{ fontSize: 11, padding: '1px 8px', borderRadius: 10, background: '#12291a', border: '1px solid #238636', color: '#3fb950' }}>
729
+ ✓ available
730
+ </span>)}
731
+ </td>
732
+ </tr>))}
733
+ </tbody>
734
+ </table>
735
+ </div>
736
+
737
+ <div style={{ fontSize: 11, color: '#6e7681', marginTop: 12 }}>
738
+ Configure limits with <code style={{ color: '#58a6ff' }}>buff config set routing.quota.gemini.requestsPerWindow 1500</code>;
739
+ the ledger always tracks usage but only parks when limits are set.
740
+ </div>
741
+
742
+ {/* M2.3/M2.4: parked multi-account keys — makes key rotation visible. */}
743
+ {quota.parkedAccounts && quota.parkedAccounts.length > 0 && (<div style={{ marginTop: 18 }}>
744
+ <div style={{ fontSize: 13, fontWeight: 600, color: '#e6edf3', marginBottom: 10 }}>
745
+ 🔑 Parked Accounts — multi-account key rotation
746
+ </div>
747
+ <div style={{ fontSize: 11, color: '#8b949e', marginBottom: 8 }}>
748
+ Accounts skipped predictively by the failover walk (rate-limit / auth). Fingerprints only — raw keys are never stored.
749
+ </div>
750
+ <div style={{ maxHeight: 180, overflowY: 'auto', background: '#0d1117', border: '1px solid #21262d', borderRadius: 10, padding: '8px 0' }}>
751
+ {quota.parkedAccounts.map((acct, i) => {
752
+ const reason = acct.reason === 'auth' ? 'auth' : acct.reason === 'rate-limit' ? 'rate-limit' : acct.reason || 'cooldown';
753
+ const color = acct.reason === 'rate-limit' ? '#d29922' : '#f85149';
754
+ return (<div key={`${acct.provider}-${acct.accountId}-${i}`} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '6px 14px', borderBottom: i < quota.parkedAccounts.length - 1 ? '1px solid #161b22' : 'none' }}>
755
+ <span style={{ fontSize: 13 }}>🔒</span>
756
+ <span style={{ width: 90, flexShrink: 0, fontSize: 12, color: '#e6edf3' }}>
757
+ {providerIcon(acct.provider)} {providerLabel(acct.provider).split(' ')[0]}
758
+ </span>
759
+ <span style={{ fontFamily: "'SFMono-Regular', Consolas, monospace", fontSize: 11, color: '#8b949e' }} title={acct.accountId}>
760
+ #{acct.accountId.slice(0, 8)}
761
+ </span>
762
+ <span style={{ fontSize: 11, padding: '1px 8px', borderRadius: 10, background: '#161b22', border: `1px solid ${color}`, color, textAlign: 'center' }}>
763
+ {reason}
764
+ </span>
765
+ <span style={{ flex: 1 }}/>
766
+ <span style={{ fontSize: 11, color: '#6e7681', whiteSpace: 'nowrap' }}>
767
+ re-admits in {fmtDuration(acct.remainingMs)}
768
+ </span>
769
+ </div>);
770
+ })}
771
+ </div>
772
+ </div>)}
773
+
774
+ {/* Failover timeline (assessment #7: show users when failover occurred and why) */}
775
+ {quota.events && quota.events.length > 0 && (<div style={{ marginTop: 18 }}>
776
+ <div style={{ fontSize: 13, fontWeight: 600, color: '#e6edf3', marginBottom: 10 }}>
777
+ 🛟 Failover Timeline — quota management & mid-session swaps
778
+ </div>
779
+ <div style={{ maxHeight: 220, overflowY: 'auto', background: '#0d1117', border: '1px solid #21262d', borderRadius: 10, padding: '8px 0' }}>
780
+ {quota.events.map((ev, i) => {
781
+ const icon = ev.type === 'parked' ? '⏸' : ev.type === 're-enabled' ? '🔁' : ev.type === 'released' ? '✅' : '⚡';
782
+ const color = ev.type === 'parked' ? '#f85149' : ev.type === 're-enabled' ? '#3fb950' : ev.type === 'released' ? '#3fb950' : '#d29922';
783
+ const label = ev.type === 'parked' ? 'parked' : ev.type === 're-enabled' ? 're-enabled' : ev.type === 'released' ? 'released' : 'failover';
784
+ return (<div key={`${ev.timestamp}-${i}`} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '6px 14px', borderBottom: i < quota.events.length - 1 ? '1px solid #161b22' : 'none' }}>
785
+ <span style={{ fontSize: 13 }}>{icon}</span>
786
+ <span style={{ width: 80, flexShrink: 0, fontSize: 11, padding: '1px 8px', borderRadius: 10, background: '#161b22', border: `1px solid ${color}`, color, textAlign: 'center' }}>
787
+ {label}
788
+ </span>
789
+ <span style={{ flex: 1, fontSize: 12, color: '#e6edf3', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
790
+ {providerIcon(ev.provider)} {providerLabel(ev.provider)}
791
+ </span>
792
+ {ev.reason && <span style={{ fontSize: 11, color: '#8b949e' }}>{ev.reason}</span>}
793
+ <span style={{ fontSize: 11, color: '#6e7681', whiteSpace: 'nowrap' }}>{timeAgo(ev.timestamp)}</span>
794
+ </div>);
795
+ })}
796
+ </div>
797
+ </div>)}
798
+ </SectionCard>);
799
+ }
800
+ // ─── Main Component ─────────────────────────────────────────────────────────
801
+ // ─── Admin Governance Policy (P6 M6.5) ─────────────────────────────────────
802
+ // The allow/deny + cap rules the Auto router enforces as HARD constraints on
803
+ // every pick — mirrored from `buff admin policy` so policy is visible exactly
804
+ // where routing decisions are made (violating providers are eliminated, never
805
+ // just scored lower).
806
+ function RuleChip({ ok, children }) {
807
+ return (<span style={{
808
+ display: 'inline-flex', alignItems: 'center', gap: 5,
809
+ fontSize: 11, padding: '3px 9px', borderRadius: 8,
810
+ background: ok === false ? '#2d1616' : '#12291a',
811
+ border: `1px solid ${ok === false ? '#f85149' : '#238636'}`,
812
+ color: ok === false ? '#f85149' : '#3fb950',
813
+ whiteSpace: 'nowrap',
814
+ }}>
815
+ {children}
816
+ </span>);
817
+ }
818
+ function GovernanceSection({ governance }) {
819
+ if (!governance.enabled) {
820
+ return (<SectionCard icon="⚖️" title="Admin Governance Policy" subtitle="Hard-constraint rules the Auto router enforces on every pick">
821
+ <div style={{ fontSize: 13, color: '#8b949e' }}>
822
+ <span style={{ color: '#3fb950' }}>Fully permissive</span> — no governance rules active; any provider × model is eligible.
823
+ <div style={{ marginTop: 6, fontSize: 12, color: '#6e7681' }}>
824
+ Add rules with <code style={{ color: '#58a6ff' }}>buff admin allow &lt;provider&gt;</code> ·{' '}
825
+ <code style={{ color: '#58a6ff' }}>buff admin deny &lt;provider&gt;</code> ·{' '}
826
+ <code style={{ color: '#58a6ff' }}>buff admin max-cost &lt;usd&gt;</code>
827
+ </div>
828
+ </div>
829
+ </SectionCard>);
830
+ }
831
+ const rules = [];
832
+ if (governance.allowProviders?.length)
833
+ rules.push({ label: 'allow providers', items: governance.allowProviders });
834
+ if (governance.denyProviders?.length)
835
+ rules.push({ label: 'deny providers', items: governance.denyProviders, deny: true });
836
+ if (governance.allowModels?.length)
837
+ rules.push({ label: 'allow models', items: governance.allowModels });
838
+ if (governance.denyModels?.length)
839
+ rules.push({ label: 'deny models', items: governance.denyModels, deny: true });
840
+ const extras = [];
841
+ if (governance.maxCostUsd !== undefined)
842
+ extras.push(`max cost $${governance.maxCostUsd}`);
843
+ if (governance.minPrivacyForPii !== undefined && governance.minPrivacyForPii < 1) {
844
+ extras.push(`PII min privacy ${governance.minPrivacyForPii}`);
845
+ }
846
+ if (governance.piiPatterns?.length)
847
+ extras.push(`PII guard (${governance.piiPatterns.length} patterns)`);
848
+ if (governance.allowUnblock === false)
849
+ extras.push('unblock admin-hard');
850
+ return (<SectionCard icon="⚖️" title="Admin Governance Policy" subtitle="Hard-constraint rules — violating providers are eliminated, never just scored lower">
851
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginBottom: 10 }}>
852
+ {rules.map((r) => (<RuleChip key={r.label} ok={!r.deny}>
853
+ {r.deny ? '⛔' : '✅'} {r.label}: {r.items.join(', ')}
854
+ </RuleChip>))}
855
+ {extras.map((e) => <RuleChip key={e}>{e}</RuleChip>)}
856
+ {rules.length === 0 && extras.length === 0 && (<RuleChip ok={false}>policy configured but no rules active</RuleChip>)}
857
+ </div>
858
+ <div style={{ fontSize: 12, color: '#6e7681' }}>
859
+ Managed via <code style={{ color: '#58a6ff' }}>buff admin</code> — the same policy the CLI&rsquo;s{' '}
860
+ <code>model explain</code> surfaces as governance-blocked.
861
+ </div>
862
+ </SectionCard>);
863
+ }
864
+ /**
865
+ * P6 M6.1 RBAC identity card — who is viewing, what role they hold, and the
866
+ * full user→role map from ~/.buff/rbac.json (mirrors `buff admin whoami` /
867
+ * `buff admin role list`). Legacy single-user mode (no roles assigned) shows
868
+ * the permissive notice; once roles exist, policy writes require admin.
869
+ */
870
+ function RbacSection({ rbac }) {
871
+ const roleColor = (role) => role === 'admin' ? '#d29922' : role === 'operator' ? '#3fb950' : '#8b949e';
872
+ return (<SectionCard icon="🔐" title="RBAC Identity" subtitle="Who may write policy — role file ~/.buff/rbac.json">
873
+ <div style={{ fontSize: 13, marginBottom: 10 }}>
874
+ <span style={{ color: '#c9d1d9' }}>You are </span>
875
+ <code style={{ color: '#58a6ff' }}>{rbac.identity}</code>
876
+ <span style={{ color: '#8b949e' }}> as </span>
877
+ {rbac.role ? (<span style={{ color: roleColor(rbac.role), fontWeight: 600 }}>{rbac.role}</span>) : (<span style={{ color: '#8b949e' }}>unassigned</span>)}
878
+ </div>
879
+ {rbac.legacy ? (<div style={{ fontSize: 12, color: '#6e7681' }}>
880
+ <span style={{ color: '#3fb950' }}>Legacy single-user mode</span> — no roles assigned; policy writes are
881
+ open to every user. Assign roles with{' '}
882
+ <code style={{ color: '#58a6ff' }}>buff admin role add &lt;you&gt; admin</code>.
883
+ </div>) : (<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
884
+ {rbac.users.map((u) => (<RuleChip key={u.user} ok={u.role !== 'viewer'}>
885
+ {u.user} · {u.role}
886
+ {u.via === 'oidc' ? ' (oidc)' : ''}
887
+ </RuleChip>))}
888
+ <div style={{ width: '100%', fontSize: 12, color: '#6e7681', marginTop: 4 }}>
889
+ Policy <em>writes</em> require <code style={{ color: '#58a6ff' }}>admin</code>; reads are open to every role.
890
+ </div>
891
+ </div>)}
892
+ </SectionCard>);
893
+ }
894
+ function RoutingInsightsPanel({ data }) {
895
+ const routing = data?.routing;
896
+ const hasAny = routing &&
897
+ (routing.preference.length > 0 || routing.providers.length > 0 || routing.bestModels.length > 0);
898
+ const hasUsage = !!routing?.usage?.total;
899
+ const hasHistory = !!routing?.history?.length;
900
+ // The walkthrough needs replayable decisions: real history or complexity profiles.
901
+ const hasWalkthrough = !!routing && (routing.history?.length ?? 0) > 0 || !!routing?.preference?.length;
902
+ const hasBandit = !!routing?.bandit?.enabled;
903
+ const hasPromotion = !!routing?.promotion?.decisionCount;
904
+ const hasMl = !!routing?.ml?.enabled && (routing.ml.recordCount ?? 0) > 0;
905
+ const hasQuota = !!routing?.quota?.enabled;
906
+ const hasRetrieval = !!routing?.retrieval?.enabled || !!routing?.retrieval?.repoChunks;
907
+ const hasGovernance = !!routing?.governance;
908
+ const hasRbac = !!routing?.rbac;
909
+ return (<>
910
+ <h2 className="section-title">🤖 Auto Routing Insights</h2>
911
+ <p className="section-description">
912
+ Which providers and models the Auto router prefers — from real pricing, benchmark
913
+ quality, per-agent success stats, and the Thompson-sampling bandit. Run{' '}
914
+ <code>buff benchmark</code> and use Auto routing to build this up over time.
915
+ </p>
916
+
917
+ {!hasAny && !hasUsage && !hasHistory && !hasBandit && !hasPromotion && !hasMl && !hasQuota && !hasRetrieval && !hasGovernance && !hasRbac ? (<EmptyNote />) : (<>
918
+ {hasWalkthrough && routing && <RoutingWalkthrough_1.default routing={routing}/>}
919
+ {hasGovernance && routing.governance && <GovernanceSection governance={routing.governance}/>}
920
+ {routing?.rbac && <RbacSection rbac={routing.rbac}/>}
921
+ {hasUsage && <UsageSection usage={routing.usage}/>}
922
+ {hasHistory && <AuditTimelineSection history={routing.history}/>}
923
+ {hasBandit && <BanditSection bandit={routing.bandit}/>}
924
+ {hasPromotion && <PromotionGateSection promotion={routing.promotion}/>}
925
+ {hasMl && routing.ml && <MlSection ml={routing.ml}/>}
926
+ {hasQuota && <QuotaSection quota={routing.quota}/>}
927
+ {hasRetrieval && <RetrievalSection retrieval={routing.retrieval}/>}
928
+ <PreferenceSection routing={routing}/>
929
+ <ProviderQualitySection routing={routing}/>
930
+ <BestModelsSection routing={routing}/>
931
+ </>)}
932
+
933
+ {routing && routing.providers.length === 0 && routing.bestModels.length === 0 && routing.preference.length > 0 && (<div style={{ fontSize: 12, color: '#6e7681', marginTop: 4 }}>
934
+ Auto-router preference is always available (static profiles + real pricing); quality
935
+ metrics appear once you run benchmarks and agent tasks.
936
+ </div>)}
937
+ </>);
938
+ }