tokentrace 0.14.2 → 0.15.1
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.
- package/CHANGELOG.md +43 -0
- package/README.md +22 -2
- package/app/discovery/page.tsx +2 -2
- package/app/evidence/page.tsx +3 -3
- package/app/globals.css +51 -3
- package/app/guide/page.tsx +4 -4
- package/app/parser-debug/page.tsx +3 -3
- package/app/sessions/[id]/page.tsx +1 -1
- package/app/sessions/page.tsx +1 -1
- package/components/charts/rank-bar-chart.tsx +3 -3
- package/components/charts/trend-chart.tsx +7 -5
- package/components/charts/trend-section.tsx +5 -3
- package/components/diagnostics/parser-panels.tsx +2 -2
- package/components/overview/recommendations-card.tsx +1 -1
- package/components/overview/review-status-strip.tsx +1 -1
- package/components/overview/summary-cards.tsx +6 -6
- package/components/period-filter.tsx +1 -1
- package/components/pricing/model-alias-suggestions-table.tsx +1 -1
- package/components/pricing/model-rates-table.tsx +1 -1
- package/components/pricing-settings.tsx +1 -1
- package/components/repair/repair-guidance.tsx +1 -1
- package/components/repair/repair-items-table.tsx +2 -2
- package/components/repair-state-control.tsx +2 -2
- package/components/scan-health-summary.tsx +1 -1
- package/components/scan-now-button.tsx +2 -2
- package/components/session-explorer.tsx +9 -8
- package/components/settings/import-profiles-section.tsx +2 -2
- package/components/settings/section-nav.tsx +2 -2
- package/components/sidebar.tsx +2 -2
- package/components/ui/badge.tsx +1 -1
- package/components/ui/button.tsx +1 -1
- package/components/ui/help-tooltip.tsx +1 -1
- package/components/ui/input.tsx +1 -1
- package/components/ui/textarea.tsx +1 -1
- package/components/ui/typography.tsx +1 -1
- package/dist/runtime/scan.mjs +5 -2
- package/package.json +21 -20
- package/postcss.config.mjs +1 -2
- package/scripts/package-inspect.mjs +4 -4
- package/server.json +2 -2
- package/src/cli/runtime.js +1 -2
- package/src/ingestion/adapters/sqlite-history.ts +6 -2
- package/tsconfig.json +20 -5
- package/tailwind.config.ts +0 -53
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,49 @@ All notable changes to TokenTrace are documented here.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## [0.15.1] - 2026-05-22
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Upgraded Next.js from 15 to 16 (and `eslint-config-next` to 16). Lint config now uses `eslint-config-next`'s native flat config; the `FlatCompat`/`@eslint/eslintrc` bridge is no longer needed.
|
|
12
|
+
- Upgraded Tailwind CSS from 3 to 4 via `@tailwindcss/upgrade`. Tailwind config moved from `tailwind.config.ts` into `@theme` directives in `app/globals.css`; `postcss.config.mjs` now uses `@tailwindcss/postcss`; `autoprefixer` is removed (built into Tailwind 4).
|
|
13
|
+
- Upgraded `tailwind-merge` from 2 to 3 (gated on Tailwind 4).
|
|
14
|
+
- Upgraded Recharts from 2 to 3.
|
|
15
|
+
- Upgraded Vitest from 3 to 4 (added `@vitejs/plugin-react` so React/JSX is parsed by Vite instead of Rolldown's parser).
|
|
16
|
+
- Upgraded TypeScript from 5 to 6. Removed the deprecated `baseUrl` from `tsconfig.json` (path mapping still works under `moduleResolution: "bundler"`).
|
|
17
|
+
- Bumped minimum Node.js engine to `>=20.9.0` (required by Next 16).
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- Replaced three `useEffect` -> `setState` synchronization patterns with the React-recommended previous-value comparison (in `components/charts/trend-chart.tsx`, `components/charts/trend-section.tsx`, `components/session-explorer.tsx`) to satisfy the new `react-hooks/set-state-in-effect` rule and avoid cascading renders.
|
|
22
|
+
- Reverted the Tailwind 4 codemod's accidental rename of the `"outline"` button/badge `variant` name to `"outline-solid"` (it is an internal variant label, not a Tailwind class).
|
|
23
|
+
- Wrapped Recharts `Tooltip.labelFormatter` to match the new v3 signature.
|
|
24
|
+
- Parallelized 10-spawn CLI subprocess test in `tests/serve-command.test.ts` so it no longer times out under parallel test contention; raised global Vitest timeouts and `spawnSync` timeouts in remaining CLI subprocess tests to match.
|
|
25
|
+
|
|
26
|
+
### Deferred
|
|
27
|
+
|
|
28
|
+
- ESLint 9 -> 10: blocked on `eslint-plugin-react` (peer-pinned by `eslint-config-next`); the latest 7.37.5 caps its eslint peer at `^9.7`. Staying on ESLint 9.39.4.
|
|
29
|
+
|
|
30
|
+
## [0.15.0] - 2026-05-22
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- Bumped minimum Node.js to 20.0.0 (Node 18 LTS reached end of life on 2025-04-30). This is a breaking change for users still on Node 18.
|
|
35
|
+
- Upgraded `better-sqlite3` from 11 to 12 (latest major).
|
|
36
|
+
- Upgraded `lucide-react` from 0.468 to 1.16 (first stable major release).
|
|
37
|
+
- Upgraded `open` from 10 to 11 (now requires Node 20+, aligns with engines bump).
|
|
38
|
+
- Bumped minor/patch versions for `tsx`, `postcss`, `autoprefixer`, `@types/react`, `@types/node`.
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- `sqlite-history` adapter now uses SQLite identifier quoting (`""`) instead of `JSON.stringify()` when interpolating user table names into raw queries. Previously, a table name containing a quote could have caused a SQL parse error during read-only ingestion.
|
|
43
|
+
- Made `eslint-plugin-react-hooks` an explicit devDependency so lint setup no longer relies on accidental dependency hoisting from `eslint-config-next`.
|
|
44
|
+
- Raised global Vitest `testTimeout`/`hookTimeout` to 30s and bumped per-test/spawn timeouts in CLI subprocess tests (`mcp-server`, `serve-command`, `statusline-cli`) so they are no longer flaky under macOS process-spawn latency.
|
|
45
|
+
|
|
46
|
+
### Documentation
|
|
47
|
+
|
|
48
|
+
- Added a Troubleshooting section to the README covering the `prebuild-install` deprecation warning from `better-sqlite3`, native build errors, and `EBADENGINE` warnings.
|
|
49
|
+
|
|
7
50
|
## [0.14.2] - 2026-05-21
|
|
8
51
|
|
|
9
52
|
### Added
|
package/README.md
CHANGED
|
@@ -244,8 +244,6 @@ The CLI sets `TOKENTRACE_DB` and `DATABASE_URL` automatically. You can override
|
|
|
244
244
|
TOKENTRACE_HOME=/custom/path tokentrace
|
|
245
245
|
```
|
|
246
246
|
|
|
247
|
-
If `npm install -g tokentrace` prints a `prebuild-install` deprecation warning, it is from the native SQLite dependency chain used by `better-sqlite3`. The install should continue normally, and TokenTrace still runs locally.
|
|
248
|
-
|
|
249
247
|
## Where TokenTrace Looks
|
|
250
248
|
|
|
251
249
|
Default discovery checks these locations when present:
|
|
@@ -478,6 +476,28 @@ The ingestion system is intentionally pluggable:
|
|
|
478
476
|
|
|
479
477
|
Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for local setup, parser guidelines, pricing update notes, and the release policy.
|
|
480
478
|
|
|
479
|
+
## Troubleshooting
|
|
480
|
+
|
|
481
|
+
### `prebuild-install` deprecation warning during install
|
|
482
|
+
|
|
483
|
+
```
|
|
484
|
+
npm warn deprecated prebuild-install@7.1.3: No longer maintained...
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
This is a transitive dependency of `better-sqlite3` (the native SQLite driver TokenTrace uses to store local data). The warning is harmless — the install completes normally and TokenTrace runs as expected. It will go away once `better-sqlite3` upstream migrates to a different prebuilt binary loader; there is nothing to fix on the TokenTrace side.
|
|
488
|
+
|
|
489
|
+
### Native build errors on `better-sqlite3`
|
|
490
|
+
|
|
491
|
+
If the prebuilt binary cannot be downloaded (offline machine, restrictive proxy, unsupported Node ABI), `better-sqlite3` will try to compile from source and may fail. Workarounds:
|
|
492
|
+
|
|
493
|
+
- Ensure Node.js is a supported LTS (TokenTrace requires `>= 20.0.0`; Node 20 or 22 LTS is recommended).
|
|
494
|
+
- Install build tools: Xcode Command Line Tools on macOS, `build-essential` and `python3` on Linux, or the "Desktop development with C++" workload on Windows.
|
|
495
|
+
- Retry the install with network access to `github.com` so the prebuilt binary can be fetched.
|
|
496
|
+
|
|
497
|
+
### `EBADENGINE` warnings
|
|
498
|
+
|
|
499
|
+
These appear when your local Node version is older than the `engines` field of a transitive dependency. They are warnings, not errors. Upgrading to the latest Node LTS resolves them.
|
|
500
|
+
|
|
481
501
|
## Known Limitations
|
|
482
502
|
|
|
483
503
|
- Claude Code and Codex CLI log formats are inferred defensively and may need refinement with real sample logs.
|
package/app/discovery/page.tsx
CHANGED
|
@@ -70,7 +70,7 @@ export default function DiscoveryPage() {
|
|
|
70
70
|
<CardDescription>Latest 500 discovered files. Ignored files are retained so support-file noise stays visible without becoming usage.</CardDescription>
|
|
71
71
|
</CardHeader>
|
|
72
72
|
<CardContent className="table-scroll">
|
|
73
|
-
<Table className="min-w-
|
|
73
|
+
<Table className="min-w-5xl">
|
|
74
74
|
<TableHeader>
|
|
75
75
|
<TableRow>
|
|
76
76
|
<TableHead>File</TableHead>
|
|
@@ -91,7 +91,7 @@ export default function DiscoveryPage() {
|
|
|
91
91
|
<TableCell>{file.sizeBytes.toLocaleString()} bytes</TableCell>
|
|
92
92
|
<TableCell>{formatDate(file.modifiedTime)}</TableCell>
|
|
93
93
|
<TableCell>{file.parser ?? "None"}</TableCell>
|
|
94
|
-
<TableCell className="max-w-sm whitespace-normal break-
|
|
94
|
+
<TableCell className="max-w-sm whitespace-normal wrap-break-word text-xs leading-relaxed text-muted-foreground">
|
|
95
95
|
{String(file.rawMetadata.ignoreReason ?? file.rawMetadata.reason ?? file.errors[0] ?? file.warnings[0] ?? "None")}
|
|
96
96
|
</TableCell>
|
|
97
97
|
</TableRow>
|
package/app/evidence/page.tsx
CHANGED
|
@@ -111,7 +111,7 @@ function EvidenceMetricTabs({
|
|
|
111
111
|
key={item.metric}
|
|
112
112
|
href={mergeHrefParams(`/evidence?metric=${item.metric}`, rangeLinkParams)}
|
|
113
113
|
className={cn(
|
|
114
|
-
"inline-flex h-8 items-center rounded-md border px-3 text-sm font-medium transition-colors focus-visible:outline-
|
|
114
|
+
"inline-flex h-8 items-center rounded-md border px-3 text-sm font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
115
115
|
current === item.metric
|
|
116
116
|
? "border-primary bg-primary text-primary-foreground"
|
|
117
117
|
: "border-border bg-card text-foreground hover:bg-muted"
|
|
@@ -141,7 +141,7 @@ function EvidenceDrilldownStrip({
|
|
|
141
141
|
key={action.label}
|
|
142
142
|
href={action.href}
|
|
143
143
|
className={cn(
|
|
144
|
-
"group min-w-0 p-3 transition-colors hover:bg-muted/50 focus-visible:outline-
|
|
144
|
+
"group min-w-0 p-3 transition-colors hover:bg-muted/50 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
145
145
|
index > 0 ? "border-t border-border md:border-l md:border-t-0" : ""
|
|
146
146
|
)}
|
|
147
147
|
>
|
|
@@ -204,7 +204,7 @@ function EvidenceContextPanel({
|
|
|
204
204
|
<Link
|
|
205
205
|
key={action.label}
|
|
206
206
|
href={action.href}
|
|
207
|
-
className="group rounded-md border bg-card p-3 text-left transition-colors hover:bg-muted/40 focus-visible:outline-
|
|
207
|
+
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"
|
|
208
208
|
>
|
|
209
209
|
<span className="flex items-center gap-1.5 text-xs font-semibold text-foreground">
|
|
210
210
|
{action.label}
|
package/app/globals.css
CHANGED
|
@@ -1,6 +1,54 @@
|
|
|
1
|
-
@
|
|
2
|
-
|
|
3
|
-
@
|
|
1
|
+
@import 'tailwindcss';
|
|
2
|
+
|
|
3
|
+
@custom-variant dark (&:is(.dark *));
|
|
4
|
+
|
|
5
|
+
@theme {
|
|
6
|
+
--color-border: hsl(var(--border));
|
|
7
|
+
--color-input: hsl(var(--input));
|
|
8
|
+
--color-ring: hsl(var(--ring));
|
|
9
|
+
--color-background: hsl(var(--background));
|
|
10
|
+
--color-foreground: hsl(var(--foreground));
|
|
11
|
+
|
|
12
|
+
--color-primary: hsl(var(--primary));
|
|
13
|
+
--color-primary-foreground: hsl(var(--primary-foreground));
|
|
14
|
+
|
|
15
|
+
--color-secondary: hsl(var(--secondary));
|
|
16
|
+
--color-secondary-foreground: hsl(var(--secondary-foreground));
|
|
17
|
+
|
|
18
|
+
--color-muted: hsl(var(--muted));
|
|
19
|
+
--color-muted-foreground: hsl(var(--muted-foreground));
|
|
20
|
+
|
|
21
|
+
--color-accent: hsl(var(--accent));
|
|
22
|
+
--color-accent-foreground: hsl(var(--accent-foreground));
|
|
23
|
+
|
|
24
|
+
--color-destructive: hsl(var(--destructive));
|
|
25
|
+
--color-destructive-foreground: hsl(var(--destructive-foreground));
|
|
26
|
+
|
|
27
|
+
--color-card: hsl(var(--card));
|
|
28
|
+
--color-card-foreground: hsl(var(--card-foreground));
|
|
29
|
+
|
|
30
|
+
--radius-lg: 0.5rem;
|
|
31
|
+
--radius-md: 0.375rem;
|
|
32
|
+
--radius-sm: 0.25rem;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/*
|
|
36
|
+
The default border color has changed to `currentcolor` in Tailwind CSS v4,
|
|
37
|
+
so we've added these compatibility styles to make sure everything still
|
|
38
|
+
looks the same as it did with Tailwind CSS v3.
|
|
39
|
+
|
|
40
|
+
If we ever want to remove these styles, we need to add an explicit border
|
|
41
|
+
color utility to any element that depends on these defaults.
|
|
42
|
+
*/
|
|
43
|
+
@layer base {
|
|
44
|
+
*,
|
|
45
|
+
::after,
|
|
46
|
+
::before,
|
|
47
|
+
::backdrop,
|
|
48
|
+
::file-selector-button {
|
|
49
|
+
border-color: var(--color-gray-200, currentcolor);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
4
52
|
|
|
5
53
|
:root {
|
|
6
54
|
--background: 40 33% 98%;
|
package/app/guide/page.tsx
CHANGED
|
@@ -421,7 +421,7 @@ export default function GuidePage() {
|
|
|
421
421
|
</div>
|
|
422
422
|
</div>
|
|
423
423
|
<div className="table-scroll">
|
|
424
|
-
<Table className="min-w-
|
|
424
|
+
<Table className="min-w-2xl">
|
|
425
425
|
<TableHeader>
|
|
426
426
|
<TableRow>
|
|
427
427
|
<TableHead>Label</TableHead>
|
|
@@ -527,7 +527,7 @@ export default function GuidePage() {
|
|
|
527
527
|
{mcpAgentEntries.map(([label, value, detail]) => (
|
|
528
528
|
<div key={label} className="rounded-md border bg-muted/30 p-3">
|
|
529
529
|
<FieldLabel>{label}</FieldLabel>
|
|
530
|
-
<div className="mt-2 min-h-10 break-
|
|
530
|
+
<div className="mt-2 min-h-10 wrap-break-word text-sm font-medium leading-5">
|
|
531
531
|
<MonoText>{value}</MonoText>
|
|
532
532
|
</div>
|
|
533
533
|
<p className="mt-2 text-xs leading-5 text-muted-foreground">{detail}</p>
|
|
@@ -538,7 +538,7 @@ export default function GuidePage() {
|
|
|
538
538
|
<div className="border-t p-4">
|
|
539
539
|
<h3 className="text-sm font-semibold leading-tight">Agent quickstart</h3>
|
|
540
540
|
<div className="table-scroll mt-3">
|
|
541
|
-
<Table className="min-w-
|
|
541
|
+
<Table className="min-w-184">
|
|
542
542
|
<TableHeader>
|
|
543
543
|
<TableRow>
|
|
544
544
|
<TableHead>Step</TableHead>
|
|
@@ -570,7 +570,7 @@ export default function GuidePage() {
|
|
|
570
570
|
Use <MonoText>tokentrace roadmap --json</MonoText> or <MonoText>/api/roadmap</MonoText> for implementation status and release gates.
|
|
571
571
|
</p>
|
|
572
572
|
<div className="table-scroll mt-3">
|
|
573
|
-
<Table className="min-w-
|
|
573
|
+
<Table className="min-w-176">
|
|
574
574
|
<TableHeader>
|
|
575
575
|
<TableRow>
|
|
576
576
|
<TableHead>Surface</TableHead>
|
|
@@ -92,14 +92,14 @@ export default async function ParserDebugPage({
|
|
|
92
92
|
<TableCell>{file.parser ?? "None"}</TableCell>
|
|
93
93
|
<TableCell><Badge variant={file.errors.length ? "destructive" : file.parser ? "success" : "secondary"}>{file.status}</Badge></TableCell>
|
|
94
94
|
<TableCell>{file.rawMetadata.confidence == null ? "Unknown" : Number(file.rawMetadata.confidence).toFixed(2)}</TableCell>
|
|
95
|
-
<TableCell className="max-w-xs whitespace-normal break-
|
|
95
|
+
<TableCell className="max-w-xs whitespace-normal wrap-break-word">
|
|
96
96
|
<MonoText className="text-muted-foreground">
|
|
97
97
|
{JSON.stringify(file.rawMetadata.tokenConfidence ?? { unknown: 0 })}
|
|
98
98
|
</MonoText>
|
|
99
99
|
</TableCell>
|
|
100
100
|
<TableCell>{file.recordsImported.toLocaleString()}</TableCell>
|
|
101
|
-
<TableCell className="max-w-sm whitespace-normal break-
|
|
102
|
-
<TableCell className="max-w-sm whitespace-normal break-
|
|
101
|
+
<TableCell className="max-w-sm whitespace-normal wrap-break-word text-xs leading-relaxed text-muted-foreground">{file.warnings.join("; ") || "None"}</TableCell>
|
|
102
|
+
<TableCell className="max-w-sm whitespace-normal wrap-break-word text-xs leading-relaxed text-muted-foreground">{file.errors.join("; ") || "None"}</TableCell>
|
|
103
103
|
<TableCell className="max-w-md break-all" title={file.path}>
|
|
104
104
|
<MonoText>{file.path}</MonoText>
|
|
105
105
|
</TableCell>
|
|
@@ -211,7 +211,7 @@ export default async function SessionTimelinePage({
|
|
|
211
211
|
<CardDescription>Interaction rows are expanded with model changes, spikes, cache activity, unknown cost, and tool calls.</CardDescription>
|
|
212
212
|
</CardHeader>
|
|
213
213
|
<CardContent className="table-scroll">
|
|
214
|
-
<Table className="min-w-
|
|
214
|
+
<Table className="min-w-312">
|
|
215
215
|
<TableHeader>
|
|
216
216
|
<TableRow>
|
|
217
217
|
<TableHead className="w-36">Time</TableHead>
|
package/app/sessions/page.tsx
CHANGED
|
@@ -45,7 +45,7 @@ export default async function SessionsPage({
|
|
|
45
45
|
</CardDescription>
|
|
46
46
|
</CardHeader>
|
|
47
47
|
<CardContent className="table-scroll overflow-x-auto">
|
|
48
|
-
<Table className="min-w-
|
|
48
|
+
<Table className="min-w-216">
|
|
49
49
|
<TableHeader>
|
|
50
50
|
<TableRow>
|
|
51
51
|
<TableHead>Priority</TableHead>
|
|
@@ -8,11 +8,11 @@ function ChartSkeleton() {
|
|
|
8
8
|
return (
|
|
9
9
|
<div className="grid h-full grid-cols-[7rem_minmax(0,1fr)]" aria-label="Chart loading">
|
|
10
10
|
<div className="space-y-10 border-r border-border/70 pt-10">
|
|
11
|
-
<div className="h-3 w-20 rounded bg-muted" />
|
|
12
|
-
<div className="h-3 w-16 rounded bg-muted" />
|
|
11
|
+
<div className="h-3 w-20 rounded-sm bg-muted" />
|
|
12
|
+
<div className="h-3 w-16 rounded-sm bg-muted" />
|
|
13
13
|
</div>
|
|
14
14
|
<div className="relative overflow-hidden p-8">
|
|
15
|
-
<div className="absolute inset-0 bg-[linear-gradient(to_right,#e7ded3_1px,transparent_1px),linear-gradient(to_bottom,#e7ded3_1px,transparent_1px)] bg-[
|
|
15
|
+
<div className="absolute inset-0 bg-[linear-gradient(to_right,#e7ded3_1px,transparent_1px),linear-gradient(to_bottom,#e7ded3_1px,transparent_1px)] bg-size-[25%_33%] opacity-80" />
|
|
16
16
|
<div className="relative mt-8 h-10 w-[82%] rounded-r bg-primary/20" />
|
|
17
17
|
<div className="relative mt-10 h-10 w-[22%] rounded-r bg-primary/15" />
|
|
18
18
|
</div>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import * as React from "react";
|
|
4
|
-
import {
|
|
4
|
+
import { useMemo, useState } from "react";
|
|
5
5
|
import { Area, AreaChart, CartesianGrid, Tooltip, XAxis, YAxis } from "recharts";
|
|
6
6
|
import type { TrendPoint } from "@/src/lib/analytics";
|
|
7
7
|
import { formatCurrency, formatShortDate, formatTokens } from "@/src/lib/format";
|
|
@@ -41,7 +41,7 @@ function ChartSkeleton() {
|
|
|
41
41
|
<div className="grid h-full grid-cols-[2.5rem_minmax(0,1fr)] grid-rows-[minmax(0,1fr)_1.5rem]" aria-label="Chart loading">
|
|
42
42
|
<div className="row-span-1 border-r border-border/70" />
|
|
43
43
|
<div className="relative overflow-hidden">
|
|
44
|
-
<div className="absolute inset-0 bg-[linear-gradient(to_right,#e7ded3_1px,transparent_1px),linear-gradient(to_bottom,#e7ded3_1px,transparent_1px)] bg-[
|
|
44
|
+
<div className="absolute inset-0 bg-[linear-gradient(to_right,#e7ded3_1px,transparent_1px),linear-gradient(to_bottom,#e7ded3_1px,transparent_1px)] bg-size-[25%_33%] opacity-80" />
|
|
45
45
|
<div className="absolute bottom-[18%] left-[8%] h-[38%] w-[84%] rounded-t-full border-t-4 border-primary/25" />
|
|
46
46
|
<div className="absolute bottom-[12%] left-[8%] h-2 w-[84%] rounded-full bg-muted" />
|
|
47
47
|
</div>
|
|
@@ -137,11 +137,13 @@ export function TrendChart({
|
|
|
137
137
|
}) {
|
|
138
138
|
const [internalPeriod, setInternalPeriod] = useState<TrendBucket>("daily");
|
|
139
139
|
const [internalTrendWindow, setInternalTrendWindow] = useState<TrendWindow>(defaultWindow);
|
|
140
|
+
const [prevDefaultWindow, setPrevDefaultWindow] = useState<TrendWindow>(defaultWindow);
|
|
140
141
|
const { ref: chartRef, size } = useChartSize<HTMLDivElement>();
|
|
141
142
|
|
|
142
|
-
|
|
143
|
+
if (defaultWindow !== prevDefaultWindow) {
|
|
144
|
+
setPrevDefaultWindow(defaultWindow);
|
|
143
145
|
if (!controlledTrendWindow) setInternalTrendWindow(defaultWindow);
|
|
144
|
-
}
|
|
146
|
+
}
|
|
145
147
|
|
|
146
148
|
const period = controlledPeriod ?? internalPeriod;
|
|
147
149
|
const trendWindow = controlledTrendWindow ?? internalTrendWindow;
|
|
@@ -217,7 +219,7 @@ export function TrendChart({
|
|
|
217
219
|
? formatCurrency(Number(value))
|
|
218
220
|
: `${formatTokens(Number(value))} tokens`
|
|
219
221
|
}
|
|
220
|
-
labelFormatter={formatShortDate}
|
|
222
|
+
labelFormatter={(label) => formatShortDate(label as string | number | null | undefined)}
|
|
221
223
|
/>
|
|
222
224
|
<Area
|
|
223
225
|
type="monotone"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import * as React from "react";
|
|
4
|
-
import {
|
|
4
|
+
import { useState } from "react";
|
|
5
5
|
import type { TrendPoint } from "@/src/lib/analytics";
|
|
6
6
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
7
7
|
import { Badge } from "@/components/ui/badge";
|
|
@@ -23,10 +23,12 @@ export function TrendSection({
|
|
|
23
23
|
}) {
|
|
24
24
|
const [period, setPeriod] = useState<TrendBucket>("daily");
|
|
25
25
|
const [trendWindow, setTrendWindow] = useState<TrendWindow>(defaultWindow);
|
|
26
|
+
const [prevDefaultWindow, setPrevDefaultWindow] = useState<TrendWindow>(defaultWindow);
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
if (defaultWindow !== prevDefaultWindow) {
|
|
29
|
+
setPrevDefaultWindow(defaultWindow);
|
|
28
30
|
setTrendWindow(defaultWindow);
|
|
29
|
-
}
|
|
31
|
+
}
|
|
30
32
|
|
|
31
33
|
return (
|
|
32
34
|
<section className="space-y-3">
|
|
@@ -19,7 +19,7 @@ export function ParserTrustPanel({ report }: { report: DoctorReport["parserTrust
|
|
|
19
19
|
</CardHeader>
|
|
20
20
|
<CardContent className="table-scroll">
|
|
21
21
|
{report.parsers.length ? (
|
|
22
|
-
<Table className="min-w-
|
|
22
|
+
<Table className="min-w-6xl">
|
|
23
23
|
<TableHeader>
|
|
24
24
|
<TableRow>
|
|
25
25
|
<TableHead>Parser</TableHead>
|
|
@@ -249,7 +249,7 @@ export function SourceCoveragePanel({ scanFiles }: { scanFiles: ReturnType<typeo
|
|
|
249
249
|
<p className="mt-1 text-xs leading-relaxed text-muted-foreground">{entry.description}</p>
|
|
250
250
|
<div className="mt-2 flex flex-wrap gap-1">
|
|
251
251
|
{entry.matchers.map((matcher) => (
|
|
252
|
-
<code key={matcher} className="rounded bg-muted px-1.5 py-0.5 text-xs">{matcher}</code>
|
|
252
|
+
<code key={matcher} className="rounded-sm bg-muted px-1.5 py-0.5 text-xs">{matcher}</code>
|
|
253
253
|
))}
|
|
254
254
|
</div>
|
|
255
255
|
</div>
|
|
@@ -23,7 +23,7 @@ export function OverviewRecommendationsCard({
|
|
|
23
23
|
<li key={item.id} className="min-w-0">
|
|
24
24
|
<Link
|
|
25
25
|
href={item.href}
|
|
26
|
-
className="group flex h-full min-w-0 gap-3 px-4 py-4 transition-colors hover:bg-muted/30 focus-visible:outline-
|
|
26
|
+
className="group flex h-full min-w-0 gap-3 px-4 py-4 transition-colors hover:bg-muted/30 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
|
27
27
|
>
|
|
28
28
|
<span className="mt-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-full border border-border text-xs font-semibold tabular-nums text-muted-foreground">
|
|
29
29
|
{index + 1}
|
|
@@ -41,7 +41,7 @@ function OverviewReviewStatusTile({
|
|
|
41
41
|
<Link
|
|
42
42
|
href={href}
|
|
43
43
|
className={cn(
|
|
44
|
-
"group flex min-w-0 gap-3 p-3 transition-colors hover:bg-muted/40 focus-visible:outline-
|
|
44
|
+
"group flex min-w-0 gap-3 p-3 transition-colors hover:bg-muted/40 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
45
45
|
className
|
|
46
46
|
)}
|
|
47
47
|
>
|
|
@@ -36,7 +36,7 @@ function CostSessionsMetricPane({
|
|
|
36
36
|
return (
|
|
37
37
|
<section
|
|
38
38
|
className={cn(
|
|
39
|
-
"cost-sessions-section grid min-w-0 grid-rows-[auto_auto_auto_auto_1fr_auto] p-4 md:row-span-6 md:
|
|
39
|
+
"cost-sessions-section grid min-w-0 grid-rows-[auto_auto_auto_auto_1fr_auto] p-4 md:row-span-6 md:grid-rows-subgrid",
|
|
40
40
|
className
|
|
41
41
|
)}
|
|
42
42
|
>
|
|
@@ -48,7 +48,7 @@ function CostSessionsMetricPane({
|
|
|
48
48
|
</div>
|
|
49
49
|
|
|
50
50
|
<div className="mt-3">
|
|
51
|
-
<DataValue size="lg" className={cn("break-
|
|
51
|
+
<DataValue size="lg" className={cn("wrap-break-word leading-none", valueClassName)}>{value}</DataValue>
|
|
52
52
|
</div>
|
|
53
53
|
|
|
54
54
|
<div className="mt-3 flex flex-wrap items-start gap-x-2 gap-y-1 text-xs leading-snug text-muted-foreground">
|
|
@@ -118,11 +118,11 @@ export function CostSessionsCard({
|
|
|
118
118
|
<CardDescription>Model-rate trust and imported activity in one place.</CardDescription>
|
|
119
119
|
</CardHeader>
|
|
120
120
|
<CardContent className="flex flex-1 flex-col p-0">
|
|
121
|
-
<div className="grid flex-1 border-t md:grid-cols-2 md:
|
|
121
|
+
<div className="grid flex-1 border-t md:grid-cols-2 md:grid-rows-[auto_auto_auto_auto_1fr_auto]">
|
|
122
122
|
<CostSessionsMetricPane
|
|
123
123
|
label="Cost"
|
|
124
124
|
value={formatCurrency(summary.totalCost)}
|
|
125
|
-
valueClassName="break-
|
|
125
|
+
valueClassName="wrap-break-word text-2xl"
|
|
126
126
|
detailItems={[
|
|
127
127
|
`${formatCurrency(summary.exactCost)} exact`,
|
|
128
128
|
`${formatCurrency(summary.estimatedCost)} estimated`,
|
|
@@ -176,7 +176,7 @@ function TokenAccountingSlice({
|
|
|
176
176
|
return (
|
|
177
177
|
<div className="min-w-0 border-t p-3 first:border-t-0 md:border-l md:border-t-0 md:first:border-l-0">
|
|
178
178
|
<FieldLabel>{label}</FieldLabel>
|
|
179
|
-
<DataValue className="mt-1 break-
|
|
179
|
+
<DataValue className="mt-1 wrap-break-word leading-none" size="md">{value}</DataValue>
|
|
180
180
|
<p className="mt-2 text-xs leading-5 text-muted-foreground">{detail}</p>
|
|
181
181
|
</div>
|
|
182
182
|
);
|
|
@@ -220,7 +220,7 @@ export function TokenAccountingCard({
|
|
|
220
220
|
</span>
|
|
221
221
|
</CardHeader>
|
|
222
222
|
<CardContent className="flex flex-1 flex-col">
|
|
223
|
-
<DataValue size="lg" className="break-
|
|
223
|
+
<DataValue size="lg" className="wrap-break-word text-3xl leading-none">{formatTokens(summary.totalTokens)}</DataValue>
|
|
224
224
|
<div className="mt-1 text-sm font-medium text-foreground">processed tokens</div>
|
|
225
225
|
<div className="mt-3 flex flex-wrap items-center gap-x-2 gap-y-1 text-xs leading-snug text-muted-foreground">
|
|
226
226
|
{[
|
|
@@ -71,7 +71,7 @@ export function PeriodFilter({
|
|
|
71
71
|
const statusLabel = range.key === "custom" ? "Custom range" : range.label;
|
|
72
72
|
|
|
73
73
|
return (
|
|
74
|
-
<div className="min-w-0 max-w-full rounded-lg bg-card p-3 outline outline-1 outline-border sm:p-4">
|
|
74
|
+
<div className="min-w-0 max-w-full rounded-lg bg-card p-3 outline-solid outline-1 outline-border sm:p-4">
|
|
75
75
|
<form className="max-w-full" action={basePath}>
|
|
76
76
|
<input type="hidden" name="range" value="custom" />
|
|
77
77
|
{Object.entries(preserveParams).map(([key, value]) =>
|
|
@@ -19,7 +19,7 @@ function ModelAliasSuggestionMobileCards({
|
|
|
19
19
|
<div className="flex flex-wrap items-start justify-between gap-2">
|
|
20
20
|
<div className="min-w-0">
|
|
21
21
|
<div className="text-xs font-medium uppercase text-muted-foreground">Observed model</div>
|
|
22
|
-
<div className="mt-1 break-
|
|
22
|
+
<div className="mt-1 wrap-break-word text-sm font-semibold">{suggestion.model}</div>
|
|
23
23
|
</div>
|
|
24
24
|
<Badge variant={suggestion.confidence === "high" ? "success" : suggestion.confidence === "medium" ? "warning" : "secondary"}>
|
|
25
25
|
{suggestion.confidence}
|
|
@@ -40,7 +40,7 @@ function ModelRateMobileCards({
|
|
|
40
40
|
<div className="flex flex-wrap items-start justify-between gap-2">
|
|
41
41
|
<div className="min-w-0">
|
|
42
42
|
<div className="text-xs font-medium uppercase text-muted-foreground">Provider/model</div>
|
|
43
|
-
<div className="mt-1 break-
|
|
43
|
+
<div className="mt-1 wrap-break-word text-sm font-medium">{row.model || "Unnamed model"}</div>
|
|
44
44
|
<div className="mt-1 text-xs text-muted-foreground">{(row.providerName ?? row.provider) || row.providerId || "No provider"}</div>
|
|
45
45
|
</div>
|
|
46
46
|
{duplicate ? <Badge variant="warning">duplicate rate</Badge> : null}
|
|
@@ -110,7 +110,7 @@ export function PricingSettings({
|
|
|
110
110
|
<CardContent className="space-y-3 text-sm text-muted-foreground">
|
|
111
111
|
<Label>Formula</Label>
|
|
112
112
|
<pre className="overflow-x-auto rounded-md bg-muted/40 p-3 whitespace-pre-wrap">
|
|
113
|
-
<MonoText className="break-
|
|
113
|
+
<MonoText className="wrap-break-word">
|
|
114
114
|
input * inputPrice + output * outputPrice + cacheRead * cacheReadPrice + cacheWrite * cacheWritePrice
|
|
115
115
|
</MonoText>
|
|
116
116
|
</pre>
|
|
@@ -77,7 +77,7 @@ export function RepairFlowSteps() {
|
|
|
77
77
|
<CardDescription>Move left to right so every unknown-cost item ends with a checked local result.</CardDescription>
|
|
78
78
|
</CardHeader>
|
|
79
79
|
<CardContent className="overflow-x-auto p-0">
|
|
80
|
-
<div className="grid divide-y border-t sm:min-w-
|
|
80
|
+
<div className="grid divide-y border-t sm:min-w-184 sm:grid-cols-5 sm:divide-x sm:divide-y-0">
|
|
81
81
|
{steps.map(([label, detail], index) => (
|
|
82
82
|
<div key={label} className="p-3">
|
|
83
83
|
<div className="flex items-center gap-2">
|
|
@@ -35,7 +35,7 @@ function RepairItemsMobileList({
|
|
|
35
35
|
<Badge variant={causeVariant(group.cause)}>{causeLabel(group.cause)}</Badge>
|
|
36
36
|
<span className="text-xs text-muted-foreground">{group.interactions.toLocaleString()} interactions</span>
|
|
37
37
|
</div>
|
|
38
|
-
<div className="mt-2 break-
|
|
38
|
+
<div className="mt-2 wrap-break-word text-sm font-medium">{group.model}</div>
|
|
39
39
|
<div className="mt-1 text-xs text-muted-foreground">
|
|
40
40
|
{group.provider} / {group.tool}
|
|
41
41
|
</div>
|
|
@@ -144,7 +144,7 @@ export function RepairItemsTable({
|
|
|
144
144
|
/>
|
|
145
145
|
<RepairItemsMobileList groups={workbench.groups} focusKey={focusKey} rangeLinkParams={rangeLinkParams} />
|
|
146
146
|
<div className="hidden overflow-x-auto md:block">
|
|
147
|
-
<Table className="min-w-
|
|
147
|
+
<Table className="min-w-336">
|
|
148
148
|
<TableHeader>
|
|
149
149
|
<TableRow>
|
|
150
150
|
<TableHead>State</TableHead>
|
|
@@ -84,7 +84,7 @@ export function RepairStateControl({
|
|
|
84
84
|
<Badge variant={statusVariant(status)}>{status}</Badge>
|
|
85
85
|
<select
|
|
86
86
|
aria-label="Repair state"
|
|
87
|
-
className="h-8 w-full rounded-md border bg-card px-2 text-xs font-medium focus-visible:outline-
|
|
87
|
+
className="h-8 w-full rounded-md border bg-card px-2 text-xs font-medium focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
|
|
88
88
|
value={status}
|
|
89
89
|
disabled={isPending}
|
|
90
90
|
onChange={(event) => {
|
|
@@ -100,7 +100,7 @@ export function RepairStateControl({
|
|
|
100
100
|
</select>
|
|
101
101
|
<input
|
|
102
102
|
aria-label="Repair note"
|
|
103
|
-
className="h-8 w-full rounded-md border bg-card px-2 text-xs focus-visible:outline-
|
|
103
|
+
className="h-8 w-full rounded-md border bg-card px-2 text-xs focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
|
|
104
104
|
value={notes}
|
|
105
105
|
maxLength={500}
|
|
106
106
|
disabled={isPending}
|
|
@@ -193,7 +193,7 @@ export function ScanHealthSummary({ health }: { health: ScanHealth }) {
|
|
|
193
193
|
</div>
|
|
194
194
|
<ul className="space-y-1 text-xs leading-relaxed text-muted-foreground">
|
|
195
195
|
{health.latestNoteGroups.map((group) => (
|
|
196
|
-
<li key={`${group.severity}-${group.message}`} className="break-
|
|
196
|
+
<li key={`${group.severity}-${group.message}`} className="wrap-break-word">
|
|
197
197
|
<span className="font-medium text-foreground">{group.message}</span>
|
|
198
198
|
<span className="ml-2 text-muted-foreground">
|
|
199
199
|
{plural(group.count, "file")}
|
|
@@ -114,10 +114,10 @@ export function ScanNowButton({
|
|
|
114
114
|
{isPending ? "Scanning..." : label}
|
|
115
115
|
</Button>
|
|
116
116
|
{status ? (
|
|
117
|
-
<div role="status" aria-live="polite" className={cn("max-w-
|
|
117
|
+
<div role="status" aria-live="polite" className={cn("max-w-104 text-xs leading-5", statusClassName)}>
|
|
118
118
|
<p className={scanStatusClassName(status.tone)}>{status.message}</p>
|
|
119
119
|
{status.result ? (
|
|
120
|
-
<div className="mt-2 rounded-md border bg-card p-2 text-left text-muted-foreground shadow-
|
|
120
|
+
<div className="mt-2 rounded-md border bg-card p-2 text-left text-muted-foreground shadow-xs">
|
|
121
121
|
<div className="grid grid-cols-2 gap-x-3 gap-y-1">
|
|
122
122
|
<span>{status.result.filesScanned.toLocaleString()} files checked</span>
|
|
123
123
|
<span>{status.result.recordsImported.toLocaleString()} records imported</span>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useMemo, useState } from "react";
|
|
4
4
|
import Link from "next/link";
|
|
5
5
|
import { BookmarkPlus, Download, Filter, RotateCcw, Trash2 } from "lucide-react";
|
|
6
6
|
import type { SessionRow } from "@/src/lib/analytics";
|
|
@@ -93,6 +93,11 @@ export function SessionExplorer({
|
|
|
93
93
|
() => ({ query, tool, model, project, exact, cost, from, to, highCost, hasCache }),
|
|
94
94
|
[cost, exact, from, hasCache, highCost, model, project, query, to, tool]
|
|
95
95
|
);
|
|
96
|
+
const [prevFilterState, setPrevFilterState] = useState(filterState);
|
|
97
|
+
if (filterState !== prevFilterState) {
|
|
98
|
+
setPrevFilterState(filterState);
|
|
99
|
+
setPage(1);
|
|
100
|
+
}
|
|
96
101
|
const filtered = useMemo(
|
|
97
102
|
() => filterSessions(sessions, filterState, highCostThreshold),
|
|
98
103
|
[filterState, highCostThreshold, sessions]
|
|
@@ -106,10 +111,6 @@ export function SessionExplorer({
|
|
|
106
111
|
const activeFilters = useMemo(() => getActiveSessionFilters(filterState), [filterState]);
|
|
107
112
|
const currentFilters = useMemo(() => getCurrentSessionFilters(filterState), [filterState]);
|
|
108
113
|
|
|
109
|
-
useEffect(() => {
|
|
110
|
-
setPage(1);
|
|
111
|
-
}, [filterState]);
|
|
112
|
-
|
|
113
114
|
function clearFilters() {
|
|
114
115
|
setQuery("");
|
|
115
116
|
setTool("all");
|
|
@@ -431,9 +432,9 @@ export function SessionExplorer({
|
|
|
431
432
|
</div>
|
|
432
433
|
</div>
|
|
433
434
|
{filtered.length ? (
|
|
434
|
-
<div className="table-scroll max-h-
|
|
435
|
-
<Table className={cn("min-w-
|
|
436
|
-
<TableHeader className="sticky top-0 z-10 bg-background shadow-
|
|
435
|
+
<div className="table-scroll max-h-152 overflow-x-auto">
|
|
436
|
+
<Table className={cn("min-w-312", tableDensityClass)}>
|
|
437
|
+
<TableHeader className="sticky top-0 z-10 bg-background shadow-xs">
|
|
437
438
|
<TableRow>
|
|
438
439
|
<TableHead className="w-28">Date</TableHead>
|
|
439
440
|
<TableHead className="w-28">Tool</TableHead>
|
|
@@ -59,7 +59,7 @@ export function ImportProfilesSection({
|
|
|
59
59
|
</div>
|
|
60
60
|
<div className="mt-2 flex flex-wrap gap-1">
|
|
61
61
|
{profile.matchers.map((matcher) => (
|
|
62
|
-
<code key={matcher} className="rounded bg-muted px-1.5 py-0.5 text-xs">{matcher}</code>
|
|
62
|
+
<code key={matcher} className="rounded-sm bg-muted px-1.5 py-0.5 text-xs">{matcher}</code>
|
|
63
63
|
))}
|
|
64
64
|
</div>
|
|
65
65
|
<div className="mt-3 flex flex-wrap gap-2">
|
|
@@ -126,7 +126,7 @@ export function ImportProfilesSection({
|
|
|
126
126
|
<FieldLabel>Matchers</FieldLabel>
|
|
127
127
|
<div className="mt-1 flex flex-wrap gap-1">
|
|
128
128
|
{previewResult.recommendedMatchers.map((matcher) => (
|
|
129
|
-
<code key={matcher} className="rounded bg-muted px-1.5 py-0.5 text-xs">{matcher}</code>
|
|
129
|
+
<code key={matcher} className="rounded-sm bg-muted px-1.5 py-0.5 text-xs">{matcher}</code>
|
|
130
130
|
))}
|
|
131
131
|
</div>
|
|
132
132
|
</div>
|
|
@@ -22,7 +22,7 @@ export function SettingsSectionNav() {
|
|
|
22
22
|
return (
|
|
23
23
|
<nav
|
|
24
24
|
aria-label="Settings sections"
|
|
25
|
-
className="sticky top-2 z-20 rounded-lg border bg-background/95 p-2 shadow-
|
|
25
|
+
className="sticky top-2 z-20 rounded-lg border bg-background/95 p-2 shadow-xs backdrop-blur-sm supports-backdrop-filter:bg-background/80"
|
|
26
26
|
>
|
|
27
27
|
<div className="flex items-center gap-2 overflow-x-auto pb-1 sm:pb-0">
|
|
28
28
|
<div className="sticky left-0 shrink-0 bg-background/95 pr-2 text-xs font-semibold text-foreground">
|
|
@@ -33,7 +33,7 @@ export function SettingsSectionNav() {
|
|
|
33
33
|
key={section.id}
|
|
34
34
|
href={`#${section.id}`}
|
|
35
35
|
title={section.detail}
|
|
36
|
-
className="inline-flex h-8 shrink-0 items-center rounded-md border bg-card px-3 text-xs font-medium text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-
|
|
36
|
+
className="inline-flex h-8 shrink-0 items-center rounded-md border bg-card px-3 text-xs font-medium text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
|
37
37
|
>
|
|
38
38
|
{section.label}
|
|
39
39
|
</a>
|
package/components/sidebar.tsx
CHANGED
|
@@ -64,7 +64,7 @@ function NavLink({
|
|
|
64
64
|
? cn(
|
|
65
65
|
"flex shrink-0 items-center gap-2 rounded-md border px-3 py-2 text-xs transition-colors",
|
|
66
66
|
isActive
|
|
67
|
-
? "border-primary/30 bg-primary/10 font-medium text-primary shadow-
|
|
67
|
+
? "border-primary/30 bg-primary/10 font-medium text-primary shadow-xs"
|
|
68
68
|
: variant === "support"
|
|
69
69
|
? "bg-muted/50 font-medium text-foreground"
|
|
70
70
|
: "text-muted-foreground"
|
|
@@ -107,7 +107,7 @@ function MobileIconLink({
|
|
|
107
107
|
className={cn(
|
|
108
108
|
"flex h-10 w-10 shrink-0 items-center justify-center rounded-md border transition-colors",
|
|
109
109
|
isActive
|
|
110
|
-
? "border-primary/30 bg-primary/10 text-primary shadow-
|
|
110
|
+
? "border-primary/30 bg-primary/10 text-primary shadow-xs"
|
|
111
111
|
: "bg-card text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
112
112
|
)}
|
|
113
113
|
>
|
package/components/ui/badge.tsx
CHANGED
|
@@ -3,7 +3,7 @@ import { cva, type VariantProps } from "class-variance-authority";
|
|
|
3
3
|
import { cn } from "@/src/lib/utils";
|
|
4
4
|
|
|
5
5
|
const badgeVariants = cva(
|
|
6
|
-
"inline-flex items-center rounded-md px-2 py-1 text-xs font-medium leading-none outline outline-1 -outline-offset-1",
|
|
6
|
+
"inline-flex items-center rounded-md px-2 py-1 text-xs font-medium leading-none outline-solid outline-1 -outline-offset-1",
|
|
7
7
|
{
|
|
8
8
|
variants: {
|
|
9
9
|
variant: {
|
package/components/ui/button.tsx
CHANGED
|
@@ -4,7 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority";
|
|
|
4
4
|
import { cn } from "@/src/lib/utils";
|
|
5
5
|
|
|
6
6
|
const buttonVariants = cva(
|
|
7
|
-
"inline-flex h-9 items-center justify-center gap-2 whitespace-nowrap rounded-md px-3 py-2 text-sm font-medium leading-none transition-colors focus-visible:outline-
|
|
7
|
+
"inline-flex h-9 items-center justify-center gap-2 whitespace-nowrap rounded-md px-3 py-2 text-sm font-medium leading-none transition-colors focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
|
|
8
8
|
{
|
|
9
9
|
variants: {
|
|
10
10
|
variant: {
|
|
@@ -19,7 +19,7 @@ export function HelpTooltip({
|
|
|
19
19
|
type="button"
|
|
20
20
|
aria-label={`${label} details`}
|
|
21
21
|
aria-describedby={id}
|
|
22
|
-
className="inline-flex h-5 w-5 items-center justify-center rounded-
|
|
22
|
+
className="inline-flex h-5 w-5 items-center justify-center rounded-xs text-muted-foreground outline-hidden transition-colors hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
|
23
23
|
>
|
|
24
24
|
<Info className="h-3.5 w-3.5" aria-hidden="true" />
|
|
25
25
|
</button>
|
package/components/ui/input.tsx
CHANGED
|
@@ -7,7 +7,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, type,
|
|
|
7
7
|
<input
|
|
8
8
|
type={type}
|
|
9
9
|
className={cn(
|
|
10
|
-
"flex h-9 w-full rounded-md border bg-card px-3 py-1 text-sm leading-5 shadow-
|
|
10
|
+
"flex h-9 w-full rounded-md border bg-card px-3 py-1 text-sm leading-5 shadow-xs tabular-nums transition-colors placeholder:text-muted-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
|
11
11
|
className
|
|
12
12
|
)}
|
|
13
13
|
ref={ref}
|
|
@@ -6,7 +6,7 @@ export type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>;
|
|
|
6
6
|
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(({ className, ...props }, ref) => (
|
|
7
7
|
<textarea
|
|
8
8
|
className={cn(
|
|
9
|
-
"flex min-h-24 w-full rounded-md border bg-card px-3 py-2 text-sm leading-6 shadow-
|
|
9
|
+
"flex min-h-24 w-full rounded-md border bg-card px-3 py-2 text-sm leading-6 shadow-xs placeholder:text-muted-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
|
|
10
10
|
className
|
|
11
11
|
)}
|
|
12
12
|
ref={ref}
|
|
@@ -16,7 +16,7 @@ export function PageHeader({
|
|
|
16
16
|
<div className={cn("flex w-full max-w-full flex-col justify-between gap-4 sm:flex-row sm:items-end", className)}>
|
|
17
17
|
<div className="min-w-0 max-w-full">
|
|
18
18
|
<h1 className="text-2xl font-semibold leading-tight tracking-normal text-foreground">{title}</h1>
|
|
19
|
-
<p className="mt-1 max-w-full break-
|
|
19
|
+
<p className="mt-1 max-w-full wrap-break-word text-sm leading-6 text-muted-foreground sm:max-w-[65ch]">{description}</p>
|
|
20
20
|
</div>
|
|
21
21
|
{actions ? <div className="flex w-full min-w-0 flex-wrap gap-2 sm:w-auto sm:shrink-0 sm:justify-end">{actions}</div> : null}
|
|
22
22
|
</div>
|
package/dist/runtime/scan.mjs
CHANGED
|
@@ -2909,12 +2909,15 @@ import Database2 from "better-sqlite3";
|
|
|
2909
2909
|
function openReadonly(filePath) {
|
|
2910
2910
|
return new Database2(filePath, { readonly: true, fileMustExist: true });
|
|
2911
2911
|
}
|
|
2912
|
+
function quoteIdent(name) {
|
|
2913
|
+
return `"${name.replace(/"/g, '""')}"`;
|
|
2914
|
+
}
|
|
2912
2915
|
function candidateTables(db2) {
|
|
2913
2916
|
const tables = db2.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'").all();
|
|
2914
2917
|
return tables.map((table) => table.name).filter((name) => usageTableNames.includes(name) || /usage|token|message|session/i.test(name));
|
|
2915
2918
|
}
|
|
2916
2919
|
function columnsFor(db2, tableName) {
|
|
2917
|
-
const columns = db2.prepare(`PRAGMA table_info(${
|
|
2920
|
+
const columns = db2.prepare(`PRAGMA table_info(${quoteIdent(tableName)})`).all();
|
|
2918
2921
|
return new Set(columns.map((column) => column.name.toLowerCase()));
|
|
2919
2922
|
}
|
|
2920
2923
|
function hasUsageShape(columns) {
|
|
@@ -2923,7 +2926,7 @@ function hasUsageShape(columns) {
|
|
|
2923
2926
|
return hasModel && hasUsage2;
|
|
2924
2927
|
}
|
|
2925
2928
|
function tableRows(db2, tableName) {
|
|
2926
|
-
return db2.prepare(`SELECT * FROM ${
|
|
2929
|
+
return db2.prepare(`SELECT * FROM ${quoteIdent(tableName)} LIMIT 50000`).all();
|
|
2927
2930
|
}
|
|
2928
2931
|
function providerId(value) {
|
|
2929
2932
|
if (!value) return "local";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tokentrace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.1",
|
|
4
4
|
"mcpName": "io.github.abhiyoheswaran1/tokentrace",
|
|
5
5
|
"description": "Local-first dashboard for AI CLI token, cost, and session analytics.",
|
|
6
6
|
"author": {
|
|
@@ -40,7 +40,6 @@
|
|
|
40
40
|
"fixtures",
|
|
41
41
|
"next.config.mjs",
|
|
42
42
|
"postcss.config.mjs",
|
|
43
|
-
"tailwind.config.ts",
|
|
44
43
|
"tsconfig.json",
|
|
45
44
|
"components.json",
|
|
46
45
|
"README.md"
|
|
@@ -74,10 +73,10 @@
|
|
|
74
73
|
"prepack": "npm run build:runtime"
|
|
75
74
|
},
|
|
76
75
|
"overrides": {
|
|
77
|
-
"postcss": "^8.5.
|
|
76
|
+
"postcss": "^8.5.15"
|
|
78
77
|
},
|
|
79
78
|
"engines": {
|
|
80
|
-
"node": ">=
|
|
79
|
+
"node": ">=20.9.0"
|
|
81
80
|
},
|
|
82
81
|
"keywords": [
|
|
83
82
|
"tokens",
|
|
@@ -94,34 +93,36 @@
|
|
|
94
93
|
],
|
|
95
94
|
"license": "MIT",
|
|
96
95
|
"dependencies": {
|
|
97
|
-
"@types/node": "^22.10.5",
|
|
98
|
-
"@types/react": "^19.0.4",
|
|
99
96
|
"@radix-ui/react-slot": "^1.1.1",
|
|
100
|
-
"
|
|
97
|
+
"@tailwindcss/postcss": "^4.3.0",
|
|
98
|
+
"@types/node": "^22.19.19",
|
|
99
|
+
"@types/react": "^19.2.15",
|
|
100
|
+
"better-sqlite3": "^12.10.0",
|
|
101
101
|
"class-variance-authority": "^0.7.1",
|
|
102
102
|
"clsx": "^2.1.1",
|
|
103
103
|
"drizzle-orm": "^0.45.2",
|
|
104
104
|
"get-port": "^7.2.0",
|
|
105
|
-
"lucide-react": "^
|
|
106
|
-
"next": "^
|
|
107
|
-
"open": "^
|
|
108
|
-
"
|
|
109
|
-
"postcss": "^8.5.14",
|
|
105
|
+
"lucide-react": "^1.16.0",
|
|
106
|
+
"next": "^16.2.6",
|
|
107
|
+
"open": "^11.0.0",
|
|
108
|
+
"postcss": "^8.5.15",
|
|
110
109
|
"react": "^19.0.0",
|
|
111
110
|
"react-dom": "^19.0.0",
|
|
112
|
-
"recharts": "^
|
|
113
|
-
"
|
|
114
|
-
"
|
|
115
|
-
"typescript": "^
|
|
111
|
+
"recharts": "^3.8.1",
|
|
112
|
+
"tailwind-merge": "^3.6.0",
|
|
113
|
+
"tailwindcss": "^4.3.0",
|
|
114
|
+
"typescript": "^6.0.3"
|
|
116
115
|
},
|
|
117
116
|
"devDependencies": {
|
|
118
117
|
"@playwright/test": "^1.60.0",
|
|
119
118
|
"@types/better-sqlite3": "^7.6.12",
|
|
120
119
|
"@types/react-dom": "^19.0.2",
|
|
120
|
+
"@vitejs/plugin-react": "^5.2.0",
|
|
121
121
|
"esbuild": "^0.28.0",
|
|
122
|
-
"eslint": "^9.
|
|
123
|
-
"eslint-config-next": "^
|
|
124
|
-
"
|
|
125
|
-
"
|
|
122
|
+
"eslint": "^9.39.4",
|
|
123
|
+
"eslint-config-next": "^16.2.6",
|
|
124
|
+
"eslint-plugin-react-hooks": "^5.2.0",
|
|
125
|
+
"tsx": "^4.22.3",
|
|
126
|
+
"vitest": "^4.1.7"
|
|
126
127
|
}
|
|
127
128
|
}
|
package/postcss.config.mjs
CHANGED
|
@@ -40,16 +40,16 @@ try {
|
|
|
40
40
|
);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
if (packageJson.dependencies?.next !== "^
|
|
44
|
-
fail("Keep the published Next.js dependency floor at ^
|
|
43
|
+
if (packageJson.dependencies?.next !== "^16.2.6") {
|
|
44
|
+
fail("Keep the published Next.js dependency floor at ^16.2.6 or newer.");
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
if (packageJson.dependencies?.["drizzle-orm"] !== "^0.45.2") {
|
|
48
48
|
fail("Keep the published drizzle-orm dependency floor at ^0.45.2 or newer.");
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
if (packageJson.overrides?.postcss !== "^8.5.
|
|
52
|
-
fail("Keep the PostCSS override at ^8.5.
|
|
51
|
+
if (packageJson.overrides?.postcss !== "^8.5.15") {
|
|
52
|
+
fail("Keep the PostCSS override at ^8.5.15 or newer.");
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
const blockedPackageEntries = [
|
package/server.json
CHANGED
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
"url": "https://github.com/abhiyoheswaran1/tokentrace",
|
|
9
9
|
"source": "github"
|
|
10
10
|
},
|
|
11
|
-
"version": "0.
|
|
11
|
+
"version": "0.15.1",
|
|
12
12
|
"packages": [
|
|
13
13
|
{
|
|
14
14
|
"registryType": "npm",
|
|
15
15
|
"identifier": "tokentrace",
|
|
16
|
-
"version": "0.
|
|
16
|
+
"version": "0.15.1",
|
|
17
17
|
"runtimeHint": "npx",
|
|
18
18
|
"packageArguments": [
|
|
19
19
|
{
|
package/src/cli/runtime.js
CHANGED
|
@@ -31,7 +31,6 @@ function copyDashboardSource(context, targetRoot) {
|
|
|
31
31
|
"next.config.mjs",
|
|
32
32
|
"package.json",
|
|
33
33
|
"postcss.config.mjs",
|
|
34
|
-
"tailwind.config.ts",
|
|
35
34
|
"tsconfig.json"
|
|
36
35
|
];
|
|
37
36
|
|
|
@@ -63,7 +62,7 @@ function copyDashboardSource(context, targetRoot) {
|
|
|
63
62
|
|
|
64
63
|
function runNextBuild(context, cwd) {
|
|
65
64
|
return new Promise((resolve, reject) => {
|
|
66
|
-
const child = spawn(process.execPath, [context.nextBin(), "build"], {
|
|
65
|
+
const child = spawn(process.execPath, [context.nextBin(), "build", "--webpack"], {
|
|
67
66
|
cwd,
|
|
68
67
|
env: context.runtimeEnv(),
|
|
69
68
|
stdio: "inherit"
|
|
@@ -24,6 +24,10 @@ function openReadonly(filePath: string) {
|
|
|
24
24
|
return new Database(filePath, { readonly: true, fileMustExist: true });
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
function quoteIdent(name: string) {
|
|
28
|
+
return `"${name.replace(/"/g, '""')}"`;
|
|
29
|
+
}
|
|
30
|
+
|
|
27
31
|
function candidateTables(db: Database.Database) {
|
|
28
32
|
const tables = db
|
|
29
33
|
.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'")
|
|
@@ -34,7 +38,7 @@ function candidateTables(db: Database.Database) {
|
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
function columnsFor(db: Database.Database, tableName: string) {
|
|
37
|
-
const columns = db.prepare(`PRAGMA table_info(${
|
|
41
|
+
const columns = db.prepare(`PRAGMA table_info(${quoteIdent(tableName)})`).all() as Array<{ name: string }>;
|
|
38
42
|
return new Set(columns.map((column) => column.name.toLowerCase()));
|
|
39
43
|
}
|
|
40
44
|
|
|
@@ -52,7 +56,7 @@ function hasUsageShape(columns: Set<string>) {
|
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
function tableRows(db: Database.Database, tableName: string) {
|
|
55
|
-
return db.prepare(`SELECT * FROM ${
|
|
59
|
+
return db.prepare(`SELECT * FROM ${quoteIdent(tableName)} LIMIT 50000`).all() as UsageRow[];
|
|
56
60
|
}
|
|
57
61
|
|
|
58
62
|
function providerId(value: string | null) {
|
package/tsconfig.json
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"compilerOptions": {
|
|
3
3
|
"target": "ES2022",
|
|
4
|
-
"lib": [
|
|
4
|
+
"lib": [
|
|
5
|
+
"dom",
|
|
6
|
+
"dom.iterable",
|
|
7
|
+
"es2022"
|
|
8
|
+
],
|
|
5
9
|
"allowJs": false,
|
|
6
10
|
"skipLibCheck": true,
|
|
7
11
|
"strict": true,
|
|
@@ -11,11 +15,14 @@
|
|
|
11
15
|
"moduleResolution": "bundler",
|
|
12
16
|
"resolveJsonModule": true,
|
|
13
17
|
"isolatedModules": true,
|
|
14
|
-
"jsx": "
|
|
18
|
+
"jsx": "react-jsx",
|
|
15
19
|
"incremental": true,
|
|
16
20
|
"baseUrl": ".",
|
|
21
|
+
"ignoreDeprecations": "6.0",
|
|
17
22
|
"paths": {
|
|
18
|
-
"@/*": [
|
|
23
|
+
"@/*": [
|
|
24
|
+
"./*"
|
|
25
|
+
]
|
|
19
26
|
},
|
|
20
27
|
"plugins": [
|
|
21
28
|
{
|
|
@@ -23,6 +30,14 @@
|
|
|
23
30
|
}
|
|
24
31
|
]
|
|
25
32
|
},
|
|
26
|
-
"include": [
|
|
27
|
-
|
|
33
|
+
"include": [
|
|
34
|
+
"next-env.d.ts",
|
|
35
|
+
"**/*.ts",
|
|
36
|
+
"**/*.tsx",
|
|
37
|
+
".next/types/**/*.ts",
|
|
38
|
+
".next/dev/types/**/*.ts"
|
|
39
|
+
],
|
|
40
|
+
"exclude": [
|
|
41
|
+
"node_modules"
|
|
42
|
+
]
|
|
28
43
|
}
|
package/tailwind.config.ts
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import type { Config } from "tailwindcss";
|
|
2
|
-
|
|
3
|
-
const config: Config = {
|
|
4
|
-
darkMode: ["class"],
|
|
5
|
-
content: [
|
|
6
|
-
"./app/**/*.{ts,tsx}",
|
|
7
|
-
"./components/**/*.{ts,tsx}",
|
|
8
|
-
"./src/**/*.{ts,tsx}"
|
|
9
|
-
],
|
|
10
|
-
theme: {
|
|
11
|
-
extend: {
|
|
12
|
-
colors: {
|
|
13
|
-
border: "hsl(var(--border))",
|
|
14
|
-
input: "hsl(var(--input))",
|
|
15
|
-
ring: "hsl(var(--ring))",
|
|
16
|
-
background: "hsl(var(--background))",
|
|
17
|
-
foreground: "hsl(var(--foreground))",
|
|
18
|
-
primary: {
|
|
19
|
-
DEFAULT: "hsl(var(--primary))",
|
|
20
|
-
foreground: "hsl(var(--primary-foreground))"
|
|
21
|
-
},
|
|
22
|
-
secondary: {
|
|
23
|
-
DEFAULT: "hsl(var(--secondary))",
|
|
24
|
-
foreground: "hsl(var(--secondary-foreground))"
|
|
25
|
-
},
|
|
26
|
-
muted: {
|
|
27
|
-
DEFAULT: "hsl(var(--muted))",
|
|
28
|
-
foreground: "hsl(var(--muted-foreground))"
|
|
29
|
-
},
|
|
30
|
-
accent: {
|
|
31
|
-
DEFAULT: "hsl(var(--accent))",
|
|
32
|
-
foreground: "hsl(var(--accent-foreground))"
|
|
33
|
-
},
|
|
34
|
-
destructive: {
|
|
35
|
-
DEFAULT: "hsl(var(--destructive))",
|
|
36
|
-
foreground: "hsl(var(--destructive-foreground))"
|
|
37
|
-
},
|
|
38
|
-
card: {
|
|
39
|
-
DEFAULT: "hsl(var(--card))",
|
|
40
|
-
foreground: "hsl(var(--card-foreground))"
|
|
41
|
-
}
|
|
42
|
-
},
|
|
43
|
-
borderRadius: {
|
|
44
|
-
lg: "0.5rem",
|
|
45
|
-
md: "0.375rem",
|
|
46
|
-
sm: "0.25rem"
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
},
|
|
50
|
-
plugins: []
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
export default config;
|