tokentrace 0.19.0 → 0.19.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/app/api/export/route.ts +11 -9
  3. package/app/api/reports/route.ts +9 -46
  4. package/app/api/saved-views/[id]/route.ts +5 -1
  5. package/app/evidence/evidence-context-panel.tsx +40 -0
  6. package/app/evidence/evidence-page-data.ts +105 -0
  7. package/app/evidence/evidence-summary-cards.tsx +74 -0
  8. package/app/evidence/evidence-tables.tsx +185 -0
  9. package/app/evidence/evidence-workbench.tsx +101 -0
  10. package/app/evidence/page.tsx +47 -437
  11. package/app/guide/command-block.tsx +10 -0
  12. package/app/guide/guide-content.ts +103 -0
  13. package/app/guide/page.tsx +21 -531
  14. package/app/guide/section-title.tsx +22 -0
  15. package/app/guide/sections/agent-handoff-section.tsx +140 -0
  16. package/app/guide/sections/daily-loop-section.tsx +36 -0
  17. package/app/guide/sections/guide-nav.tsx +39 -0
  18. package/app/guide/sections/setup-status-section.tsx +47 -0
  19. package/app/guide/sections/start-section.tsx +52 -0
  20. package/app/guide/sections/status-line-section.tsx +79 -0
  21. package/app/guide/sections/troubleshooting-section.tsx +71 -0
  22. package/app/models/page.tsx +2 -2
  23. package/app/projects/page.tsx +1 -1
  24. package/app/tools/page.tsx +2 -2
  25. package/bin/tokentrace.js +19 -7
  26. package/components/charts/rank-bar-chart-lazy.tsx +3 -1
  27. package/components/charts/rank-bar-chart.tsx +4 -4
  28. package/components/charts/trend-chart.tsx +5 -3
  29. package/components/hooks/use-json-request.ts +46 -0
  30. package/components/overview/current-mix-panel.tsx +1 -1
  31. package/components/parser-debug/parser-overrides-panel.tsx +32 -30
  32. package/components/pricing/pricing-workflow.ts +6 -3
  33. package/components/repair/repair-guidance.tsx +2 -2
  34. package/components/repair-bulk-actions.tsx +14 -13
  35. package/components/repair-state-control.tsx +13 -17
  36. package/components/reports/saved-reports-panel.tsx +23 -25
  37. package/components/session-explorer/filters-section.tsx +152 -0
  38. package/components/session-explorer/saved-views-section.tsx +99 -0
  39. package/components/session-explorer/sessions-table.tsx +219 -0
  40. package/components/session-explorer/use-saved-views.ts +51 -0
  41. package/components/session-explorer/use-session-filters.ts +121 -0
  42. package/components/session-explorer.tsx +64 -464
  43. package/components/settings/custom-folders-section.tsx +4 -13
  44. package/components/settings/form-values.ts +18 -0
  45. package/components/settings/guardrails-section.tsx +22 -37
  46. package/components/settings/import-profiles-section.tsx +18 -26
  47. package/components/settings/scan-section.tsx +16 -27
  48. package/components/settings/types.ts +3 -0
  49. package/components/settings/use-folders-section.ts +24 -0
  50. package/components/settings/use-guardrails-section.ts +78 -0
  51. package/components/settings/use-import-profiles-section.ts +102 -0
  52. package/components/settings/use-scan-controls-section.ts +70 -0
  53. package/components/settings/use-scan-schedule-section.ts +33 -0
  54. package/components/settings/use-settings-status.ts +24 -0
  55. package/components/settings/use-storage-section.ts +12 -0
  56. package/components/settings-panel.tsx +38 -256
  57. package/components/sidebar.tsx +4 -2
  58. package/dist/cli/main.mjs +736 -0
  59. package/dist/runtime/agent.mjs +12 -6
  60. package/dist/runtime/anomalies.mjs +40 -32
  61. package/dist/runtime/digest.mjs +9 -4
  62. package/dist/runtime/doctor.mjs +3505 -3499
  63. package/dist/runtime/evidence.mjs +10 -12
  64. package/dist/runtime/insights.mjs +7 -4
  65. package/dist/runtime/mcp.mjs +9836 -43
  66. package/dist/runtime/pricing-refresh.mjs +7 -2
  67. package/dist/runtime/query.mjs +4 -0
  68. package/dist/runtime/repair.mjs +20 -16
  69. package/dist/runtime/report.mjs +534 -481
  70. package/dist/runtime/review.mjs +9 -4
  71. package/dist/runtime/scan.mjs +67 -42
  72. package/dist/runtime/status.mjs +4 -2
  73. package/package.json +5 -2
  74. package/scripts/agent.ts +1 -0
  75. package/scripts/anomalies.ts +2 -32
  76. package/scripts/build-cli-runtime.mjs +32 -19
  77. package/scripts/doctor.ts +5 -13
  78. package/scripts/evidence.ts +3 -12
  79. package/scripts/mcp.ts +2 -1
  80. package/scripts/repair.ts +3 -12
  81. package/scripts/report.ts +16 -37
  82. package/scripts/scan.ts +3 -28
  83. package/scripts/smoke-cli/runtime.mjs +6 -1
  84. package/server.json +2 -2
  85. package/src/cli/{commands.js → commands.ts} +39 -31
  86. package/src/cli/{context.js → context.ts} +35 -5
  87. package/src/cli/{help.js → help.ts} +2 -2
  88. package/src/cli/main.ts +12 -0
  89. package/src/cli/{runtime.js → runtime.ts} +37 -13
  90. package/src/cli/{serve.js → serve.ts} +36 -13
  91. package/src/ingestion/adapters/claude-code.ts +2 -1
  92. package/src/lib/analytics-query-helpers.ts +1 -1
  93. package/src/lib/anomaly-detection.ts +38 -2
  94. package/src/lib/csv.ts +4 -3
  95. package/src/lib/date-range.ts +2 -1
  96. package/src/lib/doctor.ts +15 -0
  97. package/src/lib/evidence-trail.ts +16 -0
  98. package/src/lib/mcp-server.ts +67 -24
  99. package/src/lib/overview-data.ts +1 -1
  100. package/src/lib/parser-overrides-cli.ts +1 -0
  101. package/src/lib/project-signals.ts +2 -2
  102. package/src/lib/provider-inference.ts +2 -2
  103. package/src/lib/report-cli.ts +2 -0
  104. package/src/lib/report-service.ts +99 -0
  105. package/src/lib/scan-cli.ts +61 -0
  106. package/src/lib/session-comparison.ts +4 -1
  107. package/src/lib/since-filter.ts +1 -0
  108. package/src/lib/status-cli.ts +4 -2
  109. package/src/lib/structured-query-cli.ts +1 -0
  110. package/src/lib/structured-query.ts +3 -0
  111. package/src/lib/unknown-cost-repair/auto-classify.ts +4 -2
  112. package/src/lib/unknown-cost-repair/keys.ts +9 -0
  113. package/src/lib/unknown-cost-repair/workbench.ts +13 -0
  114. package/src/lib/unknown-cost-repair.ts +3 -1
  115. package/tsconfig.json +3 -0
  116. package/src/cli/serve.d.ts +0 -32
