Files
fusion/packages/dashboard/app/components/AgentTokenStatsPanel.tsx
gsxdsm 3cf040a080 feat(tui): card windowing, log G-jump, header tiny mode, expand-log width
- BoardView columns now window their card list against the available
  rows so the cursor stays visible. Cards beyond the viewport scroll
  in via "↑ N more" / "↓ N more" indicators. Fixes the "can't reach
  cards in the Done column" bug — selectedIndex was advancing but the
  cards were clipped.
- Logs section: G (vim-style, in addition to End) jumps to newest
  entry. Lowercase g stays as the global Settings shortcut.
- MainHeader gains two new collapse tiers: cols < 50 renders just
  FUSION + the active tab pills (no inactive tabs, no dividers); rows
  < 10 hides the header entirely so the row isn't wasted.
- ExpandedLog stretches to width="100%" so the panel keeps its width
  when an entry is expanded — was collapsing to content width before.
- Lighter blue palette throughout (cyanBright / cyan); status-mode
  grid widened to 5:6 so Stats panel gets ~45% of cols.
- Stats: heap limit always on its own continuation row; bytes
  formatted with a space ("450 MB") so a forced wrap, if it ever
  happens, breaks after the unit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 00:07:19 -07:00

106 lines
4.0 KiB
TypeScript

import { useMemo } from "react";
import type { Agent } from "../api";
import "./AgentTokenStatsPanel.css";
interface AgentTokenStatsPanelProps {
agents: Agent[];
}
interface AgentTokenRow {
id: string;
name: string;
inputTokens: number;
outputTokens: number;
totalTokens: number;
}
function normalizeTokenCount(value: number | undefined): number {
return typeof value === "number" && Number.isFinite(value) ? value : 0;
}
function formatTokenCount(value: number): string {
return value.toLocaleString();
}
export function AgentTokenStatsPanel({ agents }: AgentTokenStatsPanelProps) {
const { rows, totalInputTokens, totalOutputTokens, totalTokens } = useMemo(() => {
const computedRows = agents
.map((agent): AgentTokenRow => {
const inputTokens = normalizeTokenCount(agent.totalInputTokens);
const outputTokens = normalizeTokenCount(agent.totalOutputTokens);
return {
id: agent.id,
name: agent.name,
inputTokens,
outputTokens,
totalTokens: inputTokens + outputTokens,
};
})
.sort((a, b) => b.totalTokens - a.totalTokens || a.name.localeCompare(b.name) || a.id.localeCompare(b.id));
return {
rows: computedRows,
totalInputTokens: computedRows.reduce((sum, row) => sum + row.inputTokens, 0),
totalOutputTokens: computedRows.reduce((sum, row) => sum + row.outputTokens, 0),
totalTokens: computedRows.reduce((sum, row) => sum + row.totalTokens, 0),
};
}, [agents]);
const hasUsageData = totalTokens > 0;
return (
<section className="agent-token-stats-panel" aria-label="Agent token usage statistics">
<header className="agent-token-stats-panel__header">
<h3 className="agent-token-stats-panel__title">Token Usage by Agent</h3>
</header>
<div className="agent-token-stats-panel__totals" role="list" aria-label="Token usage totals">
<div className="agent-token-stats-panel__total-card" role="listitem">
<span className="agent-token-stats-panel__total-label">Input Tokens</span>
<span className="agent-token-stats-panel__total-value">{formatTokenCount(totalInputTokens)}</span>
</div>
<div className="agent-token-stats-panel__total-card" role="listitem">
<span className="agent-token-stats-panel__total-label">Output Tokens</span>
<span className="agent-token-stats-panel__total-value">{formatTokenCount(totalOutputTokens)}</span>
</div>
<div className="agent-token-stats-panel__total-card" role="listitem">
<span className="agent-token-stats-panel__total-label">Combined Tokens</span>
<span className="agent-token-stats-panel__total-value">{formatTokenCount(totalTokens)}</span>
</div>
</div>
{hasUsageData ? (
<div className="agent-token-stats-panel__table-wrapper">
<table className="agent-token-stats-panel__table">
<thead>
<tr>
<th scope="col">Agent</th>
<th scope="col">Input</th>
<th scope="col">Output</th>
<th scope="col">Total</th>
</tr>
</thead>
<tbody>
{rows.map((row) => (
<tr key={row.id}>
<th scope="row" className="agent-token-stats-panel__agent-cell">
<span className="agent-token-stats-panel__agent-name">{row.name}</span>
<span className="agent-token-stats-panel__agent-id">{row.id}</span>
</th>
<td>{formatTokenCount(row.inputTokens)}</td>
<td>{formatTokenCount(row.outputTokens)}</td>
<td className="agent-token-stats-panel__total-cell">{formatTokenCount(row.totalTokens)}</td>
</tr>
))}
</tbody>
</table>
</div>
) : (
<div className="agent-token-stats-panel__empty" role="status">
No token usage recorded yet. Token totals appear here once agents run.
</div>
)}
</section>
);
}