FN-7117: show all token models in Command Center

Show full-fidelity model token breakdowns in Command Center while keeping Overview compact.

- Remove the Tokens detail chart cap so bars, pies, and tables include every analytics model bucket.
- Keep Overview model summaries explicitly limited to the top consumers with a named cap.
- Add dashboard coverage and docs for full Tokens attribution versus Overview summaries.
- Add a patch changeset for the published CLI bundle.

Files changed:
 .changeset/fn-7117-tokens-by-model.md              |  7 ++
 docs/dashboard-guide.md                            |  5 +-
 .../components/command-center/CommandCenter.tsx    |  8 ++-
 .../__tests__/CommandCenter.test.tsx               | 78 +++++++++++++++++++++-
 .../components/command-center/areas/TokensArea.tsx | 38 ++++++-----
 .../areas/__tests__/TokensArea.test.tsx            | 68 +++++++++++++++++++
 6 files changed, 182 insertions(+), 22 deletions(-)

Fusion-Task-Id: FN-7117

Fusion-Task-Lineage: 7bf4c090-1054-40d0-97bf-e0e495077af5

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-06-27 10:34:15 -07:00
parent c61217e71a
commit 368f1e0e45
6 changed files with 182 additions and 22 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Show every used model in Command Center token-by-model detail charts.
category: fix
dev: Removes the Tokens detail chart cap while keeping Overview explicitly top-N.

View File

