tokentrace 0.7.0 → 0.8.0

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 (59) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +12 -30
  3. package/app/api/repair-items/route.ts +81 -0
  4. package/app/diagnostics/page.tsx +140 -1
  5. package/app/evidence/page.tsx +170 -0
  6. package/app/globals.css +9 -3
  7. package/app/layout.tsx +3 -2
  8. package/app/page.tsx +50 -39
  9. package/app/repair/page.tsx +199 -0
  10. package/bin/tokentrace.js +28 -4
  11. package/components/period-filter.tsx +29 -10
  12. package/components/repair-state-control.tsx +109 -0
  13. package/components/ui/typography.tsx +3 -3
  14. package/dist/runtime/db-migrate.mjs +74 -1
  15. package/dist/runtime/db-seed.mjs +74 -1
  16. package/dist/runtime/digest.mjs +110 -13
  17. package/dist/runtime/doctor.mjs +368 -47
  18. package/dist/runtime/evidence.mjs +781 -0
  19. package/dist/runtime/insights.mjs +108 -11
  20. package/dist/runtime/pricing-refresh.mjs +74 -1
  21. package/dist/runtime/repair.mjs +863 -0
  22. package/dist/runtime/reset.mjs +74 -1
  23. package/dist/runtime/scan.mjs +74 -1
  24. package/dist/runtime/status.mjs +678 -533
  25. package/docs/assets/doctor-parser-trust-0.8.0.png +0 -0
  26. package/docs/assets/evidence-0.8.0.png +0 -0
  27. package/docs/assets/overview-0.8.0.png +0 -0
  28. package/docs/assets/repair-0.8.0.png +0 -0
  29. package/package.json +1 -1
  30. package/scripts/build-cli-runtime.mjs +2 -0
  31. package/scripts/doctor.ts +3 -1
  32. package/scripts/evidence.ts +92 -0
  33. package/scripts/repair.ts +57 -0
  34. package/scripts/smoke-cli.mjs +60 -0
  35. package/scripts/status.ts +8 -9
  36. package/src/db/migrate-core.ts +76 -0
  37. package/src/db/schema.ts +16 -0
  38. package/src/lib/analytics.ts +18 -3
  39. package/src/lib/claude-statusline.ts +202 -0
  40. package/src/lib/doctor.ts +19 -3
  41. package/src/lib/evidence-trail.ts +309 -0
  42. package/src/lib/live-status.ts +7 -205
  43. package/src/lib/parser-trust.ts +188 -0
  44. package/src/lib/scan-diff.ts +204 -0
  45. package/src/lib/unknown-cost-repair.ts +532 -0
  46. package/docs/assets/diagnostics.png +0 -0
  47. package/docs/assets/discovery.png +0 -0
  48. package/docs/assets/doctor-0.6.0.png +0 -0
  49. package/docs/assets/mobile-overview.png +0 -0
  50. package/docs/assets/overview-0.6.0.png +0 -0
  51. package/docs/assets/overview-0.7.0.png +0 -0
  52. package/docs/assets/overview.png +0 -0
  53. package/docs/assets/pricing.png +0 -0
  54. package/docs/assets/projects-0.7.0.png +0 -0
  55. package/docs/assets/session-explorer.png +0 -0
  56. package/docs/assets/sessions-0.7.0.png +0 -0
  57. package/docs/assets/settings-guardrails-0.7.0.png +0 -0
  58. package/docs/assets/settings-package-trust-0.6.0.png +0 -0
  59. package/docs/assets/usage-intelligence-0.7.0.png +0 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,28 @@
2
2
 
3
3
  All notable changes to TokenTrace are documented here.
4
4
 
