@linxiraos/pi-stats 1.1.7 → 1.1.9
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 +4 -4
- package/package.json +87 -94
- package/src/client/App.tsx +11 -1
- package/src/client/api.ts +23 -0
- package/src/client/app/routes.ts +7 -0
- package/src/client/data/useHashRoute.ts +28 -10
- package/src/client/routes/ProvidersRoute.tsx +1 -0
- package/src/client/routes/TracesRoute.tsx +182 -0
- package/src/client/routes/index.ts +1 -0
- package/src/client/styles.css +433 -111
- package/src/client/traces/AggregatesPanel.tsx +88 -0
- package/src/client/traces/Minimap.tsx +177 -0
- package/src/client/traces/SpanDrawer.tsx +309 -0
- package/src/client/traces/SummaryStrip.tsx +37 -0
- package/src/client/traces/TimelineCanvas.tsx +768 -0
- package/src/client/traces/TraceView.tsx +344 -0
- package/src/client/traces/TranscriptList.tsx +166 -0
- package/src/client/traces/time-scale.ts +250 -0
- package/src/client/traces/trace-colors.ts +77 -0
- package/src/db.ts +77 -0
- package/src/gain-aggregator.ts +1 -1
- package/src/index.ts +1 -6
- package/src/parser.ts +55 -2
- package/src/server.ts +69 -17
- package/src/shared-types.ts +138 -0
- package/src/trace.ts +1002 -0
- package/src/types.ts +45 -1
- package/THIRD-PARTY-NOTICES.txt +0 -22909
- package/dist/client/index.css +0 -1
- package/dist/client/index.html +0 -24
- package/dist/client/index.js +0 -257
- package/dist/client/styles.css +0 -1692
- package/dist/types/aggregator.d.ts +0 -103
- package/dist/types/client/App.d.ts +0 -1
- package/dist/types/client/api.d.ts +0 -22
- package/dist/types/client/app/AppLayout.d.ts +0 -16
- package/dist/types/client/app/NavRail.d.ts +0 -7
- package/dist/types/client/app/RangeControl.d.ts +0 -7
- package/dist/types/client/app/SyncButton.d.ts +0 -14
- package/dist/types/client/app/ThemeToggle.d.ts +0 -1
- package/dist/types/client/app/TopBar.d.ts +0 -15
- package/dist/types/client/app/routes.d.ts +0 -12
- package/dist/types/client/components/AgentTokenShare.d.ts +0 -5
- package/dist/types/client/components/chart-shared.d.ts +0 -178
- package/dist/types/client/components/models-table-shared.d.ts +0 -175
- package/dist/types/client/components/range-meta.d.ts +0 -21
- package/dist/types/client/data/charts.d.ts +0 -1
- package/dist/types/client/data/formatters.d.ts +0 -13
- package/dist/types/client/data/useHashRoute.d.ts +0 -8
- package/dist/types/client/data/useResource.d.ts +0 -13
- package/dist/types/client/data/view-models.d.ts +0 -75
- package/dist/types/client/index.d.ts +0 -2
- package/dist/types/client/routes/BehaviorRoute.d.ts +0 -7
- package/dist/types/client/routes/CostsRoute.d.ts +0 -7
- package/dist/types/client/routes/ErrorsRoute.d.ts +0 -8
- package/dist/types/client/routes/GainRoute.d.ts +0 -7
- package/dist/types/client/routes/ModelsRoute.d.ts +0 -7
- package/dist/types/client/routes/OverviewRoute.d.ts +0 -8
- package/dist/types/client/routes/ProjectsRoute.d.ts +0 -7
- package/dist/types/client/routes/ProvidersRoute.d.ts +0 -7
- package/dist/types/client/routes/RequestsRoute.d.ts +0 -8
- package/dist/types/client/routes/ToolsRoute.d.ts +0 -7
- package/dist/types/client/routes/index.d.ts +0 -10
- package/dist/types/client/types.d.ts +0 -63
- package/dist/types/client/ui/AsyncBoundary.d.ts +0 -12
- package/dist/types/client/ui/DataTable.d.ts +0 -17
- package/dist/types/client/ui/EmptyState.d.ts +0 -7
- package/dist/types/client/ui/ErrorState.d.ts +0 -6
- package/dist/types/client/ui/JsonBlock.d.ts +0 -7
- package/dist/types/client/ui/MetricCluster.d.ts +0 -5
- package/dist/types/client/ui/Panel.d.ts +0 -7
- package/dist/types/client/ui/RequestDrawer.d.ts +0 -5
- package/dist/types/client/ui/SegmentedControl.d.ts +0 -12
- package/dist/types/client/ui/Skeleton.d.ts +0 -8
- package/dist/types/client/ui/StatusPill.d.ts +0 -7
- package/dist/types/client/ui/index.d.ts +0 -11
- package/dist/types/client/useSystemTheme.d.ts +0 -11
- package/dist/types/db.d.ts +0 -160
- package/dist/types/embedded-client.d.ts +0 -18
- package/dist/types/gain-aggregator.d.ts +0 -26
- package/dist/types/index.d.ts +0 -17
- package/dist/types/parser.d.ts +0 -53
- package/dist/types/port-conflict.d.ts +0 -16
- package/dist/types/server.d.ts +0 -14
- package/dist/types/shared-types.d.ts +0 -421
- package/dist/types/sync-worker.d.ts +0 -31
- package/dist/types/types.d.ts +0 -164
- package/dist/types/usage-windows.d.ts +0 -72
- package/dist/types/user-metrics.d.ts +0 -81
package/CHANGELOG.md
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
-
## [1.1.
|
|
5
|
+
## [1.1.9] - 2026-09-05
|
|
6
6
|
|
|
7
|
-
-
|
|
7
|
+
- v18.1.10 sync baseline (stats tracking panel updates).
|
|
8
8
|
|
|
9
|
-
## [1.1.
|
|
9
|
+
## [1.1.8] - 2026-09-04
|
|
10
10
|
|
|
11
|
-
-
|
|
11
|
+
- OMP sync v18.1.2–v18.1.5: `/trace` tracking panel, compaction summary persistence, and stats-sync worker alignment.
|
|
12
12
|
|
|
13
13
|
## [1.1.5] - 2026-08-26
|
|
14
14
|
|
package/package.json
CHANGED
|
@@ -1,96 +1,89 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
},
|
|
90
|
-
"./client/components/*": {
|
|
91
|
-
"types": "./dist/types/client/components/*.d.ts",
|
|
92
|
-
"import": "./src/client/components/*.ts"
|
|
93
|
-
},
|
|
94
|
-
"./*.js": "./src/*.ts"
|
|
95
|
-
}
|
|
2
|
+
"exports": {
|
|
3
|
+
"./*.js": "./src/*.ts",
|
|
4
|
+
"./client/components/*": {
|
|
5
|
+
"import": "./src/client/components/*.ts",
|
|
6
|
+
"types": "./src/client/components/*.ts"
|
|
7
|
+
},
|
|
8
|
+
"./client/*": {
|
|
9
|
+
"import": "./src/client/*.ts",
|
|
10
|
+
"types": "./src/client/*.ts"
|
|
11
|
+
},
|
|
12
|
+
"./client": {
|
|
13
|
+
"import": "./src/client/index.tsx",
|
|
14
|
+
"types": "./src/client/index.tsx"
|
|
15
|
+
},
|
|
16
|
+
"./*": {
|
|
17
|
+
"import": "./src/*.ts",
|
|
18
|
+
"types": "./src/*.ts"
|
|
19
|
+
},
|
|
20
|
+
".": {
|
|
21
|
+
"import": "./src/index.ts",
|
|
22
|
+
"types": "./src/index.ts"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"src",
|
|
27
|
+
"build.ts",
|
|
28
|
+
"tailwind.config.js",
|
|
29
|
+
"README.md",
|
|
30
|
+
"CHANGELOG.md"
|
|
31
|
+
],
|
|
32
|
+
"engines": {
|
|
33
|
+
"bun": ">=1.3.14"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"postcss": "^8.5.16",
|
|
37
|
+
"@types/react-dom": "^19.2.3",
|
|
38
|
+
"@types/react": "^19.2.17",
|
|
39
|
+
"@types/bun": "^1.3.14"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@linxiraos/pi-utils": "1.1.9",
|
|
43
|
+
"@linxiraos/pi-catalog": "1.1.9",
|
|
44
|
+
"@linxiraos/pi-ai": "1.1.9",
|
|
45
|
+
"tailwindcss": "^4.3.2",
|
|
46
|
+
"react-dom": "19.2.7",
|
|
47
|
+
"react-chartjs-2": "^5.3.1",
|
|
48
|
+
"react": "19.2.7",
|
|
49
|
+
"lucide-react": "^1.24.0",
|
|
50
|
+
"chart.js": "^4.5.1",
|
|
51
|
+
"@tailwindcss/node": "^4.3.2"
|
|
52
|
+
},
|
|
53
|
+
"scripts": {
|
|
54
|
+
"check:types": "tsgo -p tsconfig.json --noEmit && tsgo -p tsconfig.client.json --noEmit",
|
|
55
|
+
"check": "tsgo -p tsconfig.json --noEmit",
|
|
56
|
+
"dev": "bun run src/index.ts",
|
|
57
|
+
"build": "bun run build.ts",
|
|
58
|
+
"gen:stats:reset": "bun scripts/generate-client-bundle.ts --reset",
|
|
59
|
+
"gen:stats": "bun scripts/generate-client-bundle.ts --generate"
|
|
60
|
+
},
|
|
61
|
+
"bin": {
|
|
62
|
+
"zeta-stats": "./src/index.ts"
|
|
63
|
+
},
|
|
64
|
+
"types": "./src/index.ts",
|
|
65
|
+
"main": "./src/index.ts",
|
|
66
|
+
"keywords": [
|
|
67
|
+
"ai",
|
|
68
|
+
"observability",
|
|
69
|
+
"metrics",
|
|
70
|
+
"dashboard",
|
|
71
|
+
"llm",
|
|
72
|
+
"statistics"
|
|
73
|
+
],
|
|
74
|
+
"bugs": {
|
|
75
|
+
"url": "https://github.com/Linxira-OS/linxira-zeta/issues"
|
|
76
|
+
},
|
|
77
|
+
"repository": {
|
|
78
|
+
"directory": "packages/stats",
|
|
79
|
+
"url": "git+https://github.com/Linxira-OS/linxira-zeta.git",
|
|
80
|
+
"type": "git"
|
|
81
|
+
},
|
|
82
|
+
"license": "MIT",
|
|
83
|
+
"author": "Can Boluk",
|
|
84
|
+
"homepage": "https://linxira-os.github.io/zeta/",
|
|
85
|
+
"description": "Local observability dashboard for pi AI usage statistics",
|
|
86
|
+
"version": "1.1.9",
|
|
87
|
+
"name": "@linxiraos/pi-stats",
|
|
88
|
+
"type": "module"
|
|
96
89
|
}
|
package/src/client/App.tsx
CHANGED
|
@@ -13,11 +13,12 @@ import {
|
|
|
13
13
|
ProvidersRoute,
|
|
14
14
|
RequestsRoute,
|
|
15
15
|
ToolsRoute,
|
|
16
|
+
TracesRoute,
|
|
16
17
|
} from "./routes";
|
|
17
18
|
import { RequestDrawer } from "./ui/RequestDrawer";
|
|
18
19
|
|
|
19
20
|
export default function App() {
|
|
20
|
-
const { section, setSection, range, setRange } = useHashRoute();
|
|
21
|
+
const { section, setSection, range, setRange, session, setSession } = useHashRoute();
|
|
21
22
|
const [refreshTrigger, setRefreshTrigger] = useState(0);
|
|
22
23
|
const [selectedRequestId, setSelectedRequestId] = useState<number | null>(null);
|
|
23
24
|
const [updatedAt, setUpdatedAt] = useState<number | null>(() => Date.now());
|
|
@@ -63,6 +64,15 @@ export default function App() {
|
|
|
63
64
|
onRequestClick={setSelectedRequestId}
|
|
64
65
|
/>
|
|
65
66
|
);
|
|
67
|
+
case "traces":
|
|
68
|
+
return (
|
|
69
|
+
<TracesRoute
|
|
70
|
+
active={isActive}
|
|
71
|
+
session={session}
|
|
72
|
+
onOpenSession={setSession}
|
|
73
|
+
refreshTrigger={refreshTrigger}
|
|
74
|
+
/>
|
|
75
|
+
);
|
|
66
76
|
case "errors":
|
|
67
77
|
return (
|
|
68
78
|
<ErrorsRoute
|
package/src/client/api.ts
CHANGED
|
@@ -8,6 +8,8 @@ import type {
|
|
|
8
8
|
OverviewStats,
|
|
9
9
|
ProviderDashboardStats,
|
|
10
10
|
RequestDetails,
|
|
11
|
+
SessionSummary,
|
|
12
|
+
SessionTrace,
|
|
11
13
|
TimeRange,
|
|
12
14
|
ToolDashboardStats,
|
|
13
15
|
} from "./types";
|
|
@@ -116,3 +118,24 @@ export async function getProviderDashboardStats(
|
|
|
116
118
|
signal,
|
|
117
119
|
});
|
|
118
120
|
}
|
|
121
|
+
export async function getSessions(limit = 100, q?: string, signal?: AbortSignal): Promise<SessionSummary[]> {
|
|
122
|
+
const params = new URLSearchParams({ limit: String(limit) });
|
|
123
|
+
if (q) params.set("q", q);
|
|
124
|
+
return fetchJson<SessionSummary[]>(`${API_BASE}/sessions?${params}`, { signal });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function getSessionTrace(file: string, signal?: AbortSignal): Promise<SessionTrace> {
|
|
128
|
+
return fetchJson<SessionTrace>(`${API_BASE}/session/trace?file=${encodeURIComponent(file)}`, { signal });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Fetch one full journal entry for the span drawer. Entries are opaque JSON. */
|
|
132
|
+
export async function getSessionEntryDetail(
|
|
133
|
+
file: string,
|
|
134
|
+
id: string,
|
|
135
|
+
signal?: AbortSignal,
|
|
136
|
+
): Promise<{ entry: unknown }> {
|
|
137
|
+
return fetchJson<{ entry: unknown }>(
|
|
138
|
+
`${API_BASE}/session/entry?file=${encodeURIComponent(file)}&id=${encodeURIComponent(id)}`,
|
|
139
|
+
{ signal },
|
|
140
|
+
);
|
|
141
|
+
}
|
package/src/client/app/routes.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
Cpu,
|
|
6
6
|
Folder,
|
|
7
7
|
LayoutDashboard,
|
|
8
|
+
ListTree,
|
|
8
9
|
Plug,
|
|
9
10
|
Smile,
|
|
10
11
|
TrendingUp,
|
|
@@ -15,6 +16,7 @@ import type React from "react";
|
|
|
15
16
|
export type DashboardSection =
|
|
16
17
|
| "overview"
|
|
17
18
|
| "requests"
|
|
19
|
+
| "traces"
|
|
18
20
|
| "errors"
|
|
19
21
|
| "models"
|
|
20
22
|
| "providers"
|
|
@@ -42,6 +44,11 @@ export const routes: DashboardRoute[] = [
|
|
|
42
44
|
label: "Requests",
|
|
43
45
|
icon: Activity,
|
|
44
46
|
},
|
|
47
|
+
{
|
|
48
|
+
id: "traces",
|
|
49
|
+
label: "Traces",
|
|
50
|
+
icon: ListTree,
|
|
51
|
+
},
|
|
45
52
|
{
|
|
46
53
|
id: "errors",
|
|
47
54
|
label: "Errors",
|
|
@@ -5,6 +5,7 @@ import type { TimeRange } from "../types";
|
|
|
5
5
|
const VALID_SECTIONS: DashboardSection[] = [
|
|
6
6
|
"overview",
|
|
7
7
|
"requests",
|
|
8
|
+
"traces",
|
|
8
9
|
"errors",
|
|
9
10
|
"models",
|
|
10
11
|
"providers",
|
|
@@ -17,7 +18,7 @@ const VALID_SECTIONS: DashboardSection[] = [
|
|
|
17
18
|
|
|
18
19
|
const VALID_RANGES: TimeRange[] = ["1h", "24h", "7d", "30d", "90d", "all"];
|
|
19
20
|
|
|
20
|
-
function parseHash(hash: string): { section: DashboardSection; range: TimeRange } {
|
|
21
|
+
function parseHash(hash: string): { section: DashboardSection; range: TimeRange; session: string | null } {
|
|
21
22
|
const cleanHash = hash.replace(/^#\/?/, "");
|
|
22
23
|
const [pathPart, queryPart] = cleanHash.split("?");
|
|
23
24
|
|
|
@@ -26,15 +27,22 @@ function parseHash(hash: string): { section: DashboardSection; range: TimeRange
|
|
|
26
27
|
: "overview";
|
|
27
28
|
|
|
28
29
|
let range: TimeRange = "24h";
|
|
30
|
+
let session: string | null = null;
|
|
29
31
|
if (queryPart) {
|
|
30
32
|
const params = new URLSearchParams(queryPart);
|
|
31
33
|
const rangeParam = params.get("range") as TimeRange;
|
|
32
34
|
if (VALID_RANGES.includes(rangeParam)) {
|
|
33
35
|
range = rangeParam;
|
|
34
36
|
}
|
|
37
|
+
session = params.get("s");
|
|
35
38
|
}
|
|
36
39
|
|
|
37
|
-
return { section, range };
|
|
40
|
+
return { section, range, session };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function buildHash(section: string, range: TimeRange, session?: string | null): string {
|
|
44
|
+
const sessionPart = session ? `&s=${encodeURIComponent(session)}` : "";
|
|
45
|
+
return `/${section}?range=${range}${sessionPart}`;
|
|
38
46
|
}
|
|
39
47
|
|
|
40
48
|
export function useHashRoute() {
|
|
@@ -51,31 +59,39 @@ export function useHashRoute() {
|
|
|
51
59
|
};
|
|
52
60
|
}, []);
|
|
53
61
|
|
|
54
|
-
const updateHash = useCallback((section: string, range: TimeRange) => {
|
|
55
|
-
window.location.hash =
|
|
62
|
+
const updateHash = useCallback((section: string, range: TimeRange, session?: string | null) => {
|
|
63
|
+
window.location.hash = buildHash(section, range, session);
|
|
56
64
|
}, []);
|
|
57
65
|
|
|
58
66
|
const setSection = useCallback(
|
|
59
67
|
(newSection: DashboardSection) => {
|
|
60
|
-
|
|
68
|
+
// The deep-linked session only applies to the traces view.
|
|
69
|
+
updateHash(newSection, route.range, newSection === "traces" ? route.session : null);
|
|
61
70
|
},
|
|
62
|
-
[route.range, updateHash],
|
|
71
|
+
[route.range, route.session, updateHash],
|
|
63
72
|
);
|
|
64
73
|
|
|
65
74
|
const setRange = useCallback(
|
|
66
75
|
(newRange: string) => {
|
|
67
76
|
const nextRange = VALID_RANGES.includes(newRange as TimeRange) ? (newRange as TimeRange) : "24h";
|
|
68
|
-
updateHash(route.section, nextRange);
|
|
77
|
+
updateHash(route.section, nextRange, route.session);
|
|
78
|
+
},
|
|
79
|
+
[route.section, route.session, updateHash],
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const setSession = useCallback(
|
|
83
|
+
(file: string | null) => {
|
|
84
|
+
updateHash(route.section, route.range, file);
|
|
69
85
|
},
|
|
70
|
-
[route.section, updateHash],
|
|
86
|
+
[route.section, route.range, updateHash],
|
|
71
87
|
);
|
|
72
88
|
|
|
73
89
|
useEffect(() => {
|
|
74
90
|
const currentHash = window.location.hash;
|
|
75
91
|
const parsed = parseHash(currentHash);
|
|
76
|
-
const expectedHash =
|
|
92
|
+
const expectedHash = `#${buildHash(parsed.section, parsed.range, parsed.session)}`;
|
|
77
93
|
if (currentHash !== expectedHash) {
|
|
78
|
-
window.location.hash =
|
|
94
|
+
window.location.hash = buildHash(parsed.section, parsed.range, parsed.session);
|
|
79
95
|
}
|
|
80
96
|
}, []);
|
|
81
97
|
|
|
@@ -84,5 +100,7 @@ export function useHashRoute() {
|
|
|
84
100
|
setSection,
|
|
85
101
|
range: route.range,
|
|
86
102
|
setRange,
|
|
103
|
+
session: route.session,
|
|
104
|
+
setSession,
|
|
87
105
|
};
|
|
88
106
|
}
|
|
@@ -234,6 +234,7 @@ function PeakHoursPanel({ hourly, providers }: { hourly: ProviderHourlyPoint[];
|
|
|
234
234
|
const chartTheme = CHART_THEMES[theme];
|
|
235
235
|
|
|
236
236
|
const { tokensByHour, peakHour } = useMemo(() => {
|
|
237
|
+
// oxlint-disable-next-line unicorn/no-new-array -- length preallocation
|
|
237
238
|
const tokens = new Array<number>(24).fill(0);
|
|
238
239
|
for (const point of hourly) {
|
|
239
240
|
if (provider !== ALL_PROVIDERS && point.provider !== provider) continue;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Traces section: root-session list (subagents folded in) that opens the
|
|
3
|
+
* flamegraph trace viewer for a selected session.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useMemo, useState } from "react";
|
|
7
|
+
import { getSessions } from "../api";
|
|
8
|
+
import { formatCompact, formatCost, formatDurationMs, formatRelativeTime } from "../data/formatters";
|
|
9
|
+
import { useResource } from "../data/useResource";
|
|
10
|
+
import { TraceView } from "../traces/TraceView";
|
|
11
|
+
import type { SessionSummary } from "../types";
|
|
12
|
+
import { AsyncBoundary, DataTable, Panel } from "../ui";
|
|
13
|
+
|
|
14
|
+
export interface TracesRouteProps {
|
|
15
|
+
active: boolean;
|
|
16
|
+
session: string | null;
|
|
17
|
+
onOpenSession: (file: string | null) => void;
|
|
18
|
+
refreshTrigger: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function ModelChips({ models }: { models: string[] }) {
|
|
22
|
+
const shown = models.slice(0, 3);
|
|
23
|
+
return (
|
|
24
|
+
<div style={{ display: "flex", gap: 4, flexWrap: "wrap" }}>
|
|
25
|
+
{shown.map(model => (
|
|
26
|
+
<span
|
|
27
|
+
key={model}
|
|
28
|
+
className="stats-text-muted truncate"
|
|
29
|
+
style={{
|
|
30
|
+
fontSize: 10,
|
|
31
|
+
border: "1px solid var(--border)",
|
|
32
|
+
borderRadius: 999,
|
|
33
|
+
padding: "1px 6px",
|
|
34
|
+
maxWidth: 140,
|
|
35
|
+
}}
|
|
36
|
+
>
|
|
37
|
+
{model}
|
|
38
|
+
</span>
|
|
39
|
+
))}
|
|
40
|
+
{models.length > 3 && (
|
|
41
|
+
<span className="stats-text-muted" style={{ fontSize: 10 }}>
|
|
42
|
+
+{models.length - 3}
|
|
43
|
+
</span>
|
|
44
|
+
)}
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function TracesRoute({ active, session, onOpenSession, refreshTrigger }: TracesRouteProps) {
|
|
50
|
+
const [filter, setFilter] = useState("");
|
|
51
|
+
|
|
52
|
+
const {
|
|
53
|
+
data: sessions,
|
|
54
|
+
error,
|
|
55
|
+
loading,
|
|
56
|
+
} = useResource(["sessions", refreshTrigger], signal => getSessions(200, undefined, signal), {
|
|
57
|
+
pollMs: 30000,
|
|
58
|
+
enabled: active && session === null,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const filtered = useMemo(() => {
|
|
62
|
+
if (!sessions) return [];
|
|
63
|
+
const needle = filter.trim().toLowerCase();
|
|
64
|
+
if (!needle) return sessions;
|
|
65
|
+
return sessions.filter(
|
|
66
|
+
row =>
|
|
67
|
+
(row.title ?? "").toLowerCase().includes(needle) ||
|
|
68
|
+
row.folder.toLowerCase().includes(needle) ||
|
|
69
|
+
row.models.some(model => model.toLowerCase().includes(needle)),
|
|
70
|
+
);
|
|
71
|
+
}, [sessions, filter]);
|
|
72
|
+
|
|
73
|
+
const columns = useMemo(
|
|
74
|
+
() => [
|
|
75
|
+
{
|
|
76
|
+
key: "title",
|
|
77
|
+
header: "Title",
|
|
78
|
+
render: (item: SessionSummary) => (
|
|
79
|
+
<div className="stats-font-medium stats-text-primary truncate" style={{ maxWidth: 280 }}>
|
|
80
|
+
{item.title ?? item.file.split("/").pop()}
|
|
81
|
+
</div>
|
|
82
|
+
),
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
key: "folder",
|
|
86
|
+
header: "Project",
|
|
87
|
+
render: (item: SessionSummary) => (
|
|
88
|
+
<span className="stats-text-muted truncate" style={{ maxWidth: 160, display: "inline-block" }}>
|
|
89
|
+
{item.folder.split("/").slice(-2).join("/")}
|
|
90
|
+
</span>
|
|
91
|
+
),
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
key: "started",
|
|
95
|
+
header: "Started",
|
|
96
|
+
render: (item: SessionSummary) => formatRelativeTime(item.startedAt),
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
key: "duration",
|
|
100
|
+
header: "Duration",
|
|
101
|
+
numeric: true,
|
|
102
|
+
render: (item: SessionSummary) => formatDurationMs(item.endedAt - item.startedAt),
|
|
103
|
+
},
|
|
104
|
+
{ key: "requests", header: "Requests", numeric: true, render: (item: SessionSummary) => item.requests },
|
|
105
|
+
{ key: "toolCalls", header: "Tools", numeric: true, render: (item: SessionSummary) => item.toolCalls },
|
|
106
|
+
{ key: "subagents", header: "Agents", numeric: true, render: (item: SessionSummary) => item.subagents },
|
|
107
|
+
{
|
|
108
|
+
key: "tokens",
|
|
109
|
+
header: "Tokens",
|
|
110
|
+
numeric: true,
|
|
111
|
+
render: (item: SessionSummary) => formatCompact(item.totalTokens),
|
|
112
|
+
},
|
|
113
|
+
{ key: "cost", header: "Cost", numeric: true, render: (item: SessionSummary) => formatCost(item.costTotal) },
|
|
114
|
+
{ key: "models", header: "Models", render: (item: SessionSummary) => <ModelChips models={item.models} /> },
|
|
115
|
+
],
|
|
116
|
+
[],
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const renderMobileCard = (item: SessionSummary, onClick?: () => void) => (
|
|
120
|
+
<div className="stats-mobile-card" onClick={onClick}>
|
|
121
|
+
<div className="stats-mobile-card-header">
|
|
122
|
+
<div className="stats-font-semibold stats-text-primary truncate">
|
|
123
|
+
{item.title ?? item.file.split("/").pop()}
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
<div className="stats-mobile-card-grid">
|
|
127
|
+
<div>
|
|
128
|
+
<div className="stats-mobile-card-label">Started</div>
|
|
129
|
+
<div className="stats-mobile-card-value">{formatRelativeTime(item.startedAt)}</div>
|
|
130
|
+
</div>
|
|
131
|
+
<div>
|
|
132
|
+
<div className="stats-mobile-card-label">Duration</div>
|
|
133
|
+
<div className="stats-mobile-card-value">{formatDurationMs(item.endedAt - item.startedAt)}</div>
|
|
134
|
+
</div>
|
|
135
|
+
<div>
|
|
136
|
+
<div className="stats-mobile-card-label">Requests</div>
|
|
137
|
+
<div className="stats-mobile-card-value">{item.requests}</div>
|
|
138
|
+
</div>
|
|
139
|
+
<div>
|
|
140
|
+
<div className="stats-mobile-card-label">Cost</div>
|
|
141
|
+
<div className="stats-mobile-card-value">{formatCost(item.costTotal)}</div>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
if (session !== null) {
|
|
148
|
+
return <TraceView file={session} active={active} onBack={() => onOpenSession(null)} />;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<div className="stats-route-container">
|
|
153
|
+
<Panel
|
|
154
|
+
title="Sessions"
|
|
155
|
+
subtitle="Recent sessions with subagent activity folded in — click one to open its trace"
|
|
156
|
+
actions={
|
|
157
|
+
<input
|
|
158
|
+
type="search"
|
|
159
|
+
value={filter}
|
|
160
|
+
onChange={event => setFilter(event.target.value)}
|
|
161
|
+
placeholder="Filter by title, project, model…"
|
|
162
|
+
aria-label="Filter sessions"
|
|
163
|
+
spellCheck={false}
|
|
164
|
+
className="stats-trace-input"
|
|
165
|
+
style={{ width: 220 }}
|
|
166
|
+
/>
|
|
167
|
+
}
|
|
168
|
+
>
|
|
169
|
+
<AsyncBoundary loading={loading} error={error} data={sessions}>
|
|
170
|
+
<DataTable
|
|
171
|
+
columns={columns}
|
|
172
|
+
data={filtered}
|
|
173
|
+
keyExtractor={item => item.file}
|
|
174
|
+
onRowClick={item => onOpenSession(item.file)}
|
|
175
|
+
renderMobileCard={renderMobileCard}
|
|
176
|
+
emptyText="No sessions found — run a Sync to index recent activity"
|
|
177
|
+
/>
|
|
178
|
+
</AsyncBoundary>
|
|
179
|
+
</Panel>
|
|
180
|
+
</div>
|
|
181
|
+
);
|
|
182
|
+
}
|