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>
This commit is contained in:
gsxdsm
2026-04-25 00:07:19 -07:00
parent 7fc3334934
commit 21569e6d48
8 changed files with 434 additions and 16 deletions

View File

@@ -779,6 +779,7 @@ function MainHeader({ state }: { state: DashboardState }) {
const interactiveView = state.interactiveView;
const { stdout } = useStdout();
const cols = stdout?.columns ?? 80;
const rows = stdout?.rows ?? 24;
const interactiveTabs: Array<{ key: string; label: string; view: InteractiveView }> = [
{ key: "b", label: "Board", view: "board" },
{ key: "a", label: "Agents", view: "agents" },
@@ -791,20 +792,44 @@ function MainHeader({ state }: { state: DashboardState }) {
// * Help/quit (~20 chars) drops first.
// * Inactive interactive tabs collapse to a `[k]` glyph.
// * Inactive section tabs collapse to `[N]`.
// * Final fallback shows just the active tab — but never abbreviates the
// FUSION mark itself.
// * Active-only mode hides every inactive tab.
// * Tiny mode drops the whole header — the row is too precious to spend.
if (rows < 10) return null;
const showHelpHint = cols >= 110;
const compactInteractive = cols < 100;
const compactSections = cols < 90;
const minimal = cols < 70;
const tiny = cols < 50;
if (tiny) {
// Just FUSION + the single active tab. No dividers, no other tabs.
const activeSectionIdx = inInteractive
? -1
: SECTION_ORDER.indexOf(focused);
const activeSectionLabel = activeSectionIdx >= 0
? SECTION_ORDER[activeSectionIdx].charAt(0).toUpperCase() + SECTION_ORDER[activeSectionIdx].slice(1)
: null;
const activeInteractive = inInteractive
? interactiveTabs.find((t) => t.view === interactiveView) ?? null
: null;
return (
<Box flexDirection="row" gap={1} paddingX={1}>
<MiniLogo />
{activeSectionLabel && (
<Text backgroundColor="cyan" color="black" bold>{` ${activeSectionIdx + 1} ${activeSectionLabel} `}</Text>
)}
{activeInteractive && (
<Text backgroundColor="cyan" color="black" bold>{` ${activeInteractive.key} ${activeInteractive.label} `}</Text>
)}
</Box>
);
}
return (
<Box flexDirection="row" gap={1} paddingX={1} paddingY={0}>
<MiniLogo />
<Text dimColor></Text>
{!minimal && <Text dimColor></Text>}
{SECTION_ORDER.map((section, i) => {
const isActive = !inInteractive && section === focused;
const label = section.charAt(0).toUpperCase() + section.slice(1);
// In minimal mode only show the active section tab.
if (minimal && !isActive) return null;
return (
<Box key={section} marginRight={1}>
@@ -902,12 +927,14 @@ function KanbanColumnView({
isFocused,
selectedIndex,
width,
availableRows,
}: {
column: KanbanColumn;
tasks: TaskItem[];
isFocused: boolean;
selectedIndex: number;
width: number;
availableRows: number;
}) {
const accent = COLUMN_COLORS[column];
const headerColor = isFocused ? "whiteBright" : accent;
@@ -917,6 +944,21 @@ function KanbanColumnView({
const headerText = ` ${label} `.length > innerHeaderWidth
? ` ${label} `.slice(0, innerHeaderWidth)
: ` ${label} `.padEnd(innerHeaderWidth, " ");
// Each TaskCard takes ~4 rows (top border + id row + title row + bottom
// border + sometimes a wrapped title row). Reserve 2 rows for header +
// header spacer + 2 rows for "↑/↓ N more" hints, then floor(remaining/4).
const cardRowsBudget = Math.max(0, availableRows - 4);
const visibleCount = Math.max(1, Math.floor(cardRowsBudget / 4));
// Slide the window so the selected card is centered when possible.
const halfWindow = Math.floor(visibleCount / 2);
const maxStart = Math.max(0, tasks.length - visibleCount);
const windowStart = Math.max(0, Math.min(selectedIndex - halfWindow, maxStart));
const windowEnd = Math.min(tasks.length, windowStart + visibleCount);
const visibleTasks = tasks.slice(windowStart, windowEnd);
const hiddenAbove = windowStart;
const hiddenBelow = tasks.length - windowEnd;
return (
<Box
flexDirection="column"
@@ -936,14 +978,20 @@ function KanbanColumnView({
<Text dimColor></Text>
) : (
<Box flexDirection="column" gap={0} flexShrink={1} overflow="hidden">
{tasks.map((task, i) => (
{hiddenAbove > 0 && (
<Text dimColor> {hiddenAbove} more</Text>
)}
{visibleTasks.map((task, i) => (
<TaskCard
key={task.id}
task={task}
selected={isFocused && i === selectedIndex}
selected={isFocused && (windowStart + i) === selectedIndex}
width={cardWidth}
/>
))}
{hiddenBelow > 0 && (
<Text dimColor> {hiddenBelow} more</Text>
)}
</Box>
)}
</Box>
@@ -1357,12 +1405,17 @@ function groupTasksByColumn(tasks: TaskItem[]): Record<KanbanColumn, TaskItem[]>
function BoardView({ state, controller }: { state: DashboardState; controller: DashboardTUI }) {
const { stdout } = useStdout();
const cols = stdout?.columns ?? 80;
const rows = stdout?.rows ?? 24;
// Narrow mode: collapse 4-column kanban to a single full-width column so
// columns don't overflow or overlap when the terminal is too slim.
const isNarrow = cols < NARROW_THRESHOLD;
const columnWidth = isNarrow
? Math.max(20, cols - 2)
: Math.max(20, Math.floor((cols - 2) / KANBAN_COLUMNS.length));
// Reserve rows for header + project bar + spacer + footer hints. The
// kanban column then windows its cards based on what remains so cards
// beyond the visible area still scroll into view as the cursor moves.
const availableCardRows = Math.max(8, rows - 8);
const [subView, setSubView] = useState<BoardSubView>("board");
const [projectIndex, setProjectIndex] = useState(0);
@@ -1608,6 +1661,7 @@ function BoardView({ state, controller }: { state: DashboardState; controller: D
isFocused={true}
selectedIndex={focusedRow}
width={columnWidth}
availableRows={availableCardRows}
/>
) : (
KANBAN_COLUMNS.map((col, i) => (
@@ -1618,6 +1672,7 @@ function BoardView({ state, controller }: { state: DashboardState; controller: D
isFocused={i === colIndex}
selectedIndex={rowByColumn[col] ?? 0}
width={columnWidth}
availableRows={availableCardRows}
/>
))
)}
@@ -3527,7 +3582,9 @@ export function DashboardApp({ controller }: DashboardAppProps) {
return;
}
if (key.end) {
// Vim-style "G" jumps to the newest log entry. "g" stays as the
// global Settings shortcut, so we don't bind it here.
if (key.end || input === "G") {
controller.setSelectedLogIndex(Math.max(0, filteredEntries.length - 1));
return;
}

View File

@@ -0,0 +1,128 @@
.agent-token-stats-panel {
margin-bottom: var(--space-lg);
padding: var(--space-md);
border: 1px solid var(--border);
border-radius: var(--radius-md);
background: var(--card);
box-shadow: var(--shadow-sm);
}
.agent-token-stats-panel__header {
margin-bottom: var(--space-md);
}
.agent-token-stats-panel__title {
margin: 0;
font-size: calc(var(--space-md) + var(--space-xs));
font-weight: 600;
color: var(--text);
}
.agent-token-stats-panel__totals {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(calc(var(--space-2xl) * 4), 1fr));
gap: var(--space-sm);
margin-bottom: var(--space-md);
}
.agent-token-stats-panel__total-card {
display: flex;
flex-direction: column;
gap: var(--space-xs);
padding: var(--space-sm) var(--space-md);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
background: var(--surface);
}
.agent-token-stats-panel__total-label {
color: var(--text-muted);
font-size: calc(var(--space-sm) + var(--space-xs));
text-transform: uppercase;
letter-spacing: calc(var(--space-xs) / 8);
}
.agent-token-stats-panel__total-value {
color: var(--text);
font-size: calc(var(--space-lg) + var(--space-xs));
line-height: 1.2;
font-family: var(--font-mono);
font-weight: 700;
}
.agent-token-stats-panel__table-wrapper {
overflow-x: auto;
}
.agent-token-stats-panel__table {
width: 100%;
border-collapse: collapse;
}
.agent-token-stats-panel__table th,
.agent-token-stats-panel__table td {
padding: var(--space-sm);
border-bottom: 1px solid var(--border);
color: var(--text);
text-align: right;
font-size: calc(var(--space-sm) + var(--space-xs));
}
.agent-token-stats-panel__table thead th {
text-transform: uppercase;
letter-spacing: calc(var(--space-xs) / 8);
color: var(--text-muted);
font-weight: 600;
}
.agent-token-stats-panel__table th:first-child,
.agent-token-stats-panel__table td:first-child {
text-align: left;
}
.agent-token-stats-panel__agent-cell {
display: flex;
flex-direction: column;
gap: calc(var(--space-xs) * 0.5);
}
.agent-token-stats-panel__agent-name {
color: var(--text);
}
.agent-token-stats-panel__agent-id {
color: var(--text-muted);
font-family: var(--font-mono);
font-size: calc(var(--space-sm) + var(--space-xs) * 0.75);
text-transform: none;
letter-spacing: normal;
}
.agent-token-stats-panel__total-cell {
font-family: var(--font-mono);
font-weight: 700;
}
.agent-token-stats-panel__empty {
padding: var(--space-md);
border: 1px dashed var(--border);
border-radius: var(--radius-sm);
color: var(--text-muted);
background: color-mix(in srgb, var(--surface) 85%, transparent);
font-size: calc(var(--space-sm) + var(--space-xs));
}
@media (max-width: 768px) {
.agent-token-stats-panel {
padding: var(--space-sm);
}
.agent-token-stats-panel__totals {
grid-template-columns: 1fr;
}
.agent-token-stats-panel__table th,
.agent-token-stats-panel__table td {
padding: var(--space-xs) var(--space-sm);
}
}

View File

@@ -0,0 +1,105 @@
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>
);
}

View File

@@ -7,6 +7,7 @@ import { updateAgent, updateAgentState, deleteAgent, startAgentRun, fetchOrgTree
const AgentDetailView = lazy(() => import("./AgentDetailView").then((m) => ({ default: m.AgentDetailView })));
import { ActiveAgentsPanel } from "./ActiveAgentsPanel";
import { AgentMetricsBar } from "./AgentMetricsBar";
import { AgentTokenStatsPanel } from "./AgentTokenStatsPanel";
import { AgentEmptyState } from "./AgentEmptyState";
import { useAgents } from "../hooks/useAgents";
import { useAgentHierarchy } from "../hooks/useAgentHierarchy";
@@ -1266,6 +1267,7 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
{/* Secondary sections after the main collection */}
<AgentMetricsBar stats={stats} />
<AgentTokenStatsPanel agents={displayAgents} />
<ActiveAgentsPanel agents={activeAgents} projectId={projectId} onAgentSelect={setSelectedAgentId} />
</div>

View File

@@ -0,0 +1,69 @@
import { describe, expect, it } from "vitest";
import { render, screen, within } from "@testing-library/react";
import { AgentTokenStatsPanel } from "../AgentTokenStatsPanel";
import type { Agent } from "../../api";
function makeAgent(overrides: Partial<Agent>): Agent {
return {
id: "agent-default",
name: "Default Agent",
role: "executor",
state: "idle",
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
metadata: {},
...overrides,
};
}
describe("AgentTokenStatsPanel", () => {
it("renders aggregate totals from agent cumulative token fields", () => {
render(
<AgentTokenStatsPanel
agents={[
makeAgent({ id: "a-1", name: "Alpha", totalInputTokens: 100, totalOutputTokens: 40 }),
makeAgent({ id: "a-2", name: "Beta", totalInputTokens: 10, totalOutputTokens: 5 }),
]}
/>,
);
expect(screen.getByText("Input Tokens")).toBeInTheDocument();
expect(screen.getByText("110")).toBeInTheDocument();
expect(screen.getByText("Output Tokens")).toBeInTheDocument();
expect(screen.getByText("45")).toBeInTheDocument();
expect(screen.getByText("Combined Tokens")).toBeInTheDocument();
expect(screen.getByText("155")).toBeInTheDocument();
});
it("sorts per-agent rows by total usage descending", () => {
render(
<AgentTokenStatsPanel
agents={[
makeAgent({ id: "a-1", name: "Alpha", totalInputTokens: 50, totalOutputTokens: 20 }),
makeAgent({ id: "a-2", name: "Beta", totalInputTokens: 5, totalOutputTokens: 5 }),
makeAgent({ id: "a-3", name: "Gamma", totalInputTokens: 20, totalOutputTokens: 40 }),
]}
/>,
);
const rows = screen.getAllByRole("row").slice(1);
expect(within(rows[0]).getByText("Alpha")).toBeInTheDocument();
expect(within(rows[1]).getByText("Gamma")).toBeInTheDocument();
expect(within(rows[2]).getByText("Beta")).toBeInTheDocument();
});
it("treats missing token fields as zero and shows empty state when there is no usage", () => {
render(
<AgentTokenStatsPanel
agents={[
makeAgent({ id: "a-1", name: "Zero One", totalInputTokens: undefined, totalOutputTokens: undefined }),
makeAgent({ id: "a-2", name: "Zero Two" }),
]}
/>,
);
expect(screen.getByText("No token usage recorded yet. Token totals appear here once agents run.")).toBeInTheDocument();
expect(screen.queryByRole("table")).toBeNull();
expect(screen.getAllByText("0").length).toBeGreaterThanOrEqual(3);
});
});

View File

@@ -46,6 +46,8 @@ describe("AgentsView", () => {
name: "Test Agent 1",
role: "executor" as AgentCapability,
state: "idle" as AgentState,
totalInputTokens: 100,
totalOutputTokens: 20,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
metadata: {},
@@ -56,6 +58,8 @@ describe("AgentsView", () => {
role: "triage" as AgentCapability,
state: "active" as AgentState,
taskId: "FN-001",
totalInputTokens: 10,
totalOutputTokens: 5,
lastHeartbeatAt: new Date().toISOString(),
runtimeConfig: { heartbeatIntervalMs: 30000 },
createdAt: new Date(Date.now() - 86400000).toISOString(),
@@ -76,6 +80,8 @@ describe("AgentsView", () => {
name: "Test Agent 4",
role: "reviewer" as AgentCapability,
state: "terminated" as AgentState,
totalInputTokens: 1,
totalOutputTokens: 1,
createdAt: new Date(Date.now() - 259200000).toISOString(),
updatedAt: new Date().toISOString(),
metadata: {},
@@ -170,15 +176,18 @@ describe("AgentsView", () => {
await waitFor(() => {
expect(container.querySelector(".agent-list")).toBeTruthy();
expect(container.querySelector(".agent-metrics-bar")).toBeTruthy();
expect(container.querySelector(".agent-token-stats-panel")).toBeTruthy();
expect(container.querySelector(".active-agents-panel")).toBeTruthy();
});
const list = container.querySelector(".agent-list");
const metrics = container.querySelector(".agent-metrics-bar");
const tokenPanel = container.querySelector(".agent-token-stats-panel");
const activePanel = container.querySelector(".active-agents-panel");
expect(list && metrics && activePanel).toBeTruthy();
expect(list && metrics && tokenPanel && activePanel).toBeTruthy();
expect(list!.compareDocumentPosition(metrics!) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
expect(metrics!.compareDocumentPosition(activePanel!) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
expect(metrics!.compareDocumentPosition(tokenPanel!) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
expect(tokenPanel!.compareDocumentPosition(activePanel!) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
});
it("fetches agents only once on mount (regression: no duplicate initial load path)", async () => {
@@ -193,6 +202,25 @@ describe("AgentsView", () => {
expect(screen.getByText("Active Agents (1)")).toBeTruthy();
});
it("renders token stats derived from the currently displayed agents", async () => {
render(<AgentsView addToast={mockAddToast} />);
await waitFor(() => {
expect(screen.getByText("Token Usage by Agent")).toBeTruthy();
});
expect(screen.getByText("Input Tokens")).toBeTruthy();
expect(screen.getByText("111")).toBeTruthy();
expect(screen.getByText("Output Tokens")).toBeTruthy();
expect(screen.getByText("26")).toBeTruthy();
expect(screen.getByText("Combined Tokens")).toBeTruthy();
expect(screen.getByText("137")).toBeTruthy();
const tokenRows = screen.getAllByRole("row");
expect(tokenRows[1]).toHaveTextContent("Test Agent 1");
expect(tokenRows[2]).toHaveTextContent("Test Agent 2");
});
it("passes projectId to agent fetches", async () => {
render(<AgentsView addToast={mockAddToast} projectId={projectId} />);
await waitFor(() => {
@@ -280,8 +308,13 @@ describe("AgentsView", () => {
it("keeps clickable identity area behavior for opening detail view", async () => {
render(<AgentsView addToast={mockAddToast} />);
const agentName = await screen.findByText("Test Agent 1");
const clickableIdentity = agentName.closest(".agent-info--clickable") as HTMLElement | null;
await waitFor(() => {
expect(screen.getAllByText("Test Agent 1").length).toBeGreaterThan(0);
});
const clickableIdentity = Array.from(document.querySelectorAll(".agent-info--clickable")).find((element) =>
element.textContent?.includes("Test Agent 1"),
) as HTMLElement | undefined;
expect(clickableIdentity).toBeTruthy();
fireEvent.click(clickableIdentity!);
@@ -912,7 +945,7 @@ describe("AgentsView", () => {
render(<AgentsView addToast={mockAddToast} projectId={projectId} />);
await waitFor(() => {
expect(screen.getByText("Test Agent 1")).toBeTruthy();
expect(screen.getAllByText("Test Agent 1").length).toBeGreaterThan(0);
});
expect(screen.queryByText("executor-FN-TEST")).toBeNull();
@@ -1220,7 +1253,7 @@ describe("AgentsView", () => {
expect(deleteButtons.length).toBeGreaterThanOrEqual(2);
});
expect(screen.getByText("Test Agent 4")).toBeTruthy();
expect(screen.getAllByText("Test Agent 4").length).toBeGreaterThan(0);
});
it("shows Delete button for terminated agents when explicitly filtered", async () => {
@@ -1232,7 +1265,7 @@ describe("AgentsView", () => {
fireEvent.change(filterSelect, { target: { value: "terminated" } });
await waitFor(() => {
expect(screen.getByText("Test Agent 4")).toBeTruthy();
expect(screen.getAllByText("Test Agent 4").length).toBeGreaterThan(0);
// Now we should see the Delete button for terminated agent
expect(screen.getAllByTitle("Delete").length).toBeGreaterThanOrEqual(1);
});
@@ -1268,7 +1301,7 @@ describe("AgentsView", () => {
fireEvent.change(filterSelect, { target: { value: "terminated" } });
await waitFor(() => {
expect(screen.getByText("Test Agent 4")).toBeTruthy();
expect(screen.getAllByText("Test Agent 4").length).toBeGreaterThan(0);
// Click the delete button for the terminated agent (agent-004)
const agentCards = document.querySelectorAll(".agent-card");
let terminatedCard: Element | null = null;
@@ -1299,7 +1332,7 @@ describe("AgentsView", () => {
fireEvent.change(filterSelect, { target: { value: "terminated" } });
await waitFor(() => {
expect(screen.getByText("Test Agent 4")).toBeTruthy();
expect(screen.getAllByText("Test Agent 4").length).toBeGreaterThan(0);
});
// Find the delete button for terminated agent (agent-004)

View File

@@ -65,6 +65,8 @@ const mockAgents: Agent[] = [
role: "executor" as AgentCapability,
state: "active" as AgentState,
taskId: "FN-101",
totalInputTokens: 60,
totalOutputTokens: 20,
lastHeartbeatAt: new Date().toISOString(),
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
@@ -75,6 +77,8 @@ const mockAgents: Agent[] = [
name: "Mobile Reviewer",
role: "reviewer" as AgentCapability,
state: "idle" as AgentState,
totalInputTokens: 15,
totalOutputTokens: 5,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
metadata: {},
@@ -132,6 +136,9 @@ describe("AgentsView mobile adaptations", () => {
await waitFor(() => {
expect(container.querySelector(".agent-list")).toBeTruthy();
expect(container.querySelectorAll(".agent-card").length).toBeGreaterThan(0);
expect(container.querySelector(".agent-token-stats-panel")).toBeTruthy();
expect(screen.getByText("Combined Tokens")).toBeTruthy();
expect(screen.getByText("100")).toBeTruthy();
});
});