5
+ ## [0.8.0] - 2026-05-12
6
+
7
+ ### Added
8
+
9
+ - Evidence detail pages and `tokentrace evidence --json` for tracing metric totals back to sessions, source files, parser status, and pricing context.
10
+ - Unknown Cost Repair workbench and `tokentrace repair --json` for grouped local repair state, alias hints, parser review links, and pricing follow-up.
11
+ - Parser Trust Report and Scan History Diff panels in Diagnostics for latest scan parser coverage and scan-to-scan import changes.
12
+
13
+ ### Changed
14
+
15
+ - Overview metric cards now link major totals to evidence trails and route unknown cost work to Unknown Cost Repair.
16
+ - Overview now places Token Trend and Cost Trend directly after Usage Pulse and the metric cards, with Monthly Guardrails and Recommended Next Actions below the charts.
17
+ - Dense evidence, repair, parser trust, and scan diff tables preserve horizontal scrolling and stable source-path truncation.
18
+ - Evidence and repair copy now states local-first behavior, support-file ignores, and parser-review requirements for unsupported files.
19
+ - README screenshots now use public-safe synthetic Evidence + Repair views, and obsolete screenshot assets were removed from the package payload.
20
+
21
+ ### Fixed
22
+
23
+ - Overview custom period date fields now use an intentionally inset calendar icon while preserving native `type="date"` submission fields and the single-line desktop toolbar.
24
+ - The app shell now constrains page width on small screens so wide toolbars scroll internally instead of widening the whole page.
25
+ - `tokentrace statusline setup claude` and piped Claude status-line input no longer touch the TokenTrace app database or start the dashboard.
26
+
5
27
  ## [0.7.0] - 2026-05-12
6
28
 
7
29
  ### Added
package/README.md CHANGED
@@ -8,7 +8,7 @@ Local-first analytics for AI CLI usage. TokenTrace scans local CLI logs, normali
8
8
 
9
9
  TokenTrace is designed for local development machines first, with macOS-oriented defaults. It does not require a cloud account and does not send telemetry or logs anywhere.
10
10
 
11
- ![TokenTrace 0.7.0 overview dashboard](docs/assets/overview-0.7.0.png)
11
+ ![TokenTrace overview dashboard](docs/assets/overview-0.8.0.png)
12
12
 
13
13
  ## Start In Seconds
14
14
 
@@ -37,6 +37,10 @@ tokentrace serve --port 3210 --no-open
37
37
  tokentrace scan # Scan local AI CLI usage logs
38
38
  tokentrace doctor --json
39
39
  # Inspect scan health and repair recommendations
40
+ tokentrace evidence --json
41
+ # Print metric evidence trails as JSON
42
+ tokentrace repair --json
43
+ # Print unknown-cost repair groups as JSON
40
44
  tokentrace digest --json
41
45
  # Print current-month local usage digest
42
46
  tokentrace insights --json
@@ -184,6 +188,8 @@ tokentrace statusline claude
184
188
 
185
189
  Claude Code sends session JSON to the command on stdin. TokenTrace reads the transcript path, model, context usage, and session cost, then prints one compact local line:
186
190
 
191
+ Do not set the Claude Code `statusLine.command` to plain `tokentrace`. Plain `tokentrace` starts the dashboard, while `tokentrace statusline claude` prints exactly one status-line response.
192
+
187
193
  ![TokenTrace Claude Code status line](docs/assets/claude-statusline.svg)
188
194
 
189
195
  You can also inspect the same local status outside Claude Code:
@@ -197,21 +203,15 @@ Codex CLI status-line integration is intentionally deferred until its status-lin
197
203
 
198
204
  ## Screenshots
199
205
 
200
- Usage Intelligence views from `0.7.0`:
201
-
202
- ![TokenTrace 0.7.0 overview dashboard](docs/assets/overview-0.7.0.png)
203
-
204
- ![TokenTrace 0.7.0 Usage Intelligence review queue](docs/assets/usage-intelligence-0.7.0.png)
205
-
206
- ![TokenTrace 0.7.0 session comparison flags](docs/assets/sessions-0.7.0.png)
206
+ Evidence + Repair views:
207
207
 
