diff --git a/components/graph-view.tsx b/components/graph-view.tsx index ba71be4..16ab806 100644 --- a/components/graph-view.tsx +++ b/components/graph-view.tsx @@ -292,6 +292,23 @@ function fmtRunCost(dollars: number | null): string { return dollars === null ? "—" : `$${dollars.toFixed(3)}`; } +/** + * The cost figures the UI DISPLAYS (owner refinement round, 2026-09-24): + * extraction + audit only. Cluster naming is dead — labels are session + * titles / file names — so the core's naming fold (and its historical + * spend) is deliberately not shown anywhere: the pill and the panel must + * agree, and both read THIS, never GraphCostSummary.totalDollars. + */ +export function displayCostTotals(costs: Pick): { + totalDollars: number; + unpricedRuns: number; +} { + return { + totalDollars: costs.stages.extraction.dollars + costs.stages.audit.dollars, + unpricedRuns: costs.stages.extraction.unpricedRuns + costs.stages.audit.unpricedRuns, + }; +} + /** * The graph-pipeline cost control (2026-09-20 placement wave): a bare * cumulative-dollars number — no pill chrome (owner: "just a number"), @@ -316,7 +333,7 @@ function GraphCostControl({ costs }: { costs: GraphCostSummary }) { open ? "text-foreground" : "text-muted-foreground hover:text-foreground" }`} > - ${costs.totalDollars.toFixed(2)} + ${displayCostTotals(costs).totalDollars.toFixed(2)} {open && (
@@ -328,17 +345,23 @@ function GraphCostControl({ costs }: { costs: GraphCostSummary }) { } /** - * The cost control's expanded panel: stage totals (extraction / audit / - * cluster-naming — dollars headline, tokens secondary) + the recent-runs - * list. Presentational: the fold happens server-side - * (core/cost-summary.ts); dollars are inferred read-time, never stored. + * The cost control's expanded panel: stage totals (extraction / audit — + * dollars headline, tokens secondary) + the recent-runs list. + * Presentational: the fold happens server-side (core/cost-summary.ts); + * dollars are inferred read-time, never stored. Cluster naming is GONE + * from the panel (owner refinement round, 2026-09-24): labels are session + * titles / file names now, the namer is dead, and its historical spend is + * not tracked anymore — the headline sums the displayed stages only and + * naming-kind runs never reach the list. */ function GraphCostPanel({ costs }: { costs: GraphCostSummary }) { const stageRows: [string, GraphCostSummary["stages"]["extraction"]][] = [ ["Extraction", costs.stages.extraction], ["Audit", costs.stages.audit], - ["Cluster naming", costs.stages.naming], ]; + // The displayed figures come from displayCostTotals (naming excluded — + // the pill and this panel must agree). + const { totalDollars, unpricedRuns } = displayCostTotals(costs); return (
@@ -346,9 +369,9 @@ function GraphCostPanel({ costs }: { costs: GraphCostSummary }) { Graph pipeline costs - ${costs.totalDollars.toFixed(2)} - {costs.unpricedRuns > 0 && ( - · {costs.unpricedRuns} unpriced + ${totalDollars.toFixed(2)} + {unpricedRuns > 0 && ( + · {unpricedRuns} unpriced )}
@@ -370,24 +393,23 @@ function GraphCostPanel({ costs }: { costs: GraphCostSummary }) { ))}
- {costs.runs.map((run) => ( -
- - {timeAgo(run.startedAt)}{" "} - {run.title} - {run.merged && ( - - {" "} - +{run.merged.nodes}n - - )} - {run.kind === "naming" && ( - {run.labels ?? 0} labels - )} - - {fmtRunCost(run.dollars)} -
- ))} + {costs.runs + .filter((run) => run.kind !== "naming") + .map((run) => ( +
+ + {timeAgo(run.startedAt)}{" "} + {run.title} + {run.merged && ( + + {" "} + +{run.merged.nodes}n + + )} + + {fmtRunCost(run.dollars)} +
+ ))}
); @@ -942,7 +964,8 @@ export function GraphView() { }, []); // Graph pipeline costs (2026-09 cost metrics wave): the cumulative fold - // of every usage channel (chat + notes extraction runs, cluster naming) + // of the usage channels (chat + notes extraction runs; the dead namer's + // channel is not displayed anymore, owner 2026-09-24) // behind the bottom-left cost pill. Fetched once on mount and refetched // when a pipeline run finishes (running -> none) so fresh spend lands // without a manual reload. Best-effort: a fetch failure never blocks diff --git a/tests/graph-view-filters.test.ts b/tests/graph-view-filters.test.ts index b31ec5b..f984885 100644 --- a/tests/graph-view-filters.test.ts +++ b/tests/graph-view-filters.test.ts @@ -5,7 +5,7 @@ import assert from "node:assert/strict"; import { describe, it } from "node:test"; -import { isNodeVisible } from "../components/graph-view.tsx"; +import { displayCostTotals, isNodeVisible } from "../components/graph-view.tsx"; const node = { name: "Money multiplier", @@ -36,4 +36,34 @@ describe("isNodeVisible (search predicate)", () => { }); -}); \ No newline at end of file +}); + +describe("displayCostTotals (naming-free cost fold)", () => { + const stage = (dollars: number, unpricedRuns = 0) => ({ + runs: dollars > 0 ? 1 : 0, + dollars, + unpricedRuns, + tokens: { + inputTokens: 0, + outputTokens: 0, + cacheReadTokens: 0, + cacheWriteTokens: 0, + reasoningTokens: 0, + }, + }); + const costs = { + stages: { + extraction: stage(1.25, 1), + audit: stage(0.75), + naming: stage(9.99, 2), // dead channel — must never reach the display + }, + } as const; + + it("sums extraction + audit only — the dead namer's spend is invisible", () => { + assert.equal(displayCostTotals(costs).totalDollars, 2); + }); + + it("unpriced counts fold the same two stages", () => { + assert.equal(displayCostTotals(costs).unpricedRuns, 1); + }); +});