@@ -844,11 +844,12 @@ Features:
<!-- FNXC:GlobalConcurrencyControls 2026-06-26-00:00: The Command Center Concurrency card mirrors the footer concurrency popover by showing read-only running-agent counts and current-use markers for the shared global cap and current-project max-concurrent slider. --> <!-- FNXC:GlobalConcurrencyControls 2026-06-26-00:00: The Command Center Concurrency card mirrors the footer concurrency popover by showing read-only running-agent counts and current-use markers for the shared global cap and current-project max-concurrent slider. -->
<!-- FNXC:GlobalConcurrencyControls 2026-06-26-18:35: Those running-agent counts include in-progress executors and active triage planners (`triage` + `planning`, not paused) because both consume global concurrency slots. --> <!-- FNXC:GlobalConcurrencyControls 2026-06-26-18:35: Those running-agent counts include in-progress executors and active triage planners (`triage` + `planning`, not paused) because both consume global concurrency slots. -->
<!-- FNXC:CommandCenter 2026-06-26-00:00: The four Overview Concurrency sliders change live scheduler capacity, so each settled edit opens a confirmation popup before persisting; cancel, backdrop, or Escape leaves the previous persisted value in place. --> <!-- FNXC:CommandCenter 2026-06-26-00:00: The four Overview Concurrency sliders change live scheduler capacity, so each settled edit opens a confirmation popup before persisting; cancel, backdrop, or Escape leaves the previous persisted value in place. -->
<!-- FNXC:CommandCenter 2026-06-27-10:03: Tokens detail charts must show every model bucket returned by analytics for accurate spend attribution; Overview remains a compact top-model summary because its copy explicitly frames those cards as top consumers/share. -->
- **Overview controls dashboard** sits at the top of the Overview landing surface on desktop and mobile. It includes AI engine stop/start backed by `globalPause`, live scheduler status from executor stats, the shared Global Max Concurrent slider backed by `/api/global-concurrency`, range sliders for `maxConcurrent`, `maxTriageConcurrent`, and `maxWorktrees` that persist through `/api/settings`, and a compact theme dropdown with the same color-chip swatches and Shadcn variant list as Settings → Appearance. The four concurrency sliders ask for confirmation after a changed value settles; confirming persists the new cap, while cancel, backdrop, or Escape dismissal reverts to the last persisted value without saving. The global and current-project max-concurrent sliders show running-agent counts plus a current-use dot on the track once utilization data loads; triage and worktree sliders remain cap-only. These controls reuse existing APIs and App-level theme setters; they do not add a new backend route or second theme owner. - **Overview controls dashboard** sits at the top of the Overview landing surface on desktop and mobile. It includes AI engine stop/start backed by `globalPause`, live scheduler status from executor stats, the shared Global Max Concurrent slider backed by `/api/global-concurrency`, range sliders for `maxConcurrent`, `maxTriageConcurrent`, and `maxWorktrees` that persist through `/api/settings`, and a compact theme dropdown with the same color-chip swatches and Shadcn variant list as Settings → Appearance. The four concurrency sliders ask for confirmation after a changed value settles; confirming persists the new cap, while cancel, backdrop, or Escape dismissal reverts to the last persisted value without saving. The global and current-project max-concurrent sliders show running-agent counts plus a current-use dot on the track once utilization data loads; triage and worktree sliders remain cap-only. These controls reuse existing APIs and App-level theme setters; they do not add a new backend route or second theme owner.
- **Overview** summarizes token usage/cost, autonomy, active nodes, sessions, agent runs, tasks done, model breadth, and real open signals, and includes the SDLC throughput funnel for the selected range at the bottom of the Overview content in loading, error, empty, and populated states. Its token total and Live activity snapshot token metric refresh on a bounded live cadence and animate number changes while preserving reduced-motion preferences. The sessions card uses the selected-range `ActivityAnalytics.sessions` value already loaded for the overview. The Live activity snapshot also shows the current board-state count for tasks in progress, independent of the selected analytics date range. Overview includes a graph-rich software-factory snapshot with the existing tokens-by-model bar, tool-category bar, real recharts token-share pie, and the daily activity multi-series line chart placed before the daily activity sparkline/trend so the richer line graph sits higher in the chart grid. These reuse the already-loaded tokens, tools, activity, and signals analytics; the signals count comes from `/api/command-center/signals` and renders unavailable (`—`) while the incidents-backed response is loading or unavailable. The chart reveal/glow accents are decorative and disabled when reduced-motion preferences are active. The SDLC completion rate is shown as a radial gauge and is calculated as cohort conversion from in-range triage entrants, so the rate is capped at 100% even when older tasks finish during the range. - **Overview** summarizes token usage/cost, autonomy, active nodes, sessions, agent runs, tasks done, model breadth, and real open signals, and includes the SDLC throughput funnel for the selected range at the bottom of the Overview content in loading, error, empty, and populated states. Its token total and Live activity snapshot token metric refresh on a bounded live cadence and animate number changes while preserving reduced-motion preferences. The sessions card uses the selected-range `ActivityAnalytics.sessions` value already loaded for the overview. The Live activity snapshot also shows the current board-state count for tasks in progress, independent of the selected analytics date range. Overview includes a graph-rich software-factory snapshot with the existing top-model-consumers bar, tool-category bar, top-model token-share pie, and the daily activity multi-series line chart placed before the daily activity sparkline/trend so the richer line graph sits higher in the chart grid. These reuse the already-loaded tokens, tools, activity, and signals analytics; the signals count comes from `/api/command-center/signals` and renders unavailable (`—`) while the incidents-backed response is loading or unavailable. The chart reveal/glow accents are decorative and disabled when reduced-motion preferences are active. The SDLC completion rate is shown as a radial gauge and is calculated as cohort conversion from in-range triage entrants, so the rate is capped at 100% even when older tasks finish during the range.
<!-- FNXC:CommandCenter 2026-06-21-00:00: Command Center cost must read as an estimated, derived value from recorded token counts and the hand-maintained model pricing map; it is never persisted, and the UI must surface prices-as-of, stale low-confidence, and unavailable unknown-model states instead of implying billing truth. --> <!-- FNXC:CommandCenter 2026-06-21-00:00: Command Center cost must read as an estimated, derived value from recorded token counts and the hand-maintained model pricing map; it is never persisted, and the UI must surface prices-as-of, stale low-confidence, and unavailable unknown-model states instead of implying billing truth. -->
<!-- FNXC:CommandCenter 2026-06-22-00:00: FN-6876 requires user-maintained/LiteLLM-fetched pricing overrides to feed Tokens and Team estimates immediately without implying provider billing reconciliation. --> <!-- FNXC:CommandCenter 2026-06-22-00:00: FN-6876 requires user-maintained/LiteLLM-fetched pricing overrides to feed Tokens and Team estimates immediately without implying provider billing reconciliation. -->
- **Tokens** breaks down token totals, estimated cost, tasks, and per-model usage. Per-model and per-provider breakdowns use the task's analytics-only actually-used model snapshot when available, so usage from settings-resolved runs appears under the real runtime model instead of `(unknown)` without changing future model resolution; estimated cost uses the same snapshot-first, legacy-fallback model identity so those resolved runs price normally when the model is in the pricing table. Estimated cost is derived at read time from recorded token counts multiplied by the effective per-model pricing table: Settings → Global Models pricing overrides win first, then the built-in fallback table is used. It is not persisted, so historical rows stay tied to current maintained prices instead of stale stored billing truth. The Tokens area shows a **prices as of** date/source for the effective table, marks pricing older than the staleness threshold as low-confidence, and shows cost unavailable for models with no pricing entry rather than guessing a price. It includes the existing token-usage-over-time chart, an additive recharts multi-series line graph, and a token-share pie backed by the same grouped token analytics; use the granularity control to switch the time-series request between hourly, daily, and weekly buckets. The token total and charts poll on a bounded cadence, keep the previous data visible during refresh, animate decorative count/bar transitions, and disable those animations for reduced-motion users. - **Tokens** breaks down token totals, estimated cost, tasks, and per-model usage. Per-model and per-provider breakdowns use the task's analytics-only actually-used model snapshot when available, so usage from settings-resolved runs appears under the real runtime model instead of `(unknown)` without changing future model resolution; estimated cost uses the same snapshot-first, legacy-fallback model identity so those resolved runs price normally when the model is in the pricing table. Estimated cost is derived at read time from recorded token counts multiplied by the effective per-model pricing table: Settings → Global Models pricing overrides win first, then the built-in fallback table is used. It is not persisted, so historical rows stay tied to current maintained prices instead of stale stored billing truth. The Tokens area shows a **prices as of** date/source for the effective table, marks pricing older than the staleness threshold as low-confidence, and shows cost unavailable for models with no pricing entry rather than guessing a price. It includes the existing token-usage-over-time chart, an additive recharts multi-series line graph, a full token-by-model bar, and a token-share pie backed by every grouped model returned by token analytics; use the granularity control to switch the time-series request between hourly, daily, and weekly buckets. The token total and charts poll on a bounded cadence, keep the previous data visible during refresh, animate decorative count/bar transitions, and disable those animations for reduced-motion users.
- **Tools** shows autonomy ratio, tool-call volume, intervention counts, sessions, and tool categories. The area keeps the existing category bar and adds a recharts category-share pie from `ToolAnalytics.byCategory`. There is intentionally no tools line chart yet because `ToolAnalytics` does not expose a per-day tool trend; the dashboard does not fabricate one or call a new endpoint. - **Tools** shows autonomy ratio, tool-call volume, intervention counts, sessions, and tool categories. The area keeps the existing category bar and adds a recharts category-share pie from `ToolAnalytics.byCategory`. There is intentionally no tools line chart yet because `ToolAnalytics` does not expose a per-day tool trend; the dashboard does not fabricate one or call a new endpoint.
- **Activity** tracks sessions, messages, active nodes, active agents, agent heartbeat runs, and stickiness. Agent-run sheets show total, active, completed, and failed runs for the selected range, and the Agent runs/day sparkline trends runs by `agentRuns.startedAt`. The area keeps the existing live animated line charts for messages/day, active agents/day, active nodes/day, and combined throughput/day (`messages + active agents + active nodes`), and adds a recharts multi-series line graph for messages, active agents, and agent runs plus an agent-run outcome pie from the existing `agentRuns` split. These charts reuse the existing activity analytics endpoint, refresh on a bounded 15-second cadence while mounted, keep the previous data visible during refreshes, and disable decorative draw-on motion for reduced-motion users. - **Activity** tracks sessions, messages, active nodes, active agents, agent heartbeat runs, and stickiness. Agent-run sheets show total, active, completed, and failed runs for the selected range, and the Agent runs/day sparkline trends runs by `agentRuns.startedAt`. The area keeps the existing live animated line charts for messages/day, active agents/day, active nodes/day, and combined throughput/day (`messages + active agents + active nodes`), and adds a recharts multi-series line graph for messages, active agents, and agent runs plus an agent-run outcome pie from the existing `agentRuns` split. These charts reuse the existing activity analytics endpoint, refresh on a bounded 15-second cadence while mounted, keep the previous data visible during refreshes, and disable decorative draw-on motion for reduced-motion users.
- **Productivity** separates outcome counters (commits and pull requests), task-duration stats, and volume proxies such as modified files, lines changed, and files by language. The task-duration block counts done tasks completed in the selected range and shows average, median, p90, and total active execution time from `cumulativeActiveMs`; when no qualifying duration data exists, duration values render the unavailable `—` sentinel rather than `0`. The Lines changed card includes **Preview LOC backfill**, an explicit operator control for historical commit-association diff stats. Preview runs the project-scoped backfill in dry-run mode by default and reports scanned rows, distinct commits, updated rows, skipped unavailable commits, and skipped invalid SHAs without writing; **Apply backfill** appears after a preview and requires danger confirmation before persisting additions/deletions to `task_commit_associations`, then renders the same counts as an applied report. It keeps the files-by-language bar and adds a language-share pie from `ProductivityAnalytics.byLanguage`. There is intentionally no productivity line chart because the current productivity response has no per-day throughput or completion time series; no new endpoint is called. - **Productivity** separates outcome counters (commits and pull requests), task-duration stats, and volume proxies such as modified files, lines changed, and files by language. The task-duration block counts done tasks completed in the selected range and shows average, median, p90, and total active execution time from `cumulativeActiveMs`; when no qualifying duration data exists, duration values render the unavailable `—` sentinel rather than `0`. The Lines changed card includes **Preview LOC backfill**, an explicit operator control for historical commit-association diff stats. Preview runs the project-scoped backfill in dry-run mode by default and reports scanned rows, distinct commits, updated rows, skipped unavailable commits, and skipped invalid SHAs without writing; **Apply backfill** appears after a preview and requires danger confirmation before persisting additions/deletions to `task_commit_associations`, then renders the same counts as an applied report. It keeps the files-by-language bar and adds a language-share pie from `ProductivityAnalytics.byLanguage`. There is intentionally no productivity line chart because the current productivity response has no per-day throughput or completion time series; no new endpoint is called.