208
- ![TokenTrace 0.7.0 project signals](docs/assets/projects-0.7.0.png)
208
+ ![TokenTrace overview dashboard](docs/assets/overview-0.8.0.png)
209
209
 
210
- ![TokenTrace 0.7.0 local guardrail settings](docs/assets/settings-guardrails-0.7.0.png)
210
+ ![TokenTrace processed tokens evidence trail](docs/assets/evidence-0.8.0.png)
211
211
 
212
- Stable Daily Tool views from `0.6.0`:
212
+ ![TokenTrace unknown cost repair queue](docs/assets/repair-0.8.0.png)
213
213
 
214
- ![TokenTrace 0.6.0 Scan Doctor](docs/assets/doctor-0.6.0.png)
214
+ ![TokenTrace Scan Doctor parser trust report](docs/assets/doctor-parser-trust-0.8.0.png)
215
215
 
216
216
  CLI startup and help:
217
217
 
@@ -225,24 +225,6 @@ Optional wrapper diagnostics:
225
225
 
226
226
  ![TokenTrace wrapper command](docs/assets/cli-wrapper.gif)
227
227
 
228
- Session exploration:
229
-
230
- ![TokenTrace session explorer](docs/assets/session-explorer.png)
231
-
232
- Scan Doctor and file discovery:
233
-
234
- ![TokenTrace ingestion diagnostics](docs/assets/diagnostics.png)
235
-
236
- ![TokenTrace file discovery](docs/assets/discovery.png)
237
-
238
- Editable model pricing:
239
-
240
- ![TokenTrace pricing configuration](docs/assets/pricing.png)
241
-
242
- Mobile overview:
243
-
244
- ![TokenTrace mobile overview](docs/assets/mobile-overview.png)
245
-
246
228
  ## Privacy Model
247
229
 
248
230
  - All processing runs locally on your machine.
@@ -0,0 +1,81 @@
1
+ import { NextResponse } from "next/server";
2
+ import {
3
+ buildUnknownCostRepairWorkbench,
4
+ getUnknownCostReview,
5
+ saveUnknownCostReview,
6
+ type UnknownCostRepairStatus
7
+ } from "@/src/lib/unknown-cost-repair";
8
+
9
+ export const dynamic = "force-dynamic";
10
+
11
+ const reviewStates = new Set<UnknownCostRepairStatus>([
12
+ "unresolved",
13
+ "ignored",
14
+ "resolved",
15
+ "needs-parser-review"
16
+ ]);
17
+
18
+ function text(value: unknown, maxLength: number) {
19
+ return typeof value === "string" ? value.trim().slice(0, maxLength) : "";
20
+ }
21
+
22
+ function reviewState(value: unknown): UnknownCostRepairStatus | null {
23
+ return typeof value === "string" && reviewStates.has(value as UnknownCostRepairStatus)
24
+ ? (value as UnknownCostRepairStatus)
25
+ : null;
26
+ }
27
+
28
+ function workbenchGroupForKey(key: string) {
29
+ return buildUnknownCostRepairWorkbench().groups.find((group) => group.key === key) ?? null;
30
+ }
31
+
32
+ export async function GET() {
33
+ return NextResponse.json(buildUnknownCostRepairWorkbench());
34
+ }
35
+
36
+ export async function PUT(request: Request) {
37
+ const body = await request.json();
38
+ const key = text(body.key, 1000);
39
+ const status = reviewState(body.status ?? body.state);
40
+
41
+ if (!key) {
42
+ return NextResponse.json({ error: "key is required" }, { status: 400 });
43
+ }
44
+
45
+ if (!status) {
46
+ return NextResponse.json({ error: "status must be unresolved, ignored, resolved, or needs-parser-review" }, { status: 400 });
47
+ }
48
+
49
+ const group = workbenchGroupForKey(key);
50
+ const existing = group ? null : getUnknownCostReview(key);
51
+ if (!group && (!existing || existing.updatedAt == null)) {
52
+ return NextResponse.json({ error: "repair key was not found in current workbench evidence" }, { status: 404 });
53
+ }
54
+
55
+ const metadata = {
56
+ sourceFile: "",
57
+ model: "",
58
+ cause: ""
59
+ };
60
+ if (group) {
61
+ metadata.sourceFile = group.sourceFile;
62
+ metadata.model = group.model;
63
+ metadata.cause = group.cause;
64
+ } else if (existing) {
65
+ metadata.sourceFile = existing.sourceFile;
66
+ metadata.model = existing.model;
67
+ metadata.cause = existing.cause;
68
+ }
69
+ const review = saveUnknownCostReview({
70
+ key,
71
+ status,
72
+ notes: text(body.notes ?? body.note, 500),
73
+ sourceFile: metadata.sourceFile,
74
+ model: metadata.model,
75
+ cause: metadata.cause
76
+ });
77
+
78
+ return NextResponse.json({ review });
79
+ }
80
+
81
+ export const PATCH = PUT;
@@ -2,6 +2,7 @@ import Link from "next/link";
2
2
  import { AlertTriangle, ArrowRight, CheckCircle2, CircleDashed } from "lucide-react";
