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,229 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = RequestsPanel;
4
+ /**
5
+ * Requests Panel (Nuvira-Router P3-M3.2) — per provider × model × action
6
+ * request aggregates from the action-telemetry JSONL (the SAME file the Models
7
+ * panel reads, so both panels always agree): requests, p50/p95/p99 latency,
8
+ * error rate, measured cost, and correlation ids for traceability.
9
+ *
10
+ * Degrades cleanly: an older server that doesn't send `requests` shows the
11
+ * empty state instead of crashing (established jsonOrNull guard pattern).
12
+ */
13
+ const react_1 = require("react");
14
+ const ACTION_ICONS = {
15
+ chat: '💬', execute: '⚙️', plan: '🗺️', edit: '✏️', skill: '🧩', learn: '📚',
16
+ ci: '🔁', doctor: '🩺', probe: '🔭', 'spot-check': '🧪', telemetry: '📡',
17
+ usage: '🧮', 'ide-chat': '💬', 'ide-inline': '✍️', 'ide-execute': '⚙️',
18
+ };
19
+ function fmtTime(ts) {
20
+ return new Date(ts).toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
21
+ }
22
+ function fmtMs(v) {
23
+ return v !== undefined ? `${v}ms` : '—';
24
+ }
25
+ function errorColor(rate) {
26
+ if (rate === 0)
27
+ return '#3fb950';
28
+ if (rate < 0.2)
29
+ return '#d29922';
30
+ return '#f85149';
31
+ }
32
+ function RequestsStats({ data }) {
33
+ const rows = data.rows;
34
+ const totalRequests = rows.reduce((a, r) => a + r.requests, 0);
35
+ const totalFailures = rows.reduce((a, r) => a + Math.round(r.errorRate * r.requests), 0);
36
+ const totalCost = rows.reduce((a, r) => a + (r.costUsd ?? 0), 0);
37
+ const latencies = rows.flatMap((r) => (r.latency ? [r.latency.avg] : []));
38
+ const avgLatency = latencies.length > 0
39
+ ? Math.round(latencies.reduce((a, b) => a + b, 0) / latencies.length)
40
+ : undefined;
41
+ const actions = new Set(rows.map((r) => r.action)).size;
42
+ return (<div className="stats-grid" style={{ marginBottom: 16 }}>
43
+ <div className="stat-card">
44
+ <span className="stat-icon">📨</span>
45
+ <div className="stat-body">
46
+ <div className="stat-value">{totalRequests.toLocaleString()}</div>
47
+ <div className="stat-label">Requests</div>
48
+ </div>
49
+ </div>
50
+ <div className="stat-card">
51
+ <span className="stat-icon">🎯</span>
52
+ <div className="stat-body">
53
+ <div className="stat-value">{rows.length}</div>
54
+ <div className="stat-label">provider × model × action groups</div>
55
+ </div>
56
+ </div>
57
+ <div className="stat-card">
58
+ <span className="stat-icon">⚡</span>
59
+ <div className="stat-body">
60
+ <div className="stat-value" style={{ color: avgLatency !== undefined ? '#3fb950' : '#8b949e' }}>
61
+ {avgLatency !== undefined ? `${avgLatency}ms` : '—'}
62
+ </div>
63
+ <div className="stat-label">Avg latency{avgLatency !== undefined ? ` (${latencies.length} groups)` : ' (no samples)'}</div>
64
+ </div>
65
+ </div>
66
+ <div className="stat-card">
67
+ <span className="stat-icon">📛</span>
68
+ <div className="stat-body">
69
+ <div className="stat-value" style={{ color: errorColor(totalRequests > 0 ? totalFailures / totalRequests : 0) }}>
70
+ {totalRequests > 0 ? `${((totalFailures / totalRequests) * 100).toFixed(1)}%` : '—'}
71
+ </div>
72
+ <div className="stat-label">Overall error rate ({totalFailures.toLocaleString()} failures)</div>
73
+ </div>
74
+ </div>
75
+ <div className="stat-card">
76
+ <span className="stat-icon">💰</span>
77
+ <div className="stat-body">
78
+ <div className="stat-value" style={{ color: totalCost > 0 ? '#d29922' : '#8b949e' }}>
79
+ {totalCost > 0 ? `$${totalCost.toFixed(4)}` : '—'}
80
+ </div>
81
+ <div className="stat-label">Measured cost{actions > 0 ? ` · ${actions} actions` : ''}</div>
82
+ </div>
83
+ </div>
84
+ </div>);
85
+ }
86
+ function RequestsPanel({ data }) {
87
+ const [query, setQuery] = (0, react_1.useState)('');
88
+ const [actionFilter, setActionFilter] = (0, react_1.useState)('all');
89
+ const requests = data?.requests;
90
+ const rows = (0, react_1.useMemo)(() => {
91
+ if (!requests)
92
+ return [];
93
+ const q = query.trim().toLowerCase();
94
+ return requests.rows.filter((r) => {
95
+ if (actionFilter !== 'all' && r.action !== actionFilter)
96
+ return false;
97
+ if (!q)
98
+ return true;
99
+ return `${r.provider} ${r.model} ${r.action}`.toLowerCase().includes(q);
100
+ });
101
+ }, [requests, query, actionFilter]);
102
+ if (!data) {
103
+ return <div className="loading-state"><p>Loading requests...</p></div>;
104
+ }
105
+ if (!requests || !requests.enabled || requests.rows.length === 0) {
106
+ return (<div className="empty-state" style={{ padding: '40px 24px', textAlign: 'center' }}>
107
+ <div style={{ fontSize: 28, marginBottom: 10 }}>📨</div>
108
+ <h2 className="section-title" style={{ marginTop: 0 }}>Requests — per provider × model × action</h2>
109
+ <p className="section-description" style={{ maxWidth: 560, margin: '0 auto' }}>
110
+ No request telemetry yet. As you use <strong>chat</strong>, <strong>execute</strong>,{' '}
111
+ <strong>plan</strong> and <strong>edit</strong> under Auto routing, every call writes to the
112
+ action-telemetry log and this panel aggregates it: request counts, p50/p95/p99 latency, error
113
+ rate and measured cost — the same feed that drives the Models panel.
114
+ </p>
115
+ </div>);
116
+ }
117
+ const actions = [...new Set(requests.rows.map((r) => r.action))].sort();
118
+ return (<>
119
+ <h2 className="section-title">📨 Requests — per provider × model × action</h2>
120
+ <p className="section-description">
121
+ Aggregated from the same action-telemetry log as the Models panel — every Auto-routed call,
122
+ grouped by action × provider × model. Latency percentiles appear once ≥3 samples exist; cost
123
+ when the caller reported usage.
124
+ </p>
125
+
126
+ <RequestsStats data={requests}/>
127
+
128
+ <div style={{ display: 'flex', gap: 12, marginBottom: 14, flexWrap: 'wrap', alignItems: 'center' }}>
129
+ <select value={actionFilter} onChange={(e) => setActionFilter(e.target.value)} aria-label="Filter by action" style={{
130
+ background: '#161b22', color: '#e6edf3', border: '1px solid #30363d',
131
+ borderRadius: 8, padding: '8px 10px', fontSize: 13, cursor: 'pointer',
132
+ }}>
133
+ <option value="all">All actions</option>
134
+ {actions.map((a) => (<option key={a} value={a}>{ACTION_ICONS[a] || '🎯'} {a}</option>))}
135
+ </select>
136
+ <div style={{ flex: 1, minWidth: 220, position: 'relative' }}>
137
+ <span style={{ position: 'absolute', left: 10, top: 8, color: '#6e7681', fontSize: 13 }}>🔍</span>
138
+ <input type="text" value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Search provider, model or action..." style={{
139
+ width: '100%', padding: '8px 12px 8px 32px', background: '#161b22',
140
+ border: '1px solid #30363d', borderRadius: 8, color: '#e6edf3',
141
+ fontSize: 13, outline: 'none',
142
+ }}/>
143
+ </div>
144
+ <span style={{ fontSize: 12, color: '#8b949e', whiteSpace: 'nowrap' }}>
145
+ {rows.length} of {requests.rows.length} groups
146
+ </span>
147
+ </div>
148
+
149
+ <div style={{ overflowX: 'auto' }}>
150
+ <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13 }}>
151
+ <thead>
152
+ <tr style={{ borderBottom: '1px solid #21262d', color: '#8b949e', textAlign: 'left' }}>
153
+ <th style={{ padding: '8px 12px', fontWeight: 500 }}>Action</th>
154
+ <th style={{ padding: '8px 12px', fontWeight: 500 }}>Provider</th>
155
+ <th style={{ padding: '8px 12px', fontWeight: 500 }}>Model</th>
156
+ <th style={{ padding: '8px 12px', fontWeight: 500, textAlign: 'right' }}>Requests</th>
157
+ <th style={{ padding: '8px 12px', fontWeight: 500, textAlign: 'right' }}>Error rate</th>
158
+ <th style={{ padding: '8px 12px', fontWeight: 500, textAlign: 'right' }}>Latency avg</th>
159
+ <th style={{ padding: '8px 12px', fontWeight: 500, textAlign: 'right' }}>p50</th>
160
+ <th style={{ padding: '8px 12px', fontWeight: 500, textAlign: 'right' }}>p95</th>
161
+ <th style={{ padding: '8px 12px', fontWeight: 500, textAlign: 'right' }}>p99</th>
162
+ <th style={{ padding: '8px 12px', fontWeight: 500, textAlign: 'right' }}>Measured cost</th>
163
+ <th style={{ padding: '8px 12px', fontWeight: 500, textAlign: 'right' }}>Last seen</th>
164
+ </tr>
165
+ </thead>
166
+ <tbody>
167
+ {rows.length === 0 ? (<tr>
168
+ <td colSpan={11} style={{ padding: 28, textAlign: 'center', color: '#6e7681' }}>
169
+ No request groups match your filter.
170
+ </td>
171
+ </tr>) : (rows.map((r) => (<tr key={`${r.action}|${r.provider}|${r.model}`} style={{ borderBottom: '1px solid #21262d' }}>
172
+ <td style={{ padding: '8px 12px', whiteSpace: 'nowrap' }}>
173
+ <span style={{
174
+ fontSize: 11, padding: '2px 8px', borderRadius: 8,
175
+ background: '#21262d', color: '#c9d1d9', border: '1px solid #30363d',
176
+ }}>
177
+ {ACTION_ICONS[r.action] || '🎯'} {r.action}
178
+ </span>
179
+ </td>
180
+ <td style={{ padding: '8px 12px', color: '#e6edf3', whiteSpace: 'nowrap' }}>{r.provider}</td>
181
+ <td style={{
182
+ padding: '8px 12px', color: '#8b949e', fontFamily: "'SFMono-Regular', Consolas, monospace",
183
+ fontSize: 12, maxWidth: 260, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
184
+ }} title={r.callIds.length > 0 ? `Correlation ids: ${r.callIds.join(', ')}` : undefined}>
185
+ {r.model.length > 42 ? r.model.slice(0, 39) + '…' : r.model}
186
+ {r.callIds.length > 0 && (<span style={{ color: '#6e7681', marginLeft: 6 }} title={r.callIds.join(', ')}>🔗{r.callIds.length}</span>)}
187
+ </td>
188
+ <td style={{ padding: '8px 12px', textAlign: 'right', fontFamily: "'SFMono-Regular', Consolas, monospace", fontSize: 12 }}>{r.requests}</td>
189
+ <td style={{
190
+ padding: '8px 12px', textAlign: 'right', fontFamily: "'SFMono-Regular', Consolas, monospace",
191
+ fontSize: 12, color: errorColor(r.errorRate),
192
+ }}>
193
+ {(r.errorRate * 100).toFixed(1)}%
194
+ {(r.partials ?? 0) > 0 && (<span title={`⏸ ${r.partials} mid-stream interruption(s) — started streaming, died before finish; the router deprioritizes flaky providers (P4 M4.4)`} style={{
195
+ display: 'inline-flex', alignItems: 'center', gap: 3,
196
+ marginLeft: 6, fontSize: 10, padding: '1px 6px', borderRadius: 8, whiteSpace: 'nowrap',
197
+ background: '#21122e', border: '1px solid #bc8cff', color: '#bc8cff',
198
+ }}>
199
+ ⏸ {r.partials}
200
+ </span>)}
201
+ </td>
202
+ <td style={{ padding: '8px 12px', textAlign: 'right', fontFamily: "'SFMono-Regular', Consolas, monospace", fontSize: 12 }}>
203
+ {fmtMs(r.latency?.avg)}
204
+ </td>
205
+ <td style={{ padding: '8px 12px', textAlign: 'right', fontFamily: "'SFMono-Regular', Consolas, monospace", fontSize: 12 }}>
206
+ {r.latency && r.latency.samples >= 3 ? fmtMs(r.latency.p50) : '—'}
207
+ </td>
208
+ <td style={{ padding: '8px 12px', textAlign: 'right', fontFamily: "'SFMono-Regular', Consolas, monospace", fontSize: 12 }}>
209
+ {r.latency && r.latency.samples >= 3 ? fmtMs(r.latency.p95) : '—'}
210
+ </td>
211
+ <td style={{ padding: '8px 12px', textAlign: 'right', fontFamily: "'SFMono-Regular', Consolas, monospace", fontSize: 12 }}>
212
+ {r.latency && r.latency.samples >= 3 ? fmtMs(r.latency.p99) : '—'}
213
+ </td>
214
+ <td style={{ padding: '8px 12px', textAlign: 'right', fontFamily: "'SFMono-Regular', Consolas, monospace", fontSize: 12, color: r.costUsd ? '#d29922' : '#6e7681' }}>
215
+ {r.costUsd !== undefined ? `$${r.costUsd.toFixed(4)}` : '—'}
216
+ </td>
217
+ <td style={{ padding: '8px 12px', textAlign: 'right', color: '#6e7681', fontSize: 11, whiteSpace: 'nowrap' }}>
218
+ {fmtTime(r.lastAt)}
219
+ </td>
220
+ </tr>)))}
221
+ </tbody>
222
+ </table>
223
+ </div>
224
+
225
+ <div style={{ textAlign: 'center', fontSize: 12, color: '#484f58', marginTop: 12 }}>
226
+ Auto-refreshes with the dashboard feed · percentile columns need ≥3 latency samples per group
227
+ </div>
228
+ </>);
229
+ }
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+ /**
3
+ * Unit tests for the Requests Panel (Nuvira-Router P3-M3.2).
4
+ * Covers: empty state (no data / older server), stats cards, row rendering,
5
+ * the ≥3-samples latency percentile gate, and the action/search filters.
6
+ */
7
+ var __importDefault = (this && this.__importDefault) || function (mod) {
8
+ return (mod && mod.__esModule) ? mod : { "default": mod };
9
+ };
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ const vitest_1 = require("vitest");
12
+ const react_1 = require("@testing-library/react");
13
+ const RequestsPanel_1 = __importDefault(require("./RequestsPanel"));
14
+ const makeRequests = (overrides = {}) => ({
15
+ enabled: true,
16
+ total: 3,
17
+ rows: [
18
+ {
19
+ provider: 'groq', model: 'llama-3.3-70b-versatile', action: 'chat',
20
+ requests: 10, errorRate: 0, partials: 3, costUsd: 0.00012, costCalls: 10,
21
+ latency: { avg: 420, samples: 5, p50: 400, p95: 620, p99: 700 },
22
+ callIds: ['call-1', 'call-2'], lastAt: 1750000000000,
23
+ },
24
+ {
25
+ provider: 'gemini', model: 'gemini-2.0-flash', action: 'execute',
26
+ requests: 4, errorRate: 0.25, partials: 0, costUsd: 0.0008, costCalls: 4,
27
+ latency: { avg: 900, samples: 2 }, // < 3 samples → percentiles hidden
28
+ callIds: [], lastAt: 1749990000000,
29
+ },
30
+ {
31
+ provider: 'nim', model: 'llama-3.1-8b-instruct', action: 'plan',
32
+ requests: 2, errorRate: 0.5, partials: 0, costCalls: 0,
33
+ latency: undefined, callIds: [], lastAt: 1749980000000,
34
+ },
35
+ ],
36
+ updatedAt: Date.now(),
37
+ ...overrides,
38
+ });
39
+ const makeData = (requests) => ({
40
+ cost: { totalRequests: 0, totalCost: 0, totalTokens: 0, byProvider: {}, byModel: {}, byProviderMeasured: {}, measuredCalls: 0, estimatedCalls: 0, measuredCost: 0, estimatedCost: 0, recent: [] },
41
+ history: { total: 0, recent: [] },
42
+ benchmarks: { totalRuns: 0, latest: null, runs: [] },
43
+ memory: { total: 0, avgScore: 0, byFingerprint: {} },
44
+ health: { patterns: 0, feedback: 0, vectors: 0, agentStats: null, memoryDir: '' },
45
+ requests,
46
+ serverTime: Date.now(),
47
+ });
48
+ (0, vitest_1.describe)('RequestsPanel', () => {
49
+ (0, vitest_1.afterEach)(() => {
50
+ (0, react_1.cleanup)();
51
+ vitest_1.vi.restoreAllMocks();
52
+ });
53
+ (0, vitest_1.it)('shows the empty state when the server sends no requests data (older server)', () => {
54
+ (0, react_1.render)(<RequestsPanel_1.default data={makeData(undefined)}/>);
55
+ (0, vitest_1.expect)(react_1.screen.getByText(/No request telemetry yet/i)).toBeTruthy();
56
+ });
57
+ (0, vitest_1.it)('renders stats cards and per provider × model × action rows', () => {
58
+ (0, react_1.render)(<RequestsPanel_1.default data={makeData(makeRequests())}/>);
59
+ // Stats: 10 + 4 + 2 = 16 requests.
60
+ (0, vitest_1.expect)(react_1.screen.getByText('16')).toBeTruthy();
61
+ (0, vitest_1.expect)(react_1.screen.getByText('3')).toBeTruthy(); // groups
62
+ // Rows (action chips render as "💬 chat" — match on the substring; the
63
+ // action <select> also lists the action names, so assert multiplicity).
64
+ (0, vitest_1.expect)(react_1.screen.getByText('groq')).toBeTruthy();
65
+ (0, vitest_1.expect)(react_1.screen.getByText('gemini')).toBeTruthy();
66
+ (0, vitest_1.expect)(react_1.screen.getByText('nim')).toBeTruthy();
67
+ (0, vitest_1.expect)(react_1.screen.getAllByText((c) => c.includes('chat')).length).toBeGreaterThan(0);
68
+ (0, vitest_1.expect)(react_1.screen.getAllByText((c) => c.includes('execute')).length).toBeGreaterThan(0);
69
+ (0, vitest_1.expect)(react_1.screen.getAllByText((c) => c.includes('plan')).length).toBeGreaterThan(0);
70
+ });
71
+ (0, vitest_1.it)('shows percentile columns only when ≥3 latency samples exist, else —', () => {
72
+ (0, react_1.render)(<RequestsPanel_1.default data={makeData(makeRequests())}/>);
73
+ // groq row has 5 samples → p95 = 620ms shown.
74
+ (0, vitest_1.expect)(react_1.screen.getByText('620ms')).toBeTruthy();
75
+ // gemini row has 2 samples → its p50 is a dash.
76
+ (0, vitest_1.expect)(react_1.screen.getAllByText('—').length).toBeGreaterThan(0);
77
+ });
78
+ (0, vitest_1.it)('shows a violet ⏸ partial chip on rows with mid-stream interruptions (P4 M4.4)', () => {
79
+ (0, react_1.render)(<RequestsPanel_1.default data={makeData(makeRequests())}/>);
80
+ // groq row has 3 partials → the ⏸ 3 chip renders with the flaky tooltip.
81
+ (0, vitest_1.expect)(react_1.screen.getByText(/⏸ 3/)).toBeTruthy();
82
+ (0, vitest_1.expect)(react_1.screen.getByTitle(/mid-stream interruption\(s\)/)).toBeTruthy();
83
+ (0, vitest_1.expect)(react_1.screen.getByTitle(/deprioritizes flaky providers/)).toBeTruthy();
84
+ });
85
+ (0, vitest_1.it)('filters by action via the select', () => {
86
+ (0, react_1.render)(<RequestsPanel_1.default data={makeData(makeRequests())}/>);
87
+ react_1.fireEvent.change(react_1.screen.getByLabelText('Filter by action'), { target: { value: 'execute' } });
88
+ (0, vitest_1.expect)(react_1.screen.queryByText('groq')).toBeNull();
89
+ (0, vitest_1.expect)(react_1.screen.getByText('gemini')).toBeTruthy();
90
+ });
91
+ (0, vitest_1.it)('filters by search query across provider/model/action', () => {
92
+ (0, react_1.render)(<RequestsPanel_1.default data={makeData(makeRequests())}/>);
93
+ react_1.fireEvent.change(react_1.screen.getByPlaceholderText('Search provider, model or action...'), { target: { value: 'gemini' } });
94
+ (0, vitest_1.expect)(react_1.screen.getByText('gemini')).toBeTruthy();
95
+ (0, vitest_1.expect)(react_1.screen.queryByText('groq')).toBeNull();
96
+ });
97
+ (0, vitest_1.it)('renders empty-state text when no rows match the filter', () => {
98
+ (0, react_1.render)(<RequestsPanel_1.default data={makeData(makeRequests())}/>);
99
+ react_1.fireEvent.change(react_1.screen.getByLabelText('Filter by action'), { target: { value: 'chat' } });
100
+ react_1.fireEvent.change(react_1.screen.getByPlaceholderText('Search provider, model or action...'), { target: { value: 'nim' } });
101
+ (0, vitest_1.expect)(react_1.screen.getByText(/No request groups match your filter/i)).toBeTruthy();
102
+ });
103
+ });