View File

@@ -49,6 +49,12 @@ interface SubView {
label: string; label: string;
} }
/*
FNXC:CommandCenter 2026-06-27-09:45:
Overview remains an intentionally compact top-consumers summary because its chart copy promises a top-N snapshot, while the Tokens detail area shows every used model. Keep the bar and pie on this same capped source so the two Overview cards never disagree about which models are summarized.
*/
const OVERVIEW_TOKEN_MODEL_LIMIT = 8;
/* /*
FNXC:CommandCenter 2026-06-18-16:57: FNXC:CommandCenter 2026-06-18-16:57:
Team tab shows each agent's tokens/cost/files-changed/tasks-completed with live status and bar charts, reusing existing analytics primitives; GitHub-issue per-agent stats are FN-6653, not here. Team tab shows each agent's tokens/cost/files-changed/tasks-completed with live status and bar charts, reusing existing analytics primitives; GitHub-issue per-agent stats are FN-6653, not here.
@@ -182,7 +188,7 @@ function OverviewTab({
() => () =>
[...(tokens.data?.groups ?? [])] [...(tokens.data?.groups ?? [])]
.sort((a, b) => b.totalTokens - a.totalTokens || (a.key ?? "").localeCompare(b.key ?? "")) .sort((a, b) => b.totalTokens - a.totalTokens || (a.key ?? "").localeCompare(b.key ?? ""))
.slice(0, 8) .slice(0, OVERVIEW_TOKEN_MODEL_LIMIT)
.map((g) => { .map((g) => {
const label = g.key ?? t("commandCenter.tokens.unknownModel", "(unknown)"); const label = g.key ?? t("commandCenter.tokens.unknownModel", "(unknown)");
return { return {

View File

@@ -48,6 +48,22 @@ function providerIconIn(element: HTMLElement, provider: string): Element | null
return element.querySelector(`.provider-icon[data-provider="${provider}"]`); return element.querySelector(`.provider-icon[data-provider="${provider}"]`);
} }
function makeTokenGroup(key: string | null, totalTokens: number) {
const inputTokens = Math.round(totalTokens * 0.6);
const outputTokens = Math.round(totalTokens * 0.3);
const cachedTokens = totalTokens - inputTokens - outputTokens;
return {
key,
inputTokens,
outputTokens,
cachedTokens,
cacheWriteTokens: 0,
totalTokens,
nTasks: 1,
cost: { usd: key === null ? null : totalTokens / 1_000, unavailable: key === null, stale: false },
};
}
function tokenFixture(totalTokens = 1_500) { function tokenFixture(totalTokens = 1_500) {
return { return {
from: "2026-06-08", from: "2026-06-08",
@@ -90,6 +106,40 @@ function tokenFixture(totalTokens = 1_500) {
}; };
} }
function manyModelTokenFixture() {
const groups = [
makeTokenGroup("model-01", 2_000),
makeTokenGroup("model-02", 1_900),
makeTokenGroup("model-03", 1_800),
makeTokenGroup("model-04", 1_700),
makeTokenGroup("model-05", 1_600),
makeTokenGroup("model-06", 1_500),
makeTokenGroup("model-07", 1_400),
makeTokenGroup("model-08", 1_300),
makeTokenGroup("model-09", 1_200),
makeTokenGroup(null, 1_100),
makeTokenGroup("(unknown)", 1_000),
];
const totals = groups.reduce(
(acc, group) => ({
inputTokens: acc.inputTokens + group.inputTokens,
outputTokens: acc.outputTokens + group.outputTokens,
cachedTokens: acc.cachedTokens + group.cachedTokens,
cacheWriteTokens: acc.cacheWriteTokens + group.cacheWriteTokens,
totalTokens: acc.totalTokens + group.totalTokens,
nTasks: acc.nTasks + group.nTasks,
}),
{ inputTokens: 0, outputTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, totalTokens: 0, nTasks: 0 },
);
return {
...tokenFixture(totals.totalTokens),
totals,
cost: { usd: null, unavailable: true, stale: false },
groups,
};
}
function toolsFixture(toolCalls = 30) { function toolsFixture(toolCalls = 30) {
return { return {
from: "2026-06-08", from: "2026-06-08",
@@ -535,6 +585,27 @@ describe("CommandCenter shell", () => {
expectThroughputLastAfter("command-center-stat-tokens", "command-center-live-strip", "command-center-overview-charts"); expectThroughputLastAfter("command-center-stat-tokens", "command-center-live-strip", "command-center-overview-charts");
}); });
it("keeps Overview token charts as an accurate top-model summary sourced from the same cap", async () => {
mockOverviewApi({ tokens: manyModelTokenFixture() });
render(<CommandCenter />);
const overviewTokenChart = await screen.findByTestId("command-center-overview-chart-tokens");
const overviewPie = screen.getByTestId("cc-overview-pie");
expect(screen.getByTestId("command-center-stat-models")).toHaveTextContent("11");
expect(within(overviewTokenChart).getByText("Top model token consumers in this range")).toBeTruthy();
expect(within(overviewPie).getByText("Top model token share in this range")).toBeTruthy();
// FNXC:CommandCenter 2026-06-27-09:55: Overview intentionally remains top-8; this locks the design decision that the bar and pie summarize the same leading models while the unique-model card still exposes the full analytics count.
for (const label of ["model-01", "model-02", "model-03", "model-04", "model-05", "model-06", "model-07", "model-08"]) {
expect(within(overviewTokenChart).getByText(label)).toBeTruthy();
expect(within(overviewPie).getByText(label)).toBeTruthy();
}
for (const label of ["model-09", "(unknown)"]) {
expect(within(overviewTokenChart).queryByText(label)).toBeNull();
expect(within(overviewPie).queryByText(label)).toBeNull();
}
});
it("renders Overview providerless and unknown model icons without touching pie labels", async () => { it("renders Overview providerless and unknown model icons without touching pie labels", async () => {
mockOverviewApi({ mockOverviewApi({
tokens: { tokens: {
@@ -855,7 +926,12 @@ describe("CommandCenter shell", () => {
await screen.findByTestId("command-center-empty"); await screen.findByTestId("command-center-empty");
expect(screen.queryByTestId("command-center-stat-tokens")).toBeNull(); expect(screen.queryByTestId("command-center-stat-tokens")).toBeNull();
expect(apiMock.mock.calls.some(([path]) => typeof path === "string" && path === "/command-center/tools")).toBe(true); // FNXC:CommandCenter 2026-06-27-09:55: The All-time preset now serializes an explicit upper bound, so range-change coverage should assert the tools endpoint refetched for the open-lower-bound range rather than requiring the legacy bare path.
expect(
apiMock.mock.calls.some(
([path]) => typeof path === "string" && path.startsWith("/command-center/tools?to=") && !path.includes("from="),
),
).toBe(true);
}); });
it("exposes the ARIA tabs pattern (tablist + tabs + tabpanel)", () => { it("exposes the ARIA tabs pattern (tablist + tabs + tabpanel)", () => {

View File

@@ -32,6 +32,9 @@ const GRANULARITIES: TokenTimeGranularity[] = ["hour", "day", "week"];
/* /*
FNXC:CommandCenterCharts 2026-06-18-23:20: FNXC:CommandCenterCharts 2026-06-18-23:20:
The Tokens surface must add real pie and line charts from already-fetched token analytics only. Keep the existing bars, tables, granularity controls, loading/error/empty branches, and testids intact while the FN-6682 recharts wrappers provide token-themed, non-finite-safe visuals. The Tokens surface must add real pie and line charts from already-fetched token analytics only. Keep the existing bars, tables, granularity controls, loading/error/empty branches, and testids intact while the FN-6682 recharts wrappers provide token-themed, non-finite-safe visuals.
FNXC:CommandCenter 2026-06-27-09:45:
The Tokens detail area is the full-fidelity by-model source of truth: every group returned by model token analytics must appear in the bar chart, pie chart, and per-model table. Do not cap these detail charts; Overview owns any intentional top-N summarization.
*/ */
function costSortValue(cost: CostResult): number { function costSortValue(cost: CostResult): number {
@@ -121,30 +124,29 @@ export function TokensArea({ range }: { range: DateRange }) {
const sortedGroups = useMemo(() => sortGroups(groups, sortKey, sortDir), [groups, sortKey, sortDir]); const sortedGroups = useMemo(() => sortGroups(groups, sortKey, sortDir), [groups, sortKey, sortDir]);
const fullModelGroupsByTokens = useMemo(
() => [...groups].sort((a, b) => b.totalTokens - a.totalTokens || (a.key ?? "").localeCompare(b.key ?? "")),
[groups],
);
const barData = useMemo( const barData = useMemo(
() => () =>
[...groups] fullModelGroupsByTokens.map((g) => ({
.sort((a, b) => b.totalTokens - a.totalTokens) label: modelGroupDisplayLabel(g, t("commandCenter.tokens.unknownModel", "(unknown)")),
.slice(0, 12) value: g.totalTokens,
.map((g) => ({ valueLabel: formatCount(g.totalTokens),
label: modelGroupDisplayLabel(g, t("commandCenter.tokens.unknownModel", "(unknown)")), iconProvider: modelGroupIconProvider(g),
value: g.totalTokens, })),
valueLabel: formatCount(g.totalTokens), [fullModelGroupsByTokens, t],
iconProvider: modelGroupIconProvider(g),
})),
[groups, t],
); );
const pieData = useMemo( const pieData = useMemo(
() => () =>
[...groups] fullModelGroupsByTokens.map((g) => ({
.sort((a, b) => b.totalTokens - a.totalTokens) label: modelGroupDisplayLabel(g, t("commandCenter.tokens.unknownModel", "(unknown)")),
.slice(0, 12) value: g.totalTokens,
.map((g) => ({ })),
label: modelGroupDisplayLabel(g, t("commandCenter.tokens.unknownModel", "(unknown)")), [fullModelGroupsByTokens, t],
value: g.totalTokens,
})),
[groups, t],
); );
const lineSeries = useMemo( const lineSeries = useMemo(

View File

@@ -17,6 +17,22 @@ vi.mock("../../../ProviderIcon", () => ({
const range7d: DateRange = { from: "2026-06-08", to: null, preset: "7d" }; const range7d: DateRange = { from: "2026-06-08", to: null, preset: "7d" };
function makeTokenGroup(key: string | null, totalTokens: number) {
const inputTokens = Math.round(totalTokens * 0.6);
const outputTokens = Math.round(totalTokens * 0.3);
const cachedTokens = totalTokens - inputTokens - outputTokens;
return {
key,
inputTokens,
outputTokens,
cachedTokens,
cacheWriteTokens: 0,
totalTokens,
nTasks: 1,
cost: { usd: key === null ? null : totalTokens / 1_000, unavailable: key === null, stale: false },
};
}
function tokenFixture() { function tokenFixture() {
return { return {
from: "2026-06-08", from: "2026-06-08",
@@ -67,6 +83,34 @@ function tokenFixture() {
}; };
} }
function manyModelTokenFixture() {
const groups = [
makeTokenGroup("claude-sonnet-4-5", 2_000),
makeTokenGroup("gpt-4o-mini", 1_900),
...Array.from({ length: 13 }, (_, index) => makeTokenGroup(`model-${String(index + 1).padStart(2, "0")}`, 1_800 - index * 100)),
makeTokenGroup(null, 150),
makeTokenGroup("(unknown)", 125),
];
const totals = groups.reduce(
(acc, group) => ({
inputTokens: acc.inputTokens + group.inputTokens,
outputTokens: acc.outputTokens + group.outputTokens,
cachedTokens: acc.cachedTokens + group.cachedTokens,
cacheWriteTokens: acc.cacheWriteTokens + group.cacheWriteTokens,
totalTokens: acc.totalTokens + group.totalTokens,
nTasks: acc.nTasks + group.nTasks,
}),
{ inputTokens: 0, outputTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, totalTokens: 0, nTasks: 0 },
);
return {
...tokenFixture(),
totals,
cost: { usd: null, unavailable: true, stale: false },
groups,
};
}
beforeEach(() => { beforeEach(() => {
apiMock.mockReset(); apiMock.mockReset();
apiMock.mockResolvedValue(tokenFixture()); apiMock.mockResolvedValue(tokenFixture());
@@ -107,4 +151,28 @@ describe("TokensArea provider model icons", () => {
expect(screen.getByTestId("cc-tokens-pie")).toHaveTextContent("gpt-4o-mini"); expect(screen.getByTestId("cc-tokens-pie")).toHaveTextContent("gpt-4o-mini");
expect(screen.getByTestId("cc-tokens-pie")).toHaveTextContent("(unknown)"); expect(screen.getByTestId("cc-tokens-pie")).toHaveTextContent("(unknown)");
}); });
it("renders every analytics model group in detail bar, pie, and table even beyond the old cap", async () => {
apiMock.mockResolvedValue(manyModelTokenFixture());
render(<TokensArea range={range7d} />);
const byModelChart = await screen.findByRole("list", { name: "Tokens by model" });
const pie = screen.getByTestId("cc-tokens-pie");
const table = screen.getByTestId("cc-tokens-table");
const expectedLabels = [
"claude-sonnet-4-5",
"gpt-4o-mini",
...Array.from({ length: 13 }, (_, index) => `model-${String(index + 1).padStart(2, "0")}`),
"(unknown)",
];
// FNXC:CommandCenter 2026-06-27-09:55: Symptom verification for FN-7117 uses more than the former 12-row detail cap plus null/literal unknown labels, so any reintroduced bar/pie truncation drops model-13 or one of the unknown buckets here.
for (const label of expectedLabels) {
expect(within(byModelChart).getAllByText(label).length).toBeGreaterThan(0);
expect(within(pie).getAllByText(label).length).toBeGreaterThan(0);
expect(within(table).getAllByText(label).length).toBeGreaterThan(0);
}
expect(screen.getByTestId("cc-tokens-row-unknown")).toHaveTextContent("(unknown)");
expect(screen.getByTestId("cc-tokens-row-(unknown)")).toHaveTextContent("(unknown)");
});
}); });