package/CHANGELOG.md CHANGED
@@ -4,6 +4,77 @@ All notable changes to TokenTrace are documented here.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## [0.19.2] - 2026-06-05
8
+
9
+ ### Changed
10
+
11
+ - **MCP tools now run in-process.** The MCP server calls the same library
12
+ functions as the CLI and HTTP API instead of spawning a `tokentrace` CLI
13
+ subprocess per tool call, making tool calls faster and immune to nested
14
+ process-spawn timeouts. Payload shapes are unchanged. `run_scan` invoked over
15
+ MCP now records its agent action with surface `mcp` instead of `cli`.
16
+ `get_report` still shells out to the CLI because its report composition has
17
+ not yet moved into a shared library function.
18
+ - **Stricter compile-time safety.** TypeScript now runs with
19
+ `noUncheckedIndexedAccess`, `noImplicitOverride`, and
20
+ `noFallthroughCasesInSwitch`; ~190 unguarded array/record accesses across the
21
+ app, library, and tests were given explicit guards. ESLint additionally
22
+ enforces type-aware promise rules (`no-floating-promises`,
23
+ `no-misused-promises`, `await-thenable`). No behavior change intended.
24
+ - **Type-safe chart and CSV plumbing.** `RankBarChart` is generic over its row
25
+ type (chart keys are now checked against the data at compile time) and
26
+ `toCsv` accepts typed rows, removing all eight `as unknown as` casts from the
27
+ export route and analytics pages.
28
+
29
+ ### Fixed
30
+
31
+ - **MCP server reports invalid CLI JSON clearly.** If an underlying
32
+ `tokentrace` CLI call returns unparseable JSON, the MCP server now raises a
33
+ descriptive error naming the command instead of a bare `JSON.parse` failure.
34
+ - **`DELETE /api/saved-views/:id` validates the id.** A blank or
35
+ whitespace-only id now returns `400` instead of attempting a delete.
36
+ - **CSV export keeps caller input out of header syntax.** The
37
+ `content-disposition` filename for `/api/export` now strips characters
38
+ outside `[a-z0-9-]` from the `type` parameter instead of interpolating it
39
+ raw into a quoted string.
40
+ - **Settings scan flow no longer renders an error body as a scan result.**
41
+ A failed scan previously stored the `{error}` response where the last-scan
42
+ panel expected scan counts, which could crash the panel.
43
+
44
+ ### Internal
45
+
46
+ - **CLI wrapper is now strict TypeScript.** `src/cli/*` (≈800 lines,
47
+ previously plain JS invisible to the type checker) is typed and compiled to
48
+ `dist/cli/main.mjs` by the runtime build; `bin/tokentrace.js` loads the
49
+ compiled entry and falls back to `tsx` in dev checkouts.
50
+ - **One report engine.** `/api/reports` and `tokentrace report --type` now
51
+ share `src/lib/report-service.ts`; outputs verified byte-identical against
52
+ the previous implementations.
53
+ - **API routes are integration-tested.** 14 new test files (59 tests) cover
54
+ the previously untested HTTP routes — prices, settings, files, export,
55
+ import-profile-preview, saved-views, saved-reports items, repair-items,
56
+ evidence-pack, analytics, data, operating-metadata, reports — exercising
57
+ real handlers against seeded SQLite databases.
58
+ - **Tests-and-coverage CI.** A new `Tests` workflow runs the suite with v8
59
+ coverage, typecheck, and lint on every PR and push to main.
60
+ - **Frontend decomposition.** The four largest UI files were split into
61
+ focused modules with rendering verified byte-identical: guide page
62
+ (658→148 lines), evidence page (590→200), session explorer (549→149),
63
+ settings panel (341→123, props drilling reduced 49→12 scalars). A shared
64
+ `useJsonRequest` hook replaces the per-component fetch/pending/error
65
+ boilerplate across the client components.
66
+
67
+ ## [0.19.1] - 2026-06-04
68
+
69
+ ### Fixed
70
+
71
+ - **Leaner npm package.** The published tarball no longer includes a set of
72
+ non-product brand assets (~350 KB of images) that had been parked in the
73
+ `public/` directory and were unintentionally swept into the package by the
74
+ `files` allowlist. They are now excluded from the tarball and ignored by git.
75
+ No functional change — installs are simply smaller and contain only product
76
+ files.
77
+
7
78
  ## [0.19.0] - 2026-06-04
