@katyella/legio 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +422 -0
- package/LICENSE +21 -0
- package/README.md +555 -0
- package/agents/builder.md +141 -0
- package/agents/coordinator.md +351 -0
- package/agents/cto.md +196 -0
- package/agents/gateway.md +276 -0
- package/agents/lead.md +281 -0
- package/agents/merger.md +156 -0
- package/agents/monitor.md +212 -0
- package/agents/reviewer.md +142 -0
- package/agents/scout.md +131 -0
- package/agents/supervisor.md +416 -0
- package/bin/legio.mjs +38 -0
- package/package.json +77 -0
- package/src/agents/checkpoint.test.ts +88 -0
- package/src/agents/checkpoint.ts +102 -0
- package/src/agents/hooks-deployer.test.ts +1820 -0
- package/src/agents/hooks-deployer.ts +574 -0
- package/src/agents/identity.test.ts +614 -0
- package/src/agents/identity.ts +385 -0
- package/src/agents/lifecycle.test.ts +202 -0
- package/src/agents/lifecycle.ts +184 -0
- package/src/agents/manifest.test.ts +558 -0
- package/src/agents/manifest.ts +297 -0
- package/src/agents/overlay.test.ts +592 -0
- package/src/agents/overlay.ts +316 -0
- package/src/beads/client.test.ts +210 -0
- package/src/beads/client.ts +227 -0
- package/src/beads/molecules.test.ts +320 -0
- package/src/beads/molecules.ts +209 -0
- package/src/commands/agents.test.ts +325 -0
- package/src/commands/agents.ts +286 -0
- package/src/commands/clean.test.ts +730 -0
- package/src/commands/clean.ts +653 -0
- package/src/commands/completions.test.ts +346 -0
- package/src/commands/completions.ts +950 -0
- package/src/commands/coordinator.test.ts +1524 -0
- package/src/commands/coordinator.ts +880 -0
- package/src/commands/costs.test.ts +1015 -0
- package/src/commands/costs.ts +473 -0
- package/src/commands/dashboard.test.ts +94 -0
- package/src/commands/dashboard.ts +607 -0
- package/src/commands/doctor.test.ts +295 -0
- package/src/commands/doctor.ts +213 -0
- package/src/commands/down.test.ts +308 -0
- package/src/commands/down.ts +124 -0
- package/src/commands/errors.test.ts +648 -0
- package/src/commands/errors.ts +255 -0
- package/src/commands/feed.test.ts +579 -0
- package/src/commands/feed.ts +368 -0
- package/src/commands/gateway.test.ts +698 -0
- package/src/commands/gateway.ts +419 -0
- package/src/commands/group.test.ts +262 -0
- package/src/commands/group.ts +539 -0
- package/src/commands/hooks.test.ts +292 -0
- package/src/commands/hooks.ts +210 -0
- package/src/commands/init.test.ts +211 -0
- package/src/commands/init.ts +622 -0
- package/src/commands/inspect.test.ts +670 -0
- package/src/commands/inspect.ts +455 -0
- package/src/commands/log.test.ts +1556 -0
- package/src/commands/log.ts +752 -0
- package/src/commands/logs.test.ts +379 -0
- package/src/commands/logs.ts +544 -0
- package/src/commands/mail.test.ts +1726 -0
- package/src/commands/mail.ts +926 -0
- package/src/commands/merge.test.ts +676 -0
- package/src/commands/merge.ts +374 -0
- package/src/commands/metrics.test.ts +444 -0
- package/src/commands/metrics.ts +150 -0
- package/src/commands/monitor.test.ts +151 -0
- package/src/commands/monitor.ts +394 -0
- package/src/commands/nudge.test.ts +230 -0
- package/src/commands/nudge.ts +373 -0
- package/src/commands/prime.test.ts +467 -0
- package/src/commands/prime.ts +386 -0
- package/src/commands/replay.test.ts +742 -0
- package/src/commands/replay.ts +367 -0
- package/src/commands/run.test.ts +443 -0
- package/src/commands/run.ts +365 -0
- package/src/commands/server.test.ts +626 -0
- package/src/commands/server.ts +298 -0
- package/src/commands/sling.test.ts +810 -0
- package/src/commands/sling.ts +700 -0
- package/src/commands/spec.test.ts +206 -0
- package/src/commands/spec.ts +171 -0
- package/src/commands/status.test.ts +276 -0
- package/src/commands/status.ts +339 -0
- package/src/commands/stop.test.ts +357 -0
- package/src/commands/stop.ts +119 -0
- package/src/commands/supervisor.test.ts +186 -0
- package/src/commands/supervisor.ts +544 -0
- package/src/commands/trace.test.ts +746 -0
- package/src/commands/trace.ts +332 -0
- package/src/commands/up.test.ts +597 -0
- package/src/commands/up.ts +275 -0
- package/src/commands/watch.test.ts +152 -0
- package/src/commands/watch.ts +238 -0
- package/src/commands/worktree.test.ts +648 -0
- package/src/commands/worktree.ts +266 -0
- package/src/config.test.ts +496 -0
- package/src/config.ts +616 -0
- package/src/doctor/agents.test.ts +448 -0
- package/src/doctor/agents.ts +396 -0
- package/src/doctor/config-check.test.ts +184 -0
- package/src/doctor/config-check.ts +185 -0
- package/src/doctor/consistency.test.ts +645 -0
- package/src/doctor/consistency.ts +294 -0
- package/src/doctor/databases.test.ts +284 -0
- package/src/doctor/databases.ts +211 -0
- package/src/doctor/dependencies.test.ts +150 -0
- package/src/doctor/dependencies.ts +179 -0
- package/src/doctor/logs.test.ts +244 -0
- package/src/doctor/logs.ts +295 -0
- package/src/doctor/merge-queue.test.ts +210 -0
- package/src/doctor/merge-queue.ts +144 -0
- package/src/doctor/structure.test.ts +285 -0
- package/src/doctor/structure.ts +195 -0
- package/src/doctor/types.ts +37 -0
- package/src/doctor/version.test.ts +130 -0
- package/src/doctor/version.ts +131 -0
- package/src/e2e/chat-flow.test.ts +346 -0
- package/src/e2e/init-sling-lifecycle.test.ts +288 -0
- package/src/errors.test.ts +21 -0
- package/src/errors.ts +246 -0
- package/src/events/store.test.ts +660 -0
- package/src/events/store.ts +344 -0
- package/src/events/tool-filter.test.ts +330 -0
- package/src/events/tool-filter.ts +126 -0
- package/src/global-setup.ts +14 -0
- package/src/index.ts +339 -0
- package/src/insights/analyzer.test.ts +466 -0
- package/src/insights/analyzer.ts +203 -0
- package/src/logging/color.test.ts +118 -0
- package/src/logging/color.ts +71 -0
- package/src/logging/logger.test.ts +812 -0
- package/src/logging/logger.ts +266 -0
- package/src/logging/reporter.test.ts +258 -0
- package/src/logging/reporter.ts +109 -0
- package/src/logging/sanitizer.test.ts +190 -0
- package/src/logging/sanitizer.ts +57 -0
- package/src/mail/broadcast.test.ts +203 -0
- package/src/mail/broadcast.ts +92 -0
- package/src/mail/client.test.ts +873 -0
- package/src/mail/client.ts +236 -0
- package/src/mail/store.test.ts +815 -0
- package/src/mail/store.ts +402 -0
- package/src/merge/queue.test.ts +449 -0
- package/src/merge/queue.ts +262 -0
- package/src/merge/resolver.test.ts +1453 -0
- package/src/merge/resolver.ts +759 -0
- package/src/metrics/store.test.ts +1167 -0
- package/src/metrics/store.ts +511 -0
- package/src/metrics/summary.test.ts +397 -0
- package/src/metrics/summary.ts +178 -0
- package/src/metrics/transcript.test.ts +643 -0
- package/src/metrics/transcript.ts +351 -0
- package/src/mulch/client.test.ts +547 -0
- package/src/mulch/client.ts +416 -0
- package/src/server/audit-store.test.ts +384 -0
- package/src/server/audit-store.ts +257 -0
- package/src/server/headless.test.ts +180 -0
- package/src/server/headless.ts +151 -0
- package/src/server/index.test.ts +241 -0
- package/src/server/index.ts +317 -0
- package/src/server/public/app.js +187 -0
- package/src/server/public/apple-touch-icon.png +0 -0
- package/src/server/public/components/agent-badge.js +37 -0
- package/src/server/public/components/data-table.js +114 -0
- package/src/server/public/components/gateway-chat.js +256 -0
- package/src/server/public/components/issue-card.js +96 -0
- package/src/server/public/components/layout.js +88 -0
- package/src/server/public/components/message-bubble.js +120 -0
- package/src/server/public/components/stat-card.js +26 -0
- package/src/server/public/components/terminal-panel.js +140 -0
- package/src/server/public/favicon-16.png +0 -0
- package/src/server/public/favicon-32.png +0 -0
- package/src/server/public/favicon.ico +0 -0
- package/src/server/public/favicon.png +0 -0
- package/src/server/public/index.html +64 -0
- package/src/server/public/lib/api.js +35 -0
- package/src/server/public/lib/markdown.js +8 -0
- package/src/server/public/lib/preact-setup.js +8 -0
- package/src/server/public/lib/state.js +99 -0
- package/src/server/public/lib/utils.js +309 -0
- package/src/server/public/lib/ws.js +79 -0
- package/src/server/public/views/chat.js +983 -0
- package/src/server/public/views/costs.js +692 -0
- package/src/server/public/views/dashboard.js +781 -0
- package/src/server/public/views/gateway-chat.js +622 -0
- package/src/server/public/views/inspect.js +399 -0
- package/src/server/public/views/issues.js +470 -0
- package/src/server/public/views/setup.js +94 -0
- package/src/server/public/views/task-detail.js +422 -0
- package/src/server/routes.test.ts +3816 -0
- package/src/server/routes.ts +1964 -0
- package/src/server/websocket.test.ts +288 -0
- package/src/server/websocket.ts +196 -0
- package/src/sessions/compat.test.ts +109 -0
- package/src/sessions/compat.ts +17 -0
- package/src/sessions/store.test.ts +969 -0
- package/src/sessions/store.ts +480 -0
- package/src/test-helpers.test.ts +97 -0
- package/src/test-helpers.ts +143 -0
- package/src/types.ts +708 -0
- package/src/watchdog/daemon.test.ts +1233 -0
- package/src/watchdog/daemon.ts +533 -0
- package/src/watchdog/health.test.ts +371 -0
- package/src/watchdog/health.ts +248 -0
- package/src/watchdog/triage.test.ts +162 -0
- package/src/watchdog/triage.ts +193 -0
- package/src/worktree/manager.test.ts +444 -0
- package/src/worktree/manager.ts +224 -0
- package/src/worktree/tmux.test.ts +1238 -0
- package/src/worktree/tmux.ts +644 -0
- package/templates/CLAUDE.md.tmpl +89 -0
- package/templates/hooks.json.tmpl +132 -0
- package/templates/overlay.md.tmpl +79 -0
|
@@ -0,0 +1,692 @@
|
|
|
1
|
+
// CostsView — Usage report with charts, tables, and live snapshots
|
|
2
|
+
// Preact+HTM component, no build step required.
|
|
3
|
+
|
|
4
|
+
import { fetchJson } from "../lib/api.js";
|
|
5
|
+
import { html, useCallback, useEffect, useMemo, useState } from "../lib/preact-setup.js";
|
|
6
|
+
import { timeAgo } from "../lib/utils.js";
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Constants
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
const TIME_WINDOWS = [
|
|
13
|
+
{ label: "All Time", value: null },
|
|
14
|
+
{ label: "Last Hour", value: 60 * 60 * 1000 },
|
|
15
|
+
{ label: "Last 24h", value: 24 * 60 * 60 * 1000 },
|
|
16
|
+
{ label: "Last 7d", value: 7 * 24 * 60 * 60 * 1000 },
|
|
17
|
+
{ label: "Last 30d", value: 30 * 24 * 60 * 60 * 1000 },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const MODEL_COLORS = {
|
|
21
|
+
opus: "bg-blue-500",
|
|
22
|
+
sonnet: "bg-green-500",
|
|
23
|
+
haiku: "bg-yellow-500",
|
|
24
|
+
unknown: "bg-gray-500",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Helpers
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
function formatNumber(n) {
|
|
32
|
+
if (n == null) return "—";
|
|
33
|
+
return Number(n).toLocaleString();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function formatCost(n) {
|
|
37
|
+
if (n == null) return "—";
|
|
38
|
+
return `$${Number(n).toFixed(4)}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function formatCostShort(n) {
|
|
42
|
+
if (n == null) return "—";
|
|
43
|
+
return `$${Number(n).toFixed(2)}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function formatDateShort(dateStr) {
|
|
47
|
+
if (!dateStr) return "";
|
|
48
|
+
const d = String(dateStr).split("T")[0]; // "2026-02-21"
|
|
49
|
+
const parts = d.split("-");
|
|
50
|
+
if (parts.length < 3) return d;
|
|
51
|
+
return `${parts[1]}/${parts[2]}`; // "02/21"
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function modelColor(model) {
|
|
55
|
+
if (!model) return MODEL_COLORS.unknown;
|
|
56
|
+
const lower = String(model).toLowerCase();
|
|
57
|
+
if (lower.includes("opus")) return MODEL_COLORS.opus;
|
|
58
|
+
if (lower.includes("sonnet")) return MODEL_COLORS.sonnet;
|
|
59
|
+
if (lower.includes("haiku")) return MODEL_COLORS.haiku;
|
|
60
|
+
return MODEL_COLORS.unknown;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// StatCard
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
function StatCard({ label, value }) {
|
|
68
|
+
return html`
|
|
69
|
+
<div class="bg-surface border border-border rounded-sm p-4 flex-1 min-w-0">
|
|
70
|
+
<div class="text-xs text-gray-500 uppercase tracking-wider mb-1">${label}</div>
|
|
71
|
+
<div class="text-2xl font-mono text-white">${value}</div>
|
|
72
|
+
</div>
|
|
73
|
+
`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// ModelBreakdown
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
function ModelBreakdown({ modelData }) {
|
|
81
|
+
if (!modelData || modelData.length === 0) {
|
|
82
|
+
return html`<div class="text-center text-gray-500 py-8">No model data</div>`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const sorted = [...modelData].sort(
|
|
86
|
+
(a, b) => (b.estimatedCostUsd || 0) - (a.estimatedCostUsd || 0),
|
|
87
|
+
);
|
|
88
|
+
const maxCost = sorted[0]?.estimatedCostUsd ?? 0;
|
|
89
|
+
|
|
90
|
+
return html`
|
|
91
|
+
<div class="flex flex-col gap-3">
|
|
92
|
+
${sorted.map((row) => {
|
|
93
|
+
const ioTokens = (row.inputTokens || 0) + (row.outputTokens || 0);
|
|
94
|
+
const pct = maxCost > 0 ? ((row.estimatedCostUsd || 0) / maxCost) * 100 : 0;
|
|
95
|
+
const color = modelColor(row.model);
|
|
96
|
+
return html`
|
|
97
|
+
<div key=${row.model || "unknown"} class="flex items-center gap-3">
|
|
98
|
+
<div class="flex items-center gap-2 w-[140px] shrink-0">
|
|
99
|
+
<div class=${`${color} w-2 h-2 rounded-full shrink-0`}></div>
|
|
100
|
+
<span class="text-sm text-gray-300 truncate">${row.model || "unknown"}</span>
|
|
101
|
+
</div>
|
|
102
|
+
<div class="flex-1 bg-white/5 rounded-sm h-5 overflow-hidden">
|
|
103
|
+
<div
|
|
104
|
+
class=${`${color} h-full rounded-sm`}
|
|
105
|
+
style=${`width: ${pct.toFixed(1)}%`}
|
|
106
|
+
></div>
|
|
107
|
+
</div>
|
|
108
|
+
<span class="font-mono text-xs text-gray-400 w-28 text-right shrink-0">
|
|
109
|
+
${formatNumber(ioTokens)} tok
|
|
110
|
+
</span>
|
|
111
|
+
<span class="font-mono text-sm text-gray-300 w-20 text-right shrink-0">
|
|
112
|
+
${formatCost(row.estimatedCostUsd)}
|
|
113
|
+
</span>
|
|
114
|
+
</div>
|
|
115
|
+
`;
|
|
116
|
+
})}
|
|
117
|
+
<!-- Legend details -->
|
|
118
|
+
<div class="flex flex-wrap gap-x-6 gap-y-1 mt-1">
|
|
119
|
+
${sorted.map(
|
|
120
|
+
(row) => html`
|
|
121
|
+
<div key=${row.model} class="text-xs text-gray-500">
|
|
122
|
+
${row.model || "unknown"}: ${row.sessions ?? 0} session${row.sessions === 1 ? "" : "s"}
|
|
123
|
+
</div>
|
|
124
|
+
`,
|
|
125
|
+
)}
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// DateChart
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
function DateChart({ dateData }) {
|
|
136
|
+
if (!dateData || dateData.length === 0) {
|
|
137
|
+
return html`<div class="text-center text-gray-500 py-8">No date data</div>`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const maxCost = Math.max(...dateData.map((d) => d.estimatedCostUsd || 0));
|
|
141
|
+
const maxHeight = 160; // px
|
|
142
|
+
|
|
143
|
+
return html`
|
|
144
|
+
<div class="overflow-x-auto">
|
|
145
|
+
<div class="flex items-end gap-1 min-w-0" style="min-height: ${maxHeight + 32}px">
|
|
146
|
+
${dateData.map((d) => {
|
|
147
|
+
const cost = d.estimatedCostUsd || 0;
|
|
148
|
+
const barH = maxCost > 0 ? Math.max(2, Math.round((cost / maxCost) * maxHeight)) : 2;
|
|
149
|
+
return html`
|
|
150
|
+
<div
|
|
151
|
+
key=${d.date}
|
|
152
|
+
class="flex flex-col items-center gap-1 shrink-0"
|
|
153
|
+
style="min-width: 40px"
|
|
154
|
+
title=${`${d.date}: ${formatCost(cost)} (${d.sessions ?? 0} sessions)`}
|
|
155
|
+
>
|
|
156
|
+
<span class="text-xs font-mono text-gray-500">${formatCostShort(cost)}</span>
|
|
157
|
+
<div
|
|
158
|
+
class="w-6 bg-blue-500 rounded-t-sm"
|
|
159
|
+
style=${`height: ${barH}px`}
|
|
160
|
+
></div>
|
|
161
|
+
<span class="text-xs text-gray-500 rotate-0">${formatDateShort(d.date)}</span>
|
|
162
|
+
</div>
|
|
163
|
+
`;
|
|
164
|
+
})}
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
// AgentBarChart
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
function AgentBarChart({ metrics }) {
|
|
175
|
+
const agentCosts = useMemo(() => {
|
|
176
|
+
const map = new Map();
|
|
177
|
+
for (const m of metrics) {
|
|
178
|
+
const name = m.agentName || "unknown";
|
|
179
|
+
const cost = m.estimatedCostUsd || 0;
|
|
180
|
+
map.set(name, (map.get(name) || 0) + cost);
|
|
181
|
+
}
|
|
182
|
+
return Array.from(map.entries())
|
|
183
|
+
.map(([name, cost]) => ({ name, cost }))
|
|
184
|
+
.sort((a, b) => b.cost - a.cost);
|
|
185
|
+
}, [metrics]);
|
|
186
|
+
|
|
187
|
+
if (agentCosts.length === 0) {
|
|
188
|
+
return html`<div class="text-center text-gray-500 py-8">No cost data</div>`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const maxCost = agentCosts[0]?.cost ?? 0;
|
|
192
|
+
|
|
193
|
+
return html`
|
|
194
|
+
<div class="flex flex-col gap-2">
|
|
195
|
+
${agentCosts.map(({ name, cost }) => {
|
|
196
|
+
const pct = maxCost > 0 ? (cost / maxCost) * 100 : 0;
|
|
197
|
+
return html`
|
|
198
|
+
<div key=${name} class="flex items-center gap-3">
|
|
199
|
+
<a
|
|
200
|
+
href=${`#inspect/${encodeURIComponent(name)}`}
|
|
201
|
+
class="text-blue-400 hover:text-blue-300 text-sm w-[140px] shrink-0 truncate"
|
|
202
|
+
>
|
|
203
|
+
${name}
|
|
204
|
+
</a>
|
|
205
|
+
<div class="flex-1 bg-white/5 rounded-sm h-5 overflow-hidden">
|
|
206
|
+
<div
|
|
207
|
+
class="bg-blue-500 h-full rounded-sm"
|
|
208
|
+
style=${`width: ${pct.toFixed(1)}%`}
|
|
209
|
+
></div>
|
|
210
|
+
</div>
|
|
211
|
+
<span class="font-mono text-sm text-gray-300 w-20 text-right shrink-0">
|
|
212
|
+
${formatCost(cost)}
|
|
213
|
+
</span>
|
|
214
|
+
</div>
|
|
215
|
+
`;
|
|
216
|
+
})}
|
|
217
|
+
</div>
|
|
218
|
+
`;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
// TokenDistribution
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
|
|
225
|
+
function TokenDistribution({ totals }) {
|
|
226
|
+
const totalTokens = totals.input + totals.output + totals.cacheRead + totals.cacheCreated;
|
|
227
|
+
|
|
228
|
+
if (totalTokens === 0) {
|
|
229
|
+
return html`<div class="text-center text-gray-500 py-4">No token data</div>`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const segments = [
|
|
233
|
+
{ label: "Input", value: totals.input, color: "bg-blue-500" },
|
|
234
|
+
{ label: "Output", value: totals.output, color: "bg-green-500" },
|
|
235
|
+
{ label: "Cache Read", value: totals.cacheRead, color: "bg-yellow-500" },
|
|
236
|
+
{ label: "Cache Created", value: totals.cacheCreated, color: "bg-purple-500" },
|
|
237
|
+
].filter((s) => s.value > 0);
|
|
238
|
+
|
|
239
|
+
return html`
|
|
240
|
+
<div>
|
|
241
|
+
<!-- Segmented bar -->
|
|
242
|
+
<div class="flex h-6 rounded-sm overflow-hidden gap-px">
|
|
243
|
+
${segments.map((s) => {
|
|
244
|
+
const pct = (s.value / totalTokens) * 100;
|
|
245
|
+
return html`
|
|
246
|
+
<div
|
|
247
|
+
key=${s.label}
|
|
248
|
+
class=${s.color}
|
|
249
|
+
style=${`width: ${pct.toFixed(2)}%`}
|
|
250
|
+
title=${`${s.label}: ${formatNumber(s.value)} (${pct.toFixed(1)}%)`}
|
|
251
|
+
></div>
|
|
252
|
+
`;
|
|
253
|
+
})}
|
|
254
|
+
</div>
|
|
255
|
+
<!-- Legend -->
|
|
256
|
+
<div class="flex flex-wrap gap-x-6 gap-y-2 mt-3">
|
|
257
|
+
${segments.map((s) => {
|
|
258
|
+
const pct = ((s.value / totalTokens) * 100).toFixed(1);
|
|
259
|
+
return html`
|
|
260
|
+
<div key=${s.label} class="flex items-center gap-2 text-sm">
|
|
261
|
+
<div class=${`${s.color} w-3 h-3 rounded-full shrink-0`}></div>
|
|
262
|
+
<span class="text-gray-400">${s.label}</span>
|
|
263
|
+
<span class="font-mono text-gray-300">${formatNumber(s.value)}</span>
|
|
264
|
+
<span class="text-gray-500">${pct}%</span>
|
|
265
|
+
</div>
|
|
266
|
+
`;
|
|
267
|
+
})}
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
270
|
+
`;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// ---------------------------------------------------------------------------
|
|
274
|
+
// CapabilityChart
|
|
275
|
+
// ---------------------------------------------------------------------------
|
|
276
|
+
|
|
277
|
+
function CapabilityChart({ metrics }) {
|
|
278
|
+
const capCosts = useMemo(() => {
|
|
279
|
+
const map = new Map();
|
|
280
|
+
for (const m of metrics) {
|
|
281
|
+
const cap = m.capability || "unknown";
|
|
282
|
+
const cost = m.estimatedCostUsd || 0;
|
|
283
|
+
map.set(cap, (map.get(cap) || 0) + cost);
|
|
284
|
+
}
|
|
285
|
+
return Array.from(map.entries())
|
|
286
|
+
.map(([cap, cost]) => ({ cap, cost }))
|
|
287
|
+
.sort((a, b) => b.cost - a.cost);
|
|
288
|
+
}, [metrics]);
|
|
289
|
+
|
|
290
|
+
// Only show if more than one capability
|
|
291
|
+
if (capCosts.length <= 1) return null;
|
|
292
|
+
|
|
293
|
+
const maxCost = capCosts[0]?.cost ?? 0;
|
|
294
|
+
|
|
295
|
+
return html`
|
|
296
|
+
<div class="flex flex-col gap-2">
|
|
297
|
+
${capCosts.map(({ cap, cost }) => {
|
|
298
|
+
const pct = maxCost > 0 ? (cost / maxCost) * 100 : 0;
|
|
299
|
+
return html`
|
|
300
|
+
<div key=${cap} class="flex items-center gap-3">
|
|
301
|
+
<span class="text-sm text-gray-400 w-[140px] shrink-0 truncate">${cap}</span>
|
|
302
|
+
<div class="flex-1 bg-white/5 rounded-sm h-5 overflow-hidden">
|
|
303
|
+
<div
|
|
304
|
+
class="bg-blue-400 h-full rounded-sm"
|
|
305
|
+
style=${`width: ${pct.toFixed(1)}%`}
|
|
306
|
+
></div>
|
|
307
|
+
</div>
|
|
308
|
+
<span class="font-mono text-sm text-gray-300 w-20 text-right shrink-0">
|
|
309
|
+
${formatCost(cost)}
|
|
310
|
+
</span>
|
|
311
|
+
</div>
|
|
312
|
+
`;
|
|
313
|
+
})}
|
|
314
|
+
</div>
|
|
315
|
+
`;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ---------------------------------------------------------------------------
|
|
319
|
+
// CostsView
|
|
320
|
+
// ---------------------------------------------------------------------------
|
|
321
|
+
|
|
322
|
+
export function CostsView({ metrics: initialMetrics, snapshots }) {
|
|
323
|
+
const [groupByCapability, setGroupByCapability] = useState(false);
|
|
324
|
+
const [timeWindow, setTimeWindow] = useState(null); // null = all time
|
|
325
|
+
const [filteredMetrics, setFilteredMetrics] = useState(null);
|
|
326
|
+
const [modelData, setModelData] = useState([]);
|
|
327
|
+
const [dateData, setDateData] = useState([]);
|
|
328
|
+
const [loading, setLoading] = useState(false);
|
|
329
|
+
const [agentExpanded, setAgentExpanded] = useState(() => {
|
|
330
|
+
const names = new Set(
|
|
331
|
+
(Array.isArray(initialMetrics) ? initialMetrics : []).map((m) => m.agentName || "unknown"),
|
|
332
|
+
);
|
|
333
|
+
return names.size <= 5;
|
|
334
|
+
});
|
|
335
|
+
const [detailExpanded, setDetailExpanded] = useState(
|
|
336
|
+
() => !initialMetrics || initialMetrics.length < 10,
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
const onToggleGroup = useCallback(() => setGroupByCapability((v) => !v), []);
|
|
340
|
+
|
|
341
|
+
// When timeWindow changes, fetch filtered data from all 3 endpoints
|
|
342
|
+
useEffect(() => {
|
|
343
|
+
const sinceIso = timeWindow !== null ? new Date(Date.now() - timeWindow).toISOString() : null;
|
|
344
|
+
const enc = sinceIso ? encodeURIComponent(sinceIso) : null;
|
|
345
|
+
|
|
346
|
+
const metricsUrl = enc ? `/api/metrics?since=${enc}&limit=1000` : "/api/metrics?limit=1000";
|
|
347
|
+
const modelUrl = enc ? `/api/metrics/by-model?since=${enc}` : "/api/metrics/by-model";
|
|
348
|
+
const dateUrl = enc ? `/api/metrics/by-date?since=${enc}` : "/api/metrics/by-date";
|
|
349
|
+
|
|
350
|
+
setLoading(true);
|
|
351
|
+
Promise.all([
|
|
352
|
+
fetchJson(metricsUrl).catch(() => []),
|
|
353
|
+
fetchJson(modelUrl).catch(() => []),
|
|
354
|
+
fetchJson(dateUrl).catch(() => []),
|
|
355
|
+
]).then(([metrics, byModel, byDate]) => {
|
|
356
|
+
setFilteredMetrics(Array.isArray(metrics) ? metrics : []);
|
|
357
|
+
setModelData(Array.isArray(byModel) ? byModel : []);
|
|
358
|
+
setDateData(Array.isArray(byDate) ? byDate : []);
|
|
359
|
+
setLoading(false);
|
|
360
|
+
});
|
|
361
|
+
}, [timeWindow]);
|
|
362
|
+
|
|
363
|
+
// Use filteredMetrics (always fetched), fall back to initialMetrics before first fetch completes
|
|
364
|
+
const safeMetrics = Array.isArray(filteredMetrics)
|
|
365
|
+
? filteredMetrics
|
|
366
|
+
: Array.isArray(initialMetrics)
|
|
367
|
+
? initialMetrics
|
|
368
|
+
: [];
|
|
369
|
+
const safeSnapshots = snapshots || [];
|
|
370
|
+
|
|
371
|
+
// Compute overall totals
|
|
372
|
+
const totals = useMemo(
|
|
373
|
+
() =>
|
|
374
|
+
safeMetrics.reduce(
|
|
375
|
+
(acc, m) => {
|
|
376
|
+
acc.input += m.inputTokens || 0;
|
|
377
|
+
acc.output += m.outputTokens || 0;
|
|
378
|
+
acc.cacheRead += m.cacheReadTokens || 0;
|
|
379
|
+
acc.cacheCreated += m.cacheCreationTokens || 0;
|
|
380
|
+
if (m.estimatedCostUsd != null) {
|
|
381
|
+
acc.cost = (acc.cost ?? 0) + m.estimatedCostUsd;
|
|
382
|
+
}
|
|
383
|
+
return acc;
|
|
384
|
+
},
|
|
385
|
+
{ input: 0, output: 0, cacheRead: 0, cacheCreated: 0, cost: null },
|
|
386
|
+
),
|
|
387
|
+
[safeMetrics],
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
const sessionCount = safeMetrics.length;
|
|
391
|
+
const ioTokens = totals.input + totals.output;
|
|
392
|
+
const cacheTokens = totals.cacheRead + totals.cacheCreated;
|
|
393
|
+
const avgCost = totals.cost != null && sessionCount > 0 ? totals.cost / sessionCount : null;
|
|
394
|
+
|
|
395
|
+
// Group by capability when requested
|
|
396
|
+
const grouped = useMemo(() => {
|
|
397
|
+
if (!groupByCapability) return null;
|
|
398
|
+
const map = new Map();
|
|
399
|
+
for (const m of safeMetrics) {
|
|
400
|
+
const cap = m.capability || "unknown";
|
|
401
|
+
if (!map.has(cap)) map.set(cap, []);
|
|
402
|
+
map.get(cap).push(m);
|
|
403
|
+
}
|
|
404
|
+
return map;
|
|
405
|
+
}, [safeMetrics, groupByCapability]);
|
|
406
|
+
|
|
407
|
+
const thClass = "text-gray-500 uppercase text-xs tracking-wider text-left px-3 py-2 font-normal";
|
|
408
|
+
const tdClass = "px-3 py-2 border-b border-border text-sm";
|
|
409
|
+
const tdMono = `${tdClass} font-mono`;
|
|
410
|
+
|
|
411
|
+
function MetricRow({ m }) {
|
|
412
|
+
return html`
|
|
413
|
+
<tr class="hover:bg-white/5">
|
|
414
|
+
<td class=${tdClass}>
|
|
415
|
+
<a
|
|
416
|
+
href=${`#inspect/${encodeURIComponent(m.agentName || "")}`}
|
|
417
|
+
class="text-blue-400 hover:text-blue-300"
|
|
418
|
+
>
|
|
419
|
+
${m.agentName || "—"}
|
|
420
|
+
</a>
|
|
421
|
+
</td>
|
|
422
|
+
<td class=${`text-gray-400 ${tdClass}`}>${m.capability || ""}</td>
|
|
423
|
+
<td class=${tdMono}>${formatNumber(m.inputTokens)}</td>
|
|
424
|
+
<td class=${tdMono}>${formatNumber(m.outputTokens)}</td>
|
|
425
|
+
<td class=${tdMono}>${formatNumber(m.cacheReadTokens)}</td>
|
|
426
|
+
<td class=${tdMono}>${formatNumber(m.cacheCreationTokens)}</td>
|
|
427
|
+
<td class=${tdMono}>${m.estimatedCostUsd != null ? formatCost(m.estimatedCostUsd) : "—"}</td>
|
|
428
|
+
</tr>
|
|
429
|
+
`;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function SubtotalRow({ label, rows }) {
|
|
433
|
+
const sub = rows.reduce(
|
|
434
|
+
(acc, m) => {
|
|
435
|
+
acc.input += m.inputTokens || 0;
|
|
436
|
+
acc.output += m.outputTokens || 0;
|
|
437
|
+
acc.cacheRead += m.cacheReadTokens || 0;
|
|
438
|
+
acc.cacheCreated += m.cacheCreationTokens || 0;
|
|
439
|
+
if (m.estimatedCostUsd != null) {
|
|
440
|
+
acc.cost = (acc.cost ?? 0) + m.estimatedCostUsd;
|
|
441
|
+
}
|
|
442
|
+
return acc;
|
|
443
|
+
},
|
|
444
|
+
{ input: 0, output: 0, cacheRead: 0, cacheCreated: 0, cost: null },
|
|
445
|
+
);
|
|
446
|
+
return html`
|
|
447
|
+
<tr class="bg-white/5 text-gray-400">
|
|
448
|
+
<td class=${`font-medium ${tdClass}`} colspan="2">${label} subtotal</td>
|
|
449
|
+
<td class=${tdMono}>${formatNumber(sub.input)}</td>
|
|
450
|
+
<td class=${tdMono}>${formatNumber(sub.output)}</td>
|
|
451
|
+
<td class=${tdMono}>${formatNumber(sub.cacheRead)}</td>
|
|
452
|
+
<td class=${tdMono}>${formatNumber(sub.cacheCreated)}</td>
|
|
453
|
+
<td class=${tdMono}>${sub.cost != null ? formatCost(sub.cost) : "—"}</td>
|
|
454
|
+
</tr>
|
|
455
|
+
`;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function TableBody() {
|
|
459
|
+
if (safeMetrics.length === 0) {
|
|
460
|
+
return html`
|
|
461
|
+
<tr>
|
|
462
|
+
<td colspan="7" class="text-gray-500 text-center py-8">No metrics yet</td>
|
|
463
|
+
</tr>
|
|
464
|
+
`;
|
|
465
|
+
}
|
|
466
|
+
if (grouped) {
|
|
467
|
+
const rows = [];
|
|
468
|
+
for (const [cap, capMetrics] of grouped) {
|
|
469
|
+
for (const m of capMetrics) {
|
|
470
|
+
rows.push(html`<${MetricRow} key=${m.agentName + cap} m=${m} />`);
|
|
471
|
+
}
|
|
472
|
+
rows.push(html`<${SubtotalRow} key=${`sub-${cap}`} label=${cap} rows=${capMetrics} />`);
|
|
473
|
+
}
|
|
474
|
+
return html`${rows}`;
|
|
475
|
+
}
|
|
476
|
+
return html`${safeMetrics.map((m) => html`<${MetricRow} key=${m.agentName} m=${m} />`)}`;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Determine if capability chart should show
|
|
480
|
+
const uniqueCaps = new Set(safeMetrics.map((m) => m.capability || "unknown"));
|
|
481
|
+
const agentCount = useMemo(
|
|
482
|
+
() => new Set(safeMetrics.map((m) => m.agentName || "unknown")).size,
|
|
483
|
+
[safeMetrics],
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
return html`
|
|
487
|
+
<div class="flex flex-col gap-6 p-6">
|
|
488
|
+
<!-- Time Window Selector -->
|
|
489
|
+
<div class="flex items-center justify-between">
|
|
490
|
+
<div class="text-gray-500 uppercase text-xs tracking-wider">Cost Analysis</div>
|
|
491
|
+
<div class="flex items-center gap-2">
|
|
492
|
+
${loading ? html`<span class="text-xs text-gray-500">Loading...</span>` : null}
|
|
493
|
+
<select
|
|
494
|
+
class="text-sm bg-surface border border-border rounded-sm px-3 py-1.5 text-gray-300 focus:outline-none focus:border-blue-500"
|
|
495
|
+
value=${String(timeWindow)}
|
|
496
|
+
onChange=${(e) => {
|
|
497
|
+
const val = e.target.value;
|
|
498
|
+
setTimeWindow(val === "null" ? null : Number(val));
|
|
499
|
+
}}
|
|
500
|
+
>
|
|
501
|
+
${TIME_WINDOWS.map(
|
|
502
|
+
(w) => html`
|
|
503
|
+
<option key=${String(w.value)} value=${String(w.value)}>
|
|
504
|
+
${w.label}
|
|
505
|
+
</option>
|
|
506
|
+
`,
|
|
507
|
+
)}
|
|
508
|
+
</select>
|
|
509
|
+
</div>
|
|
510
|
+
</div>
|
|
511
|
+
|
|
512
|
+
<!-- Summary Stat Cards -->
|
|
513
|
+
<div class="flex gap-4">
|
|
514
|
+
<${StatCard}
|
|
515
|
+
label="Total Cost"
|
|
516
|
+
value=${totals.cost != null ? formatCostShort(totals.cost) : "—"}
|
|
517
|
+
/>
|
|
518
|
+
<${StatCard}
|
|
519
|
+
label="Input/Output"
|
|
520
|
+
value=${formatNumber(ioTokens)}
|
|
521
|
+
/>
|
|
522
|
+
<${StatCard}
|
|
523
|
+
label="Cache"
|
|
524
|
+
value=${formatNumber(cacheTokens)}
|
|
525
|
+
/>
|
|
526
|
+
<${StatCard}
|
|
527
|
+
label="Sessions"
|
|
528
|
+
value=${String(sessionCount)}
|
|
529
|
+
/>
|
|
530
|
+
<${StatCard}
|
|
531
|
+
label="Avg Cost/Session"
|
|
532
|
+
value=${avgCost != null ? formatCostShort(avgCost) : "—"}
|
|
533
|
+
/>
|
|
534
|
+
</div>
|
|
535
|
+
|
|
536
|
+
<!-- Model Usage Breakdown (show when data available) -->
|
|
537
|
+
${
|
|
538
|
+
modelData.length > 0
|
|
539
|
+
? html`
|
|
540
|
+
<div class="bg-surface border border-border rounded-sm p-4">
|
|
541
|
+
<div class="text-gray-500 uppercase text-xs tracking-wider mb-4">Model Usage</div>
|
|
542
|
+
<${ModelBreakdown} modelData=${modelData} />
|
|
543
|
+
</div>
|
|
544
|
+
`
|
|
545
|
+
: null
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
<!-- Cost by Agent Bar Chart -->
|
|
549
|
+
<div class="bg-surface border border-border rounded-sm p-4">
|
|
550
|
+
<div
|
|
551
|
+
class="flex items-center justify-between cursor-pointer"
|
|
552
|
+
onClick=${() => setAgentExpanded((v) => !v)}
|
|
553
|
+
>
|
|
554
|
+
<div class="text-gray-500 uppercase text-xs tracking-wider">Cost by Agent</div>
|
|
555
|
+
<span class="text-gray-500 text-sm">${agentExpanded ? "▾" : "▸"}</span>
|
|
556
|
+
</div>
|
|
557
|
+
${
|
|
558
|
+
agentExpanded
|
|
559
|
+
? html`<div class="mt-4"><${AgentBarChart} metrics=${safeMetrics} /></div>`
|
|
560
|
+
: html`<div class="text-sm text-gray-500 mt-2">${agentCount} agent${agentCount === 1 ? "" : "s"} — ${totals.cost != null ? formatCostShort(totals.cost) : "—"} total</div>`
|
|
561
|
+
}
|
|
562
|
+
</div>
|
|
563
|
+
|
|
564
|
+
<!-- Date Chart (show when data available) -->
|
|
565
|
+
${
|
|
566
|
+
dateData.length > 0
|
|
567
|
+
? html`
|
|
568
|
+
<div class="bg-surface border border-border rounded-sm p-4">
|
|
569
|
+
<div class="text-gray-500 uppercase text-xs tracking-wider mb-4">Daily Cost Trend</div>
|
|
570
|
+
<${DateChart} dateData=${dateData} />
|
|
571
|
+
</div>
|
|
572
|
+
`
|
|
573
|
+
: null
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
<!-- Token Distribution -->
|
|
577
|
+
<div class="bg-surface border border-border rounded-sm p-4">
|
|
578
|
+
<div class="text-gray-500 uppercase text-xs tracking-wider mb-4">Token Distribution</div>
|
|
579
|
+
<${TokenDistribution} totals=${totals} />
|
|
580
|
+
</div>
|
|
581
|
+
|
|
582
|
+
<!-- Cost by Capability (only if more than one capability) -->
|
|
583
|
+
${
|
|
584
|
+
uniqueCaps.size > 1
|
|
585
|
+
? html`
|
|
586
|
+
<div class="bg-surface border border-border rounded-sm p-4">
|
|
587
|
+
<div class="text-gray-500 uppercase text-xs tracking-wider mb-4">Cost by Capability</div>
|
|
588
|
+
<${CapabilityChart} metrics=${safeMetrics} />
|
|
589
|
+
</div>
|
|
590
|
+
`
|
|
591
|
+
: null
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
<!-- Detailed Costs Table -->
|
|
595
|
+
<div class="bg-surface border border-border rounded-sm p-4">
|
|
596
|
+
<div class=${`flex items-center gap-3 ${detailExpanded ? "mb-4" : ""}`}>
|
|
597
|
+
<div
|
|
598
|
+
class="flex items-center gap-2 cursor-pointer"
|
|
599
|
+
onClick=${() => setDetailExpanded((v) => !v)}
|
|
600
|
+
>
|
|
601
|
+
<span class="text-gray-500 uppercase text-xs tracking-wider">Detailed Breakdown</span>
|
|
602
|
+
<span class="text-gray-500 text-sm">${detailExpanded ? "▾" : "▸"}</span>
|
|
603
|
+
</div>
|
|
604
|
+
${
|
|
605
|
+
detailExpanded
|
|
606
|
+
? html`
|
|
607
|
+
<button
|
|
608
|
+
class=${
|
|
609
|
+
"text-sm px-3 py-1.5 rounded-sm border border-border ml-auto " +
|
|
610
|
+
(groupByCapability
|
|
611
|
+
? "bg-white/10 text-gray-200"
|
|
612
|
+
: "bg-surface text-gray-400 hover:text-gray-200")
|
|
613
|
+
}
|
|
614
|
+
onClick=${onToggleGroup}
|
|
615
|
+
>
|
|
616
|
+
${groupByCapability ? "Ungroup" : "Group by Capability"}
|
|
617
|
+
</button>
|
|
618
|
+
`
|
|
619
|
+
: null
|
|
620
|
+
}
|
|
621
|
+
</div>
|
|
622
|
+
|
|
623
|
+
${
|
|
624
|
+
detailExpanded
|
|
625
|
+
? html`
|
|
626
|
+
<div class="overflow-x-auto">
|
|
627
|
+
<table class="w-full text-sm border-collapse">
|
|
628
|
+
<thead>
|
|
629
|
+
<tr class="border-b border-border">
|
|
630
|
+
<th class=${thClass}>Agent</th>
|
|
631
|
+
<th class=${thClass}>Capability</th>
|
|
632
|
+
<th class=${thClass}>Input Tokens</th>
|
|
633
|
+
<th class=${thClass}>Output Tokens</th>
|
|
634
|
+
<th class=${thClass}>Cache Read</th>
|
|
635
|
+
<th class=${thClass}>Cache Created</th>
|
|
636
|
+
<th class=${thClass}>Est. Cost</th>
|
|
637
|
+
</tr>
|
|
638
|
+
</thead>
|
|
639
|
+
<tbody>
|
|
640
|
+
<${TableBody} />
|
|
641
|
+
</tbody>
|
|
642
|
+
<tfoot class="border-t border-border">
|
|
643
|
+
<tr class="text-gray-300 font-medium">
|
|
644
|
+
<td class=${`border-b-0 ${tdClass}`} colspan="2">Total</td>
|
|
645
|
+
<td class=${`border-b-0 ${tdMono}`}>${formatNumber(totals.input)}</td>
|
|
646
|
+
<td class=${`border-b-0 ${tdMono}`}>${formatNumber(totals.output)}</td>
|
|
647
|
+
<td class=${`border-b-0 ${tdMono}`}>${formatNumber(totals.cacheRead)}</td>
|
|
648
|
+
<td class=${`border-b-0 ${tdMono}`}>${formatNumber(totals.cacheCreated)}</td>
|
|
649
|
+
<td class=${`border-b-0 ${tdMono}`}>
|
|
650
|
+
${totals.cost != null ? formatCost(totals.cost) : "—"}
|
|
651
|
+
</td>
|
|
652
|
+
</tr>
|
|
653
|
+
</tfoot>
|
|
654
|
+
</table>
|
|
655
|
+
</div>
|
|
656
|
+
`
|
|
657
|
+
: null
|
|
658
|
+
}
|
|
659
|
+
</div>
|
|
660
|
+
|
|
661
|
+
<!-- Live Snapshots (only when data exists) -->
|
|
662
|
+
${
|
|
663
|
+
safeSnapshots.length > 0
|
|
664
|
+
? html`
|
|
665
|
+
<div class="bg-surface border border-border rounded-sm p-4">
|
|
666
|
+
<div class="text-gray-500 uppercase text-xs tracking-wider mb-3">
|
|
667
|
+
Active Agent Token Usage
|
|
668
|
+
</div>
|
|
669
|
+
<div class="flex flex-col gap-2">
|
|
670
|
+
${safeSnapshots.map(
|
|
671
|
+
(s) => html`
|
|
672
|
+
<div
|
|
673
|
+
key=${s.agentName}
|
|
674
|
+
class="flex flex-row gap-4 items-baseline text-sm py-1 border-b border-border last:border-0"
|
|
675
|
+
>
|
|
676
|
+
<span class="text-gray-200 min-w-[120px]">${s.agentName || ""}</span>
|
|
677
|
+
<span class="font-mono text-gray-300">
|
|
678
|
+
${formatNumber((s.inputTokens || 0) + (s.outputTokens || 0))} tokens
|
|
679
|
+
</span>
|
|
680
|
+
<span class="text-gray-500">${s.modelUsed || ""}</span>
|
|
681
|
+
<span class="text-gray-500 ml-auto">${timeAgo(s.createdAt)}</span>
|
|
682
|
+
</div>
|
|
683
|
+
`,
|
|
684
|
+
)}
|
|
685
|
+
</div>
|
|
686
|
+
</div>
|
|
687
|
+
`
|
|
688
|
+
: null
|
|
689
|
+
}
|
|
690
|
+
</div>
|
|
691
|
+
`;
|
|
692
|
+
}
|