Graph cost data drops the dead cluster-naming channel from the UI
The cluster namer is dead (owner refinement round, 2026-09-24): cluster labels are just session titles / file names now, coined without an LLM, so there is no reason to keep tracking naming spend in the cost UI. components/graph-view.tsx (UI-only; the core fold is untouched): - NEW pure export displayCostTotals(costs) = extraction + audit dollars and unpriced counts. Both the cost pill and the panel headline read THIS — never GraphCostSummary.totalDollars, which still includes the historical naming channel — so the two can never disagree. - GraphCostPanel's stage rows lose the "Cluster naming" line; the headline and unpriced counts come from displayCostTotals. - The recent-runs list filters out kind === "naming" runs entirely (the "N labels" tail died with them). - Mount comment updated: the fold covers chat + notes extraction runs. Note for master/Platform: src/core/cost-summary.ts still folds naming-runs.jsonl into stages.naming / totalDollars (Platform's lane). The UI no longer reads those fields; Platform can cull the seam and the dead ledger whenever convenient — displayCostTotals already isolates the display from whatever the core returns. Pinned in tests/graph-view-filters.test.ts: displayCostTotals with a naming stage of $9.99 + 2 unpriced proves the display sees extraction + audit only ($2.00, 1 unpriced). Live-verified on :3001 — the panel renders Extraction and Audit rows, no naming row, no naming runs in the recent list. 905/905.
This commit is contained in:
+51
-28
@@ -292,6 +292,23 @@ function fmtRunCost(dollars: number | null): string {
|
|||||||
return dollars === null ? "—" : `$${dollars.toFixed(3)}`;
|
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<GraphCostSummary, "stages">): {
|
||||||
|
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
|
* The graph-pipeline cost control (2026-09-20 placement wave): a bare
|
||||||
* cumulative-dollars number — no pill chrome (owner: "just a number"),
|
* 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"
|
open ? "text-foreground" : "text-muted-foreground hover:text-foreground"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
${costs.totalDollars.toFixed(2)}
|
${displayCostTotals(costs).totalDollars.toFixed(2)}
|
||||||
</button>
|
</button>
|
||||||
{open && (
|
{open && (
|
||||||
<div className="absolute right-0 top-full mt-2 max-h-[60vh] overflow-y-auto">
|
<div className="absolute right-0 top-full mt-2 max-h-[60vh] overflow-y-auto">
|
||||||
@@ -328,17 +345,23 @@ function GraphCostControl({ costs }: { costs: GraphCostSummary }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The cost control's expanded panel: stage totals (extraction / audit /
|
* The cost control's expanded panel: stage totals (extraction / audit —
|
||||||
* cluster-naming — dollars headline, tokens secondary) + the recent-runs
|
* dollars headline, tokens secondary) + the recent-runs list.
|
||||||
* list. Presentational: the fold happens server-side
|
* Presentational: the fold happens server-side (core/cost-summary.ts);
|
||||||
* (core/cost-summary.ts); dollars are inferred read-time, never stored.
|
* 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 }) {
|
function GraphCostPanel({ costs }: { costs: GraphCostSummary }) {
|
||||||
const stageRows: [string, GraphCostSummary["stages"]["extraction"]][] = [
|
const stageRows: [string, GraphCostSummary["stages"]["extraction"]][] = [
|
||||||
["Extraction", costs.stages.extraction],
|
["Extraction", costs.stages.extraction],
|
||||||
["Audit", costs.stages.audit],
|
["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 (
|
return (
|
||||||
<div className="w-80 max-w-[calc(100vw-1.5rem)] rounded-xl border bg-card/95 p-3 text-xs shadow-lg backdrop-blur">
|
<div className="w-80 max-w-[calc(100vw-1.5rem)] rounded-xl border bg-card/95 p-3 text-xs shadow-lg backdrop-blur">
|
||||||
<div className="mb-2 flex items-baseline justify-between">
|
<div className="mb-2 flex items-baseline justify-between">
|
||||||
@@ -346,9 +369,9 @@ function GraphCostPanel({ costs }: { costs: GraphCostSummary }) {
|
|||||||
Graph pipeline costs
|
Graph pipeline costs
|
||||||
</span>
|
</span>
|
||||||
<span className="text-foreground tabular-nums">
|
<span className="text-foreground tabular-nums">
|
||||||
${costs.totalDollars.toFixed(2)}
|
${totalDollars.toFixed(2)}
|
||||||
{costs.unpricedRuns > 0 && (
|
{unpricedRuns > 0 && (
|
||||||
<span className="text-muted-foreground"> · {costs.unpricedRuns} unpriced</span>
|
<span className="text-muted-foreground"> · {unpricedRuns} unpriced</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -370,24 +393,23 @@ function GraphCostPanel({ costs }: { costs: GraphCostSummary }) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 flex flex-col gap-0.5 border-t pt-2">
|
<div className="mt-2 flex flex-col gap-0.5 border-t pt-2">
|
||||||
{costs.runs.map((run) => (
|
{costs.runs
|
||||||
<div key={run.startedAt} className="flex items-baseline justify-between gap-2 text-[11px]">
|
.filter((run) => run.kind !== "naming")
|
||||||
<span className="min-w-0 flex-1 truncate text-muted-foreground">
|
.map((run) => (
|
||||||
<span className="text-foreground/70">{timeAgo(run.startedAt)}</span>{" "}
|
<div key={run.startedAt} className="flex items-baseline justify-between gap-2 text-[11px]">
|
||||||
{run.title}
|
<span className="min-w-0 flex-1 truncate text-muted-foreground">
|
||||||
{run.merged && (
|
<span className="text-foreground/70">{timeAgo(run.startedAt)}</span>{" "}
|
||||||
<span className="text-muted-foreground">
|
{run.title}
|
||||||
{" "}
|
{run.merged && (
|
||||||
+{run.merged.nodes}n
|
<span className="text-muted-foreground">
|
||||||
</span>
|
{" "}
|
||||||
)}
|
+{run.merged.nodes}n
|
||||||
{run.kind === "naming" && (
|
</span>
|
||||||
<span className="text-muted-foreground"> {run.labels ?? 0} labels</span>
|
)}
|
||||||
)}
|
</span>
|
||||||
</span>
|
<span className="shrink-0 tabular-nums">{fmtRunCost(run.dollars)}</span>
|
||||||
<span className="shrink-0 tabular-nums">{fmtRunCost(run.dollars)}</span>
|
</div>
|
||||||
</div>
|
))}
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -942,7 +964,8 @@ export function GraphView() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Graph pipeline costs (2026-09 cost metrics wave): the cumulative fold
|
// 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
|
// behind the bottom-left cost pill. Fetched once on mount and refetched
|
||||||
// when a pipeline run finishes (running -> none) so fresh spend lands
|
// when a pipeline run finishes (running -> none) so fresh spend lands
|
||||||
// without a manual reload. Best-effort: a fetch failure never blocks
|
// without a manual reload. Best-effort: a fetch failure never blocks
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import { describe, it } from "node:test";
|
import { describe, it } from "node:test";
|
||||||
import { isNodeVisible } from "../components/graph-view.tsx";
|
import { displayCostTotals, isNodeVisible } from "../components/graph-view.tsx";
|
||||||
|
|
||||||
const node = {
|
const node = {
|
||||||
name: "Money multiplier",
|
name: "Money multiplier",
|
||||||
@@ -37,3 +37,33 @@ describe("isNodeVisible (search predicate)", () => {
|
|||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user