3
3
  import { Badge } from "@/components/ui/badge";
4
4
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
5
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
5
6
  import { DataValue, FieldLabel, MonoText, PageHeader } from "@/components/ui/typography";
6
7
  import { ScanHealthSummary } from "@/components/scan-health-summary";
7
8
  import { getAnalyticsData, getScanTrustData, type DebugScanRun } from "@/src/lib/analytics";
@@ -136,6 +137,7 @@ function TrustChecklist({
136
137
  function DoctorReportPanel({ report }: { report: DoctorReport }) {
137
138
  const statusRows = [
138
139
  ["Imported", report.fileStatus.imported],
140
+ ["With errors", report.fileStatus.importedWithErrors],
139
141
  ["Duplicates", report.fileStatus.duplicates],
140
142
  ["Ignored", report.fileStatus.ignored],
141
143
  ["Unsupported", report.fileStatus.unsupported],
@@ -192,7 +194,7 @@ function DoctorReportPanel({ report }: { report: DoctorReport }) {
192
194
  <div className="grid gap-6 xl:grid-cols-[1fr_1fr]">
193
195
  <div className="space-y-3">
194
196
  <div className="mb-3 text-sm font-semibold">File handling</div>
195
- <div className="grid border-y sm:grid-cols-5 sm:divide-x xl:grid-cols-2">
197
+ <div className="grid border-y sm:grid-cols-6 sm:divide-x xl:grid-cols-2">
196
198
  {statusRows.map(([label, value]) => (
197
199
  <div key={label} className="p-2">
198
200
  <FieldLabel>{label}</FieldLabel>
@@ -261,6 +263,139 @@ function DoctorReportPanel({ report }: { report: DoctorReport }) {
261
263
  );
262
264
  }
263
265
 
266
+ function ParserTrustPanel({ report }: { report: DoctorReport["parserTrust"] }) {
267
+ return (
268
+ <Card>
269
+ <CardHeader>
270
+ <CardTitle>Parser Trust Report</CardTitle>
271
+ <CardDescription>
272
+ Latest scan files grouped by parser, source family, version, status, and import yield. Ignored files are known support files, not usage transcripts. Unsupported files need parser review before they become usage.
273
+ </CardDescription>
274
+ </CardHeader>
275
+ <CardContent className="table-scroll">
276
+ {report.parsers.length ? (
277
+ <Table className="min-w-[72rem]">
278
+ <TableHeader>
279
+ <TableRow>
280
+ <TableHead>Parser</TableHead>
281
+ <TableHead>Version</TableHead>
282
+ <TableHead>Source</TableHead>
283
+ <TableHead className="text-right">Imported</TableHead>
284
+ <TableHead className="text-right">With errors</TableHead>
285
+ <TableHead className="text-right">Ignored</TableHead>
286
+ <TableHead className="text-right">Unsupported</TableHead>
287
+ <TableHead className="text-right">Failed</TableHead>
288
+ <TableHead className="text-right">Duplicate</TableHead>
289
+ <TableHead className="text-right">Records</TableHead>
290
+ <TableHead className="min-w-56">Latest reason</TableHead>
291
+ </TableRow>
292
+ </TableHeader>
293
+ <TableBody>
294
+ {report.parsers.map((row) => (
295
+ <TableRow key={`${row.parser}:${row.version}:${row.sourceFamily}`}>
296
+ <TableCell className="font-medium">{row.parser}</TableCell>
297
+ <TableCell>
298
+ <Badge variant="secondary">{row.version}</Badge>
299
+ </TableCell>
300
+ <TableCell>{row.sourceFamily}</TableCell>
301
+ <TableCell className="text-right">{row.imported.toLocaleString()}</TableCell>
302
+ <TableCell className="text-right">{row.importedWithErrors.toLocaleString()}</TableCell>
303
+ <TableCell className="text-right">{row.ignored.toLocaleString()}</TableCell>
304
+ <TableCell className="text-right">{row.unsupported.toLocaleString()}</TableCell>
305
+ <TableCell className="text-right">{row.failed.toLocaleString()}</TableCell>
306
+ <TableCell className="text-right">{row.duplicate.toLocaleString()}</TableCell>
307
+ <TableCell className="text-right">{row.recordsImported.toLocaleString()}</TableCell>
308
+ <TableCell className="max-w-md text-xs text-muted-foreground">
309
+ {row.latestReason || "No parser reason recorded."}
310
+ </TableCell>
311
+ </TableRow>
312
+ ))}
313
+ </TableBody>
314
+ </Table>
315
+ ) : (
316
+ <div className="px-4 py-6 text-sm text-muted-foreground">
317
+ No parser trust data yet. Run `tokentrace scan` to populate the latest scan report.
318
+ </div>
319
+ )}
320
+ </CardContent>
321
+ </Card>
322
+ );
323
+ }
324
+
325
+ function formatDelta(value: number) {
326
+ if (value > 0) return `+${value.toLocaleString()}`;
327
+ return value.toLocaleString();
328
+ }
329
+
330
+ function ScanDiffPanel({ report }: { report: DoctorReport["scanDiff"] }) {
331
+ const rows: Array<[string, keyof DoctorReport["scanDiff"]["current"]]> = [
332
+ ["Files scanned", "filesScanned"],
333
+ ["Records imported", "recordsImported"],
334
+ ["Imported", "imported"],
335
+ ["With errors", "importedWithErrors"],
336
+ ["Duplicates", "duplicates"],
337
+ ["Ignored", "ignored"],
338
+ ["Unsupported", "unsupported"],
339
+ ["Failed", "failed"]
340
+ ];
341
+
342
+ return (
343
+ <Card>
344
+ <CardHeader>
345
+ <CardTitle>Scan History Diff</CardTitle>
346
+ <CardDescription>
347
+ Latest scan compared with the previous scan using deterministic scan ordering. Ignored files are known support files, not usage transcripts.
348
+ </CardDescription>
349
+ </CardHeader>
350
+ <CardContent className="table-scroll space-y-4">
351
+ <div className="grid border-y md:grid-cols-2 md:divide-x">
352
+ <div className="min-w-0 p-3">
353
+ <FieldLabel>Latest scan</FieldLabel>
354
+ <div className="mt-1 text-sm font-semibold">{formatDate(report.latestCompletedAt ?? report.latestStartedAt)}</div>
355
+ <MonoText className="mt-1 block truncate text-xs text-muted-foreground">
356
+ {report.latestScanId ?? "No scan"}
357
+ </MonoText>
358
+ </div>
359
+ <div className="min-w-0 p-3">
360
+ <FieldLabel>Previous scan</FieldLabel>
361
+ <div className="mt-1 text-sm font-semibold">{formatDate(report.previousCompletedAt ?? report.previousStartedAt)}</div>
362
+ <MonoText className="mt-1 block truncate text-xs text-muted-foreground">
363
+ {report.previousScanId ?? "No previous scan"}
364
+ </MonoText>
365
+ </div>
366
+ </div>
367
+
368
+ <Table>
369
+ <TableHeader>
370
+ <TableRow>
371
+ <TableHead>Count</TableHead>
372
+ <TableHead className="text-right">Current</TableHead>
373
+ <TableHead className="text-right">Previous</TableHead>
374
+ <TableHead className="text-right">Delta</TableHead>
375
+ </TableRow>
376
+ </TableHeader>
377
+ <TableBody>
378
+ {rows.map(([label, key]) => (
379
+ <TableRow key={key}>
380
+ <TableCell className="font-medium">{label}</TableCell>
381
+ <TableCell className="text-right">{report.current[key].toLocaleString()}</TableCell>
382
+ <TableCell className="text-right">{report.previous[key].toLocaleString()}</TableCell>
383
+ <TableCell className="text-right">{formatDelta(report.delta[key])}</TableCell>
384
+ </TableRow>
385
+ ))}
386
+ </TableBody>
387
+ </Table>
388
+
389
+ {report.explanation ? (
390
+ <div className="rounded-md border bg-muted/30 p-3 text-sm leading-relaxed text-muted-foreground">
391
+ {report.explanation}
392
+ </div>
393
+ ) : null}
394
+ </CardContent>
395
+ </Card>
396
+ );
397
+ }
398
+
264
399
  function scanRunVariant(scanRun: DebugScanRun) {
265
400
  if (scanRun.errors.length > 0) return "destructive";
266
401
  if (scanRun.warnings.length > 0) return "warning";
@@ -366,6 +501,10 @@ export default async function DiagnosticsPage() {
366
501
 
367
502
  <DoctorReportPanel report={doctorReport} />
368
503
 
504
+ <ParserTrustPanel report={doctorReport.parserTrust} />
505
+
506
+ <ScanDiffPanel report={doctorReport.scanDiff} />
507
+
369
508
  <ScanHistoryPanel scanRuns={data.scanRuns} />
370
509
 
371
510
  <ScanHealthSummary health={data.health} />
@@ -0,0 +1,170 @@
1
+ import Link from "next/link";
2
+ import { ArrowLeft, ArrowRight } from "lucide-react";
3
+ import { Badge } from "@/components/ui/badge";
4
+ import { Button } from "@/components/ui/button";
5
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
6
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
7
+ import { DataValue, FieldLabel, MonoText, PageHeader } from "@/components/ui/typography";
8
+ import { buildEvidenceTrail, parseEvidenceMetric } from "@/src/lib/evidence-trail";
9
+ import { formatCurrency, formatTokens, percent } from "@/src/lib/format";
10
+
11
+ export const dynamic = "force-dynamic";
12
+
13
+ type EvidencePageProps = {
14
+ searchParams?: Promise<Record<string, string | string[] | undefined>>;
15
+ };
16
+
17
+ function confidenceVariant(value: string) {
18
+ if (value === "exact") return "success";
19
+ if (value === "unknown") return "warning";
20
+ return "secondary";
21
+ }
22
+
23
+ function parserStatusVariant(value: string | null) {
24
+ if (value === "imported") return "success";
25
+ if (value === "imported_with_errors") return "warning";
26
+ if (!value) return "secondary";
27
+ return "outline";
28
+ }
29
+
30
+ export default async function EvidencePage({ searchParams }: EvidencePageProps) {
31
+ const params = (await searchParams) ?? {};
32
+ const trail = buildEvidenceTrail({ metric: parseEvidenceMetric(params?.metric) });
33
+
34
+ return (
35
+ <div className="space-y-6">
36
+ <PageHeader
37
+ title={`${trail.title} Evidence`}
38
+ description={trail.description}
39
+ actions={
40
+ <Button asChild variant="outline" size="sm">
41
+ <Link href="/">
42
+ <ArrowLeft className="h-4 w-4" />
43
+ Overview
44
+ </Link>
45
+ </Button>
46
+ }
47
+ />
48
+
49
+ <Card>
50
+ <CardHeader>
51
+ <CardTitle>Metric Totals</CardTitle>
52
+ <CardDescription>
53
+ Totals use the same filtered metric definition as the session evidence below.
54
+ </CardDescription>
55
+ </CardHeader>
56
+ <CardContent className="p-0">
57
+ <div className="grid divide-y border-t sm:grid-cols-2 sm:divide-x sm:divide-y-0 lg:grid-cols-5">
58
+ <div className="p-3">
59
+ <FieldLabel>Tokens</FieldLabel>
60
+ <DataValue className="mt-1" size="md">{formatTokens(trail.totals.tokens)}</DataValue>
61
+ </div>
62
+ <div className="p-3">
63
+ <FieldLabel>Cost</FieldLabel>
64
+ <DataValue className="mt-1" size="md">{formatCurrency(trail.totals.cost)}</DataValue>
65
+ </div>
66
+ <div className="p-3">
67
+ <FieldLabel>Sessions</FieldLabel>
68
+ <DataValue className="mt-1" size="md">{trail.totals.sessions.toLocaleString()}</DataValue>
69
+ </div>
70
+ <div className="p-3">
71
+ <FieldLabel>Interactions</FieldLabel>
72
+ <DataValue className="mt-1" size="md">{trail.totals.interactions.toLocaleString()}</DataValue>
73
+ </div>
74
+ <div className="p-3">
75
+ <FieldLabel>Unknown Cost</FieldLabel>
76
+ <DataValue className="mt-1" size="md">{trail.totals.unknownCostInteractions.toLocaleString()}</DataValue>
77
+ </div>
78
+ </div>
79
+ </CardContent>
80
+ </Card>
81
+
82
+ <Card>
83
+ <CardHeader>
84
+ <CardTitle>Session, Source, Parser, And Pricing Evidence</CardTitle>
85
+ <CardDescription>
86
+ The table is capped at the top 100 contributing sessions; totals above include the full metric set.
87
+ </CardDescription>
88
+ </CardHeader>
89
+ <CardContent className="table-scroll p-0">
90
+ <Table>
91
+ <TableHeader>
92
+ <TableRow>
93
+ <TableHead>Session</TableHead>
94
+ <TableHead>Metric Total</TableHead>
95
+ <TableHead>Cost</TableHead>
96
+ <TableHead>Source</TableHead>
97
+ <TableHead>Parser</TableHead>
98
+ <TableHead>Pricing</TableHead>
99
+ <TableHead>Confidence</TableHead>
100
+ </TableRow>
101
+ </TableHeader>
102
+ <TableBody>
103
+ {trail.sessions.length ? (
104
+ trail.sessions.map((session) => (
105
+ <TableRow key={session.id}>
106
+ <TableCell className="min-w-64">
107
+ <Link href={session.sessionHref} className="font-medium text-primary underline-offset-4 hover:underline">
108
+ {session.title}
109
+ </Link>
110
+ <div className="mt-1 text-xs text-muted-foreground">
111
+ {session.tool} / {session.provider} / {session.project}
112
+ </div>
113
+ </TableCell>
114
+ <TableCell>{formatTokens(session.totalTokens)}</TableCell>
115
+ <TableCell>{formatCurrency(session.cost)}</TableCell>
116
+ <TableCell className="max-w-80">
117
+ <Link href={session.sourceHref} title={session.sourceFile}>
118
+ <MonoText className="block truncate text-muted-foreground underline-offset-4 hover:underline">
119
+ {session.sourceFile}
120
+ </MonoText>
121
+ </Link>
122
+ </TableCell>
123
+ <TableCell className="min-w-44">
124
+ <div className="flex flex-wrap items-center gap-2">
125
+ <Badge variant={parserStatusVariant(session.parserStatus)}>
126
+ {session.parserStatus ?? "not scanned"}
127
+ </Badge>
128
+ <Link href={session.parserHref} className="text-xs font-medium text-primary underline-offset-4 hover:underline">
129
+ Parser <ArrowRight className="inline h-3.5 w-3.5" />
130
+ </Link>
131
+ </div>
132
+ <div className="mt-1 text-xs text-muted-foreground">
133
+ {session.parser ?? "No parser"} / {session.parserConfidence == null ? "confidence unknown" : percent(session.parserConfidence)}
134
+ </div>
135
+ </TableCell>
136
+ <TableCell className="min-w-44">
137
+ {session.pricingHref ? (
138
+ <Link href={session.pricingHref} className="font-medium text-primary underline-offset-4 hover:underline">
139
+ {session.model}
140
+ </Link>
141
+ ) : (
142
+ <span>{session.model}</span>
143
+ )}
144
+ </TableCell>
145
+ <TableCell>
146
+ <div className="flex flex-col items-start gap-1">
147
+ <Badge variant={confidenceVariant(session.tokenConfidence)}>
148
+ {session.tokenConfidence}
149
+ </Badge>
150
+ <span className="text-xs text-muted-foreground">
151
+ {session.interactions.toLocaleString()} interactions
152
+ </span>
153
+ </div>
154
+ </TableCell>
155
+ </TableRow>
156
+ ))
157
+ ) : (
158
+ <TableRow>
159
+ <TableCell colSpan={7} className="h-24 text-center text-muted-foreground">
160
+ No evidence is available for this metric yet.
161
+ </TableCell>
162
+ </TableRow>
163
+ )}
164
+ </TableBody>
165
+ </Table>
166
+ </CardContent>
167
+ </Card>
168
+ </div>
169
+ );
170
+ }
package/app/globals.css CHANGED
@@ -54,11 +54,17 @@ body {
54
54
  }
55
55
 
56
56
  .period-date-input {
57
- padding-inline-end: 0.5rem;
57
+ color-scheme: light;
58
+ position: relative;
58
59
  }
59
60
 
60
61
  .period-date-input::-webkit-calendar-picker-indicator {
61
62
  cursor: pointer;
62
- margin-inline-end: 0.25rem;
63
- opacity: 0.8;
63
+ height: 100%;
64
+ margin: 0;
65
+ opacity: 0;
66
+ padding: 0;
67
+ position: absolute;
68
+ right: 0;
69
+ width: 2.5rem;
64
70
  }
package/app/layout.tsx CHANGED
@@ -1,3 +1,4 @@
1
+ import * as React from "react";
1
2
  import type { Metadata } from "next";
2
3
  import "./globals.css";
3
4
  import { MobileNav, Sidebar } from "@/components/sidebar";
@@ -17,7 +18,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
17
18
  <body>
18
19
  <div className="flex min-h-screen">
19
20
  <Sidebar appVersion={appVersion} />
20
- <main className="min-w-0 flex-1">
21
+ <main className="min-w-0 flex-1 overflow-x-hidden">
21
22
  <div className="border-b bg-card px-4 py-3 md:hidden">
22
23
  <div className="flex items-center justify-between gap-3">
23
24
  <div className="flex min-w-0 items-center gap-2">
@@ -33,7 +34,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
33
34
  </div>
34
35
  </div>
35
36
  <MobileNav />
36
- <div className="mx-auto w-full max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
37
+ <div className="mx-auto w-full min-w-0 max-w-[100vw] px-4 py-6 sm:px-6 md:max-w-7xl lg:px-8">
37
38
  {children}
38
39
  </div>
39
40
  </main>