8
79
 
9
80
  ### Security
@@ -9,19 +9,21 @@ export async function GET(request: Request) {
9
9
  const type = url.searchParams.get("type") ?? "sessions";
10
10
  const analytics = getAnalyticsData();
11
11
  const debug = type.startsWith("scan-") ? getDebugData() : null;
12
- let rows: Array<Record<string, unknown>>;
13
12
 
14
- if (type === "scan-files") rows = (debug?.scanFiles ?? []) as Array<Record<string, unknown>>;
15
- else if (type === "scan-runs") rows = (debug?.scanRuns ?? []) as Array<Record<string, unknown>>;
16
- else if (type === "projects") rows = analytics.projects as unknown as Array<Record<string, unknown>>;
17
- else if (type === "models") rows = analytics.models as unknown as Array<Record<string, unknown>>;
18
- else if (type === "tools") rows = analytics.tools as unknown as Array<Record<string, unknown>>;
19
- else rows = analytics.sessions as unknown as Array<Record<string, unknown>>;
13
+ let csv: string;
14
+ if (type === "scan-files") csv = toCsv(debug?.scanFiles ?? []);
15
+ else if (type === "scan-runs") csv = toCsv(debug?.scanRuns ?? []);
16
+ else if (type === "projects") csv = toCsv(analytics.projects);
17
+ else if (type === "models") csv = toCsv(analytics.models);
18
+ else if (type === "tools") csv = toCsv(analytics.tools);
19
+ else csv = toCsv(analytics.sessions);
20
20
 
21
- return new NextResponse(toCsv(rows), {
21
+ // Keep the caller-supplied type out of header syntax: quoted-string safe.
22
+ const filenameType = type.replace(/[^a-z0-9-]/gi, "") || "sessions";
23
+ return new NextResponse(csv, {
22
24
  headers: {
23
25
  "content-type": "text/csv; charset=utf-8",
24
- "content-disposition": `attachment; filename="tokentrace-${type}.csv"`
26
+ "content-disposition": `attachment; filename="tokentrace-${filenameType}.csv"`
25
27
  }
26
28
  });
27
29
  }
@@ -1,69 +1,32 @@
1
1
  import { NextResponse } from "next/server";
2
- import { getAnalyticsData, getScanTrustData } from "@/src/lib/analytics";
3
- import { buildSourceCatalog, summarizeSourceCoverage } from "@/src/lib/source-catalog";
4
- import { buildSavedReportDefinitions, renderSavedReport, type SavedReportFormat } from "@/src/lib/saved-reports";
2
+ import { generateReport } from "@/src/lib/report-service";
3
+ import type { SavedReportFormat } from "@/src/lib/saved-reports";
5
4
 
6
5
  export const dynamic = "force-dynamic";
7
6
 
8
- function reportRows(definitionId: string) {
9
- const analytics = getAnalyticsData();
10
- const trust = getScanTrustData();
11
- const sourceCoverage = summarizeSourceCoverage(trust.scanFiles);
12
- if (definitionId === "source-coverage") {
13
- return [
14
- { label: "Native files", value: sourceCoverage.nativeFiles.toLocaleString(), detail: "First-class adapters" },
15
- { label: "Profile-assisted files", value: sourceCoverage.profileAssistedFiles.toLocaleString(), detail: "Import profile or generic parser" },
16
- { label: "Fallback files", value: sourceCoverage.fallbackFiles.toLocaleString(), detail: "Low-confidence text or generic fallback" },
17
- { label: "Imported records", value: sourceCoverage.importedRecords.toLocaleString(), detail: "Records imported from scan files" }
18
- ];
19
- }
20
- if (definitionId === "guardrail-status") {
21
- return [
22
- { label: "Cost guardrail", value: analytics.usageGuardrails.cost.status, detail: `${analytics.usageGuardrails.cost.used.toFixed(2)} used` },
23
- { label: "Token guardrail", value: analytics.usageGuardrails.tokens.status, detail: `${analytics.usageGuardrails.tokens.used.toLocaleString()} tokens used` },
24
- { label: "Scoped guardrails", value: analytics.usageGuardrails.scoped.length.toLocaleString(), detail: "Project/model/tool limits" },
25
- { label: "Anomalies", value: analytics.usageGuardrails.anomalies.length.toLocaleString(), detail: "Warning or exceeded scoped guardrails" }
26
- ];
27
- }
28
- return [
29
- { label: "Tokens", value: analytics.summary.totalTokens.toLocaleString(), detail: "Selected local data" },
30
- { label: "Cost", value: `$${analytics.summary.totalCost.toFixed(2)}`, detail: "Provider estimate or source cost" },
31
- { label: "Sessions", value: analytics.summary.sessions.toLocaleString(), detail: "Imported sessions" },
32
- { label: "Unknown cost", value: analytics.summary.unknownCostInteractions.toLocaleString(), detail: "Repair queue candidates" },
33
- { label: "Source catalog", value: buildSourceCatalog().entries.length.toLocaleString(), detail: "Known import paths" }
34
- ];
35
- }
36
-
37
7
  export async function GET(request: Request) {
38
8
  const url = new URL(request.url);
39
- const definitions = buildSavedReportDefinitions();
40
9
  const definitionId = url.searchParams.get("type") ?? "weekly-usage";
41
- const definition = definitions.find((item) => item.id === definitionId);
42
- if (!definition) return NextResponse.json({ error: "Unknown report type." }, { status: 400 });
43
10
  const format = (url.searchParams.get("format") ?? "json") as SavedReportFormat;
44
- if (!definition.formats.includes(format)) {
45
- return NextResponse.json({ error: "Unsupported report format." }, { status: 400 });
11
+ const result = generateReport(definitionId, { format });
12
+ if (!result.ok) {
13
+ const error = result.reason === "unknown-type" ? "Unknown report type." : "Unsupported report format.";
14
+ return NextResponse.json({ error }, { status: 400 });
46
15
  }
47
- const rendered = renderSavedReport({
48
- definitionId,
49
- format,
50
- generatedAt: new Date().toISOString(),
51
- rows: reportRows(definitionId)
52
- });
53
16
  if (format === "json") {
54
- return new NextResponse(rendered, {
17
+ return new NextResponse(result.content, {
55
18
  headers: { "content-type": "application/json; charset=utf-8" }
56
19
  });
57
20
  }
58
21
  if (format === "csv") {
59
- return new NextResponse(rendered, {
22
+ return new NextResponse(result.content, {
60
23
  headers: {
61
24
  "content-type": "text/csv; charset=utf-8",
62
25
  "content-disposition": `attachment; filename="tokentrace-${definitionId}.csv"`
63
26
  }
64
27
  });
65
28
  }
66
- return new NextResponse(rendered, {
29
+ return new NextResponse(result.content, {
67
30
  headers: {
68
31
  "content-type": "text/markdown; charset=utf-8",
69
32
  "content-disposition": `attachment; filename="tokentrace-${definitionId}.md"`
@@ -12,5 +12,9 @@ export async function DELETE(
12
12
  }
13
13
  ) {
14
14
  const { id } = await params;
15
- return NextResponse.json({ deleted: deleteSavedView(decodeURIComponent(id)) });
15
+ const viewId = decodeURIComponent(id).trim();
16
+ if (!viewId) {
17
+ return NextResponse.json({ error: "view id is required" }, { status: 400 });
18
+ }
19
+ return NextResponse.json({ deleted: deleteSavedView(viewId) });
16
20
  }
@@ -0,0 +1,40 @@
1
+ import Link from "next/link";
2
+ import { ArrowRight } from "lucide-react";
3
+ import { Card, CardContent } from "@/components/ui/card";
4
+ import { FieldLabel } from "@/components/ui/typography";
5
+
6
+ export type EvidenceContextAction = {
7
+ label: string;
8
+ detail: string;
9
+ href: string;
10
+ };
11
+
12
+ export function EvidenceContextPanel({ actions }: { actions: EvidenceContextAction[] }) {
13
+ return (
14
+ <Card>
15
+ <CardContent className="flex flex-col gap-3 p-3 lg:flex-row lg:items-center lg:justify-between">
16
+ <div className="max-w-[72ch]">
17
+ <FieldLabel>Evidence path</FieldLabel>
18
+ <p className="mt-1 text-sm leading-6 text-muted-foreground">
19
+ Evidence is a contextual drill-down from Overview, Sessions, Repair, and exported packs. If you opened this page directly, start with processed tokens, then pivot by metric or follow the next action that matches what looks incomplete.
20
+ </p>
21
+ </div>
22
+ <div className="grid min-w-0 gap-2 sm:grid-cols-2 xl:grid-cols-4">
23
+ {actions.map((action) => (
24
+ <Link
25
+ key={action.label}
26
+ href={action.href}
27
+ className="group rounded-md border bg-card p-3 text-left transition-colors hover:bg-muted/40 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
28
+ >
29
+ <span className="flex items-center gap-1.5 text-xs font-semibold text-foreground">
30
+ {action.label}
31
+ <ArrowRight className="h-3.5 w-3.5 text-primary transition-transform group-hover:translate-x-0.5" aria-hidden="true" />
32
+ </span>
33
+ <span className="mt-1 block text-xs leading-5 text-muted-foreground">{action.detail}</span>
34
+ </Link>
35
+ ))}
36
+ </div>
37
+ </CardContent>
38
+ </Card>
39
+ );
40
+ }
@@ -0,0 +1,105 @@
1
+ import { dateRangeQueryParams, mergeHrefParams, resolveDateRange } from "@/src/lib/date-range";
2
+ import { buildEvidenceTrail, parseEvidenceMetric } from "@/src/lib/evidence-trail";
3
+
4
+ export type EvidencePageSearchParams = Promise<Record<string, string | string[] | undefined>> | undefined;
5
+
6
+ export type EvidenceDrilldownAction = {
7
+ label: string;
8
+ detail: string;
9
+ href: string;
10
+ };
11
+
12
+ function firstSearchValue(value: string | string[] | undefined) {
13
+ return Array.isArray(value) ? value[0] : value;
14
+ }
15
+
16
+ function openedFromLabel(value: string | undefined) {
17
+ if (value === "overview") return "Overview";
18
+ if (value === "sessions") return "Sessions";
19
+ if (value === "repair") return "Repair";
20
+ if (value === "export") return "Evidence pack";
21
+ if (value === "settings") return "Settings";
22
+ return "Direct link";
23
+ }
24
+
25
+ function safeReturnTo(value: string | string[] | undefined, fallback: string) {
26
+ const candidate = firstSearchValue(value);
27
+ if (!candidate || !candidate.startsWith("/") || candidate.startsWith("//")) return fallback;
28
+ if (candidate.includes("\n") || candidate.includes("\r")) return fallback;
29
+ return candidate;
30
+ }
31
+
32
+ export async function getEvidencePageData(searchParams: EvidencePageSearchParams) {
33
+ const params = (await searchParams) ?? {};
34
+ const range = resolveDateRange(params);
35
+ const metric = parseEvidenceMetric(params?.metric);
36
+ const trail = buildEvidenceTrail({ metric, filters: range.filters });
37
+ const rangeLinkParams = dateRangeQueryParams(range);
38
+ const overviewHref = mergeHrefParams("/", rangeLinkParams);
39
+ const openedFrom = openedFromLabel(firstSearchValue(params.openedFrom));
40
+ const fallbackReturnHref =
41
+ openedFrom === "Sessions"
42
+ ? mergeHrefParams("/sessions", rangeLinkParams)
43
+ : openedFrom === "Repair"
44
+ ? mergeHrefParams("/repair", rangeLinkParams)
45
+ : overviewHref;
46
+ const returnHref = safeReturnTo(params.returnTo, fallbackReturnHref);
47
+ const evidenceContextParams = {
48
+ ...rangeLinkParams,
49
+ openedFrom: firstSearchValue(params.openedFrom) ?? "direct",
50
+ returnTo: returnHref
51
+ };
52
+ const currentEvidenceHref = mergeHrefParams(`/evidence?metric=${trail.metric}`, evidenceContextParams);
53
+ const pricingReturnParams = { returnTo: currentEvidenceHref };
54
+ const periodPreserveParams = {
55
+ metric: trail.metric,
56
+ openedFrom: firstSearchValue(params.openedFrom),
57
+ returnTo: firstSearchValue(params.returnTo)
58
+ };
59
+ const sessionsHref = mergeHrefParams("/sessions", rangeLinkParams);
60
+ const repairHref = mergeHrefParams("/repair", rangeLinkParams);
61
+ const modelRatesHref = mergeHrefParams("/pricing", pricingReturnParams);
62
+ const confidenceTotal = Math.max(1, trail.confidence.exact + trail.confidence.estimated + trail.confidence.unknown);
63
+ const leadingSource = trail.sourceFiles[0];
64
+ const leadingSession = trail.sessions[0];
65
+ const drilldownActions: EvidenceDrilldownAction[] = [
66
+ {
67
+ label: "Top source files",
68
+ detail: "Compare the local files contributing most to this metric.",
69
+ href: "#top-source-files"
70
+ },
71
+ {
72
+ label: "Largest sessions",
73
+ detail: "Open the session evidence table and continue into filtered sessions.",
74
+ href: "#session-evidence"
75
+ },
76
+ {
77
+ label: "Parser confidence",
78
+ detail: "Check whether parser status affects the imported records.",
79
+ href: leadingSource ? mergeHrefParams(leadingSource.parserHref, rangeLinkParams) : "/parser-debug"
80
+ },
81
+ {
82
+ label: "Set model rate",
83
+ detail: "Follow provider model rates or unknown-cost repair when cost needs review.",
84
+ href: leadingSession?.pricingHref
85
+ ? mergeHrefParams(leadingSession.pricingHref, pricingReturnParams)
86
+ : mergeHrefParams("/repair", rangeLinkParams)
87
+ }
88
+ ];
89
+
90
+ return {
91
+ range,
92
+ trail,
93
+ rangeLinkParams,
94
+ openedFrom,
95
+ returnHref,
96
+ evidenceContextParams,
97
+ pricingReturnParams,
98
+ periodPreserveParams,
99
+ sessionsHref,
100
+ repairHref,
101
+ modelRatesHref,
102
+ confidenceTotal,
103
+ drilldownActions
104
+ };
105
+ }
@@ -0,0 +1,74 @@
1
+ import { Badge } from "@/components/ui/badge";
2
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
3
+ import { DataValue, FieldLabel } from "@/components/ui/typography";
4
+ import type { EvidenceTrail } from "@/src/lib/evidence-trail";
5
+ import { formatCurrency, formatExactTokens } from "@/src/lib/format";
6
+
7
+ export function MetricTotalsCard({ totals }: { totals: EvidenceTrail["totals"] }) {
8
+ return (
9
+ <Card>
10
+ <CardHeader>
11
+ <CardTitle>Metric Totals</CardTitle>
12
+ <CardDescription>
13
+ Totals use the same filtered metric definition as the session evidence below.
14
+ </CardDescription>
15
+ </CardHeader>
16
+ <CardContent className="p-0">
17
+ <div className="grid divide-y border-t sm:grid-cols-2 sm:divide-x sm:divide-y-0 lg:grid-cols-5">
18
+ <div className="p-3">
19
+ <FieldLabel>Tokens</FieldLabel>
20
+ <DataValue className="mt-1" size="md">{formatExactTokens(totals.tokens)}</DataValue>
21
+ </div>
22
+ <div className="p-3">
23
+ <FieldLabel>Cost</FieldLabel>
24
+ <DataValue className="mt-1" size="md">{formatCurrency(totals.cost)}</DataValue>
25
+ </div>
26
+ <div className="p-3">
27
+ <FieldLabel>Sessions</FieldLabel>
28
+ <DataValue className="mt-1" size="md">{totals.sessions.toLocaleString()}</DataValue>
29
+ </div>
30
+ <div className="p-3">
31
+ <FieldLabel>Interactions</FieldLabel>
32
+ <DataValue className="mt-1" size="md">{totals.interactions.toLocaleString()}</DataValue>
33
+ </div>
34
+ <div className="p-3">
35
+ <FieldLabel>Unknown Cost</FieldLabel>
36
+ <DataValue className="mt-1" size="md">{totals.unknownCostInteractions.toLocaleString()}</DataValue>
37
+ </div>
38
+ </div>
39
+ </CardContent>
40
+ </Card>
41
+ );
42
+ }
43
+
44
+ export function ConfidenceSplitCard({
45
+ confidence,
46
+ confidenceTotal
47
+ }: {
48
+ confidence: EvidenceTrail["confidence"];
49
+ confidenceTotal: number;
50
+ }) {
51
+ return (
52
+ <Card>
53
+ <CardHeader>
54
+ <CardTitle>Confidence Split</CardTitle>
55
+ <CardDescription>Interaction-level token confidence for this metric and period.</CardDescription>
56
+ </CardHeader>
57
+ <CardContent className="space-y-3">
58
+ {[
59
+ { label: "Exact", value: confidence.exact, variant: "success" as const },
60
+ { label: "Estimated", value: confidence.estimated, variant: "secondary" as const },
61
+ { label: "Unknown", value: confidence.unknown, variant: "warning" as const }
62
+ ].map((item) => (
63
+ <div key={item.label} className="grid grid-cols-[5.5rem_minmax(0,1fr)_4rem] items-center gap-3 text-sm">
64
+ <Badge variant={item.variant}>{item.label}</Badge>
65
+ <div className="h-2 overflow-hidden rounded-full bg-muted">
66
+ <div className="h-full rounded-full bg-primary" style={{ width: `${(item.value / confidenceTotal) * 100}%` }} />
67
+ </div>
68
+ <div className="text-right tabular-nums text-muted-foreground">{item.value.toLocaleString()}</div>
69
+ </div>
70
+ ))}
71
+ </CardContent>
72
+ </Card>
73
+ );
74
+ }
@@ -0,0 +1,185 @@
1
+ import Link from "next/link";
2
+ import { ArrowRight } from "lucide-react";
3
+ import { Badge } from "@/components/ui/badge";
4
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
5
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
6
+ import { MonoText } from "@/components/ui/typography";
7
+ import { mergeHrefParams } from "@/src/lib/date-range";
8
+ import type { EvidenceTrail, EvidenceTrailSession } from "@/src/lib/evidence-trail";
9
+ import { formatCurrency, formatExactTokens, percent } from "@/src/lib/format";
10
+
11
+ function confidenceVariant(value: string) {
12
+ if (value === "exact") return "success";
13
+ if (value === "unknown") return "warning";
14
+ return "secondary";
15
+ }
16
+
17
+ function parserStatusVariant(value: string | null) {
18
+ if (value === "imported") return "success";
19
+ if (value === "imported_with_errors") return "warning";
20
+ if (!value) return "secondary";
21
+ return "outline";
22
+ }
23
+
24
+ export function TopSourceFilesCard({
25
+ sourceFiles,
26
+ rangeLinkParams
27
+ }: {
28
+ sourceFiles: EvidenceTrail["sourceFiles"];
29
+ rangeLinkParams: Record<string, string | undefined>;
30
+ }) {
31
+ return (
32
+ <Card id="top-source-files">
33
+ <CardHeader>
34
+ <CardTitle>Top Source Files</CardTitle>
35
+ <CardDescription>Largest contributing local files for the same metric definition.</CardDescription>
36
+ </CardHeader>
37
+ <CardContent className="table-scroll">
38
+ <Table>
39
+ <TableHeader>
40
+ <TableRow>
41
+ <TableHead>Source</TableHead>
42
+ <TableHead>Tokens</TableHead>
43
+ <TableHead>Interactions</TableHead>
44
+ <TableHead>Unknown cost</TableHead>
45
+ <TableHead>Actions</TableHead>
46
+ </TableRow>
47
+ </TableHeader>
48
+ <TableBody>
49
+ {sourceFiles.length ? (
50
+ sourceFiles.map((source) => (
51
+ <TableRow key={source.sourceFile}>
52
+ <TableCell className="max-w-96">
53
+ <Link href={mergeHrefParams(source.sourceHref, rangeLinkParams)} title={source.sourceFile}>
54
+ <MonoText className="block truncate text-muted-foreground underline-offset-4 hover:underline">
55
+ {source.sourceFile}
56
+ </MonoText>
57
+ </Link>
58
+ </TableCell>
59
+ <TableCell>{formatExactTokens(source.tokens)}</TableCell>
60
+ <TableCell>{source.interactions.toLocaleString()}</TableCell>
61
+ <TableCell>{source.unknownCostInteractions.toLocaleString()}</TableCell>
62
+ <TableCell>
63
+ <div className="flex flex-wrap gap-2">
64
+ <Link href={mergeHrefParams(source.sourceHref, rangeLinkParams)} className="font-medium text-primary underline-offset-4 hover:underline">
65
+ Open Sessions
66
+ </Link>
67
+ <Link href={mergeHrefParams(source.parserHref, rangeLinkParams)} className="font-medium text-muted-foreground underline-offset-4 hover:underline">
68
+ Review parser
69
+ </Link>
70
+ </div>
71
+ </TableCell>
72
+ </TableRow>
73
+ ))
74
+ ) : (
75
+ <TableRow>
76
+ <TableCell colSpan={5} className="h-20 text-center text-muted-foreground">
77
+ No source-file evidence is available for this metric yet.
78
+ </TableCell>
79
+ </TableRow>
80
+ )}
81
+ </TableBody>
82
+ </Table>
83
+ </CardContent>
84
+ </Card>
85
+ );
86
+ }
87
+
88
+ export function SessionEvidenceCard({
89
+ sessions,
90
+ rangeLinkParams,
91
+ pricingReturnParams
92
+ }: {
93
+ sessions: EvidenceTrailSession[];
94
+ rangeLinkParams: Record<string, string | undefined>;
95
+ pricingReturnParams: Record<string, string | undefined>;
96
+ }) {
97
+ return (
98
+ <Card id="session-evidence">
99
+ <CardHeader>
100
+ <CardTitle>Session, Source, Parser, And Model Rate Evidence</CardTitle>
101
+ <CardDescription>
102
+ The table is capped at the top 100 contributing sessions; totals above include the full metric set.
103
+ </CardDescription>
104
+ </CardHeader>
105
+ <CardContent className="table-scroll p-0">
106
+ <Table>
107
+ <TableHeader>
108
+ <TableRow>
109
+ <TableHead>Session</TableHead>
110
+ <TableHead>Metric Total</TableHead>
111
+ <TableHead>Cost</TableHead>
112
+ <TableHead>Source</TableHead>
113
+ <TableHead>Parser</TableHead>
114
+ <TableHead>Model rates</TableHead>
115
+ <TableHead>Confidence</TableHead>
116
+ </TableRow>
117
+ </TableHeader>
118
+ <TableBody>
119
+ {sessions.length ? (
120
+ sessions.map((session) => (
121
+ <TableRow key={session.id}>
122
+ <TableCell className="min-w-64">
123
+ <Link href={mergeHrefParams(session.sessionHref, rangeLinkParams)} className="font-medium text-primary underline-offset-4 hover:underline">
124
+ {session.title}
125
+ </Link>
126
+ <div className="mt-1 text-xs text-muted-foreground">
127
+ {session.tool} / {session.provider} / {session.project}
128
+ </div>
129
+ </TableCell>
130
+ <TableCell>{formatExactTokens(session.totalTokens)}</TableCell>
131
+ <TableCell>{formatCurrency(session.cost)}</TableCell>
132
+ <TableCell className="max-w-80">
133
+ <Link href={mergeHrefParams(session.sourceHref, rangeLinkParams)} title={session.sourceFile}>
134
+ <MonoText className="block truncate text-muted-foreground underline-offset-4 hover:underline">
135
+ {session.sourceFile}
136
+ </MonoText>
137
+ </Link>
138
+ </TableCell>
139
+ <TableCell className="min-w-44">
140
+ <div className="flex flex-wrap items-center gap-2">
141
+ <Badge variant={parserStatusVariant(session.parserStatus)}>
142
+ {session.parserStatus ?? "not scanned"}
143
+ </Badge>
144
+ <Link href={mergeHrefParams(session.parserHref, rangeLinkParams)} className="text-xs font-medium text-primary underline-offset-4 hover:underline">
145
+ Parser <ArrowRight className="inline h-3.5 w-3.5" />
146
+ </Link>
147
+ </div>
148
+ <div className="mt-1 text-xs text-muted-foreground">
149
+ {session.parser ?? "No parser"} / {session.parserConfidence == null ? "confidence unknown" : percent(session.parserConfidence)}
150
+ </div>
151
+ </TableCell>
152
+ <TableCell className="min-w-44">
153
+ {session.pricingHref ? (
154
+ <Link href={mergeHrefParams(session.pricingHref, pricingReturnParams)} className="font-medium text-primary underline-offset-4 hover:underline">
155
+ {session.model}
156
+ </Link>
157
+ ) : (
158
+ <span>{session.model}</span>
159
+ )}
160
+ </TableCell>
161
+ <TableCell>
162
+ <div className="flex flex-col items-start gap-1">
163
+ <Badge variant={confidenceVariant(session.tokenConfidence)}>
164
+ {session.tokenConfidence}
165
+ </Badge>
166
+ <span className="text-xs text-muted-foreground">
167
+ {session.interactions.toLocaleString()} interactions
168
+ </span>
169
+ </div>
170
+ </TableCell>
171
+ </TableRow>
172
+ ))
173
+ ) : (
174
+ <TableRow>
175
+ <TableCell colSpan={7} className="h-24 text-center text-muted-foreground">
176
+ No evidence is available for this metric yet.
177
+ </TableCell>
178
+ </TableRow>
179
+ )}
180
+ </TableBody>
181
+ </Table>
182
+ </CardContent>
183
+ </Card>
184
+ );
185
+ }