@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.
Files changed (89) hide show
  1. package/CHANGELOG.md +4 -4
  2. package/package.json +87 -94
  3. package/src/client/App.tsx +11 -1
  4. package/src/client/api.ts +23 -0
  5. package/src/client/app/routes.ts +7 -0
  6. package/src/client/data/useHashRoute.ts +28 -10
  7. package/src/client/routes/ProvidersRoute.tsx +1 -0
  8. package/src/client/routes/TracesRoute.tsx +182 -0
  9. package/src/client/routes/index.ts +1 -0
  10. package/src/client/styles.css +433 -111
  11. package/src/client/traces/AggregatesPanel.tsx +88 -0
  12. package/src/client/traces/Minimap.tsx +177 -0
  13. package/src/client/traces/SpanDrawer.tsx +309 -0
  14. package/src/client/traces/SummaryStrip.tsx +37 -0
  15. package/src/client/traces/TimelineCanvas.tsx +768 -0
  16. package/src/client/traces/TraceView.tsx +344 -0
  17. package/src/client/traces/TranscriptList.tsx +166 -0
  18. package/src/client/traces/time-scale.ts +250 -0
  19. package/src/client/traces/trace-colors.ts +77 -0
  20. package/src/db.ts +77 -0
  21. package/src/gain-aggregator.ts +1 -1
  22. package/src/index.ts +1 -6
  23. package/src/parser.ts +55 -2
  24. package/src/server.ts +69 -17
  25. package/src/shared-types.ts +138 -0
  26. package/src/trace.ts +1002 -0
  27. package/src/types.ts +45 -1
  28. package/THIRD-PARTY-NOTICES.txt +0 -22909
  29. package/dist/client/index.css +0 -1
  30. package/dist/client/index.html +0 -24
  31. package/dist/client/index.js +0 -257
  32. package/dist/client/styles.css +0 -1692
  33. package/dist/types/aggregator.d.ts +0 -103
  34. package/dist/types/client/App.d.ts +0 -1
  35. package/dist/types/client/api.d.ts +0 -22
  36. package/dist/types/client/app/AppLayout.d.ts +0 -16
  37. package/dist/types/client/app/NavRail.d.ts +0 -7
  38. package/dist/types/client/app/RangeControl.d.ts +0 -7
  39. package/dist/types/client/app/SyncButton.d.ts +0 -14
  40. package/dist/types/client/app/ThemeToggle.d.ts +0 -1
  41. package/dist/types/client/app/TopBar.d.ts +0 -15
  42. package/dist/types/client/app/routes.d.ts +0 -12
  43. package/dist/types/client/components/AgentTokenShare.d.ts +0 -5
  44. package/dist/types/client/components/chart-shared.d.ts +0 -178
  45. package/dist/types/client/components/models-table-shared.d.ts +0 -175
  46. package/dist/types/client/components/range-meta.d.ts +0 -21
  47. package/dist/types/client/data/charts.d.ts +0 -1
  48. package/dist/types/client/data/formatters.d.ts +0 -13
  49. package/dist/types/client/data/useHashRoute.d.ts +0 -8
  50. package/dist/types/client/data/useResource.d.ts +0 -13
  51. package/dist/types/client/data/view-models.d.ts +0 -75
  52. package/dist/types/client/index.d.ts +0 -2
  53. package/dist/types/client/routes/BehaviorRoute.d.ts +0 -7
  54. package/dist/types/client/routes/CostsRoute.d.ts +0 -7
  55. package/dist/types/client/routes/ErrorsRoute.d.ts +0 -8
  56. package/dist/types/client/routes/GainRoute.d.ts +0 -7
  57. package/dist/types/client/routes/ModelsRoute.d.ts +0 -7
  58. package/dist/types/client/routes/OverviewRoute.d.ts +0 -8
  59. package/dist/types/client/routes/ProjectsRoute.d.ts +0 -7
  60. package/dist/types/client/routes/ProvidersRoute.d.ts +0 -7
  61. package/dist/types/client/routes/RequestsRoute.d.ts +0 -8
  62. package/dist/types/client/routes/ToolsRoute.d.ts +0 -7
  63. package/dist/types/client/routes/index.d.ts +0 -10
  64. package/dist/types/client/types.d.ts +0 -63
  65. package/dist/types/client/ui/AsyncBoundary.d.ts +0 -12
  66. package/dist/types/client/ui/DataTable.d.ts +0 -17
  67. package/dist/types/client/ui/EmptyState.d.ts +0 -7
  68. package/dist/types/client/ui/ErrorState.d.ts +0 -6
  69. package/dist/types/client/ui/JsonBlock.d.ts +0 -7
  70. package/dist/types/client/ui/MetricCluster.d.ts +0 -5
  71. package/dist/types/client/ui/Panel.d.ts +0 -7
  72. package/dist/types/client/ui/RequestDrawer.d.ts +0 -5
  73. package/dist/types/client/ui/SegmentedControl.d.ts +0 -12
  74. package/dist/types/client/ui/Skeleton.d.ts +0 -8
  75. package/dist/types/client/ui/StatusPill.d.ts +0 -7
  76. package/dist/types/client/ui/index.d.ts +0 -11
  77. package/dist/types/client/useSystemTheme.d.ts +0 -11
  78. package/dist/types/db.d.ts +0 -160
  79. package/dist/types/embedded-client.d.ts +0 -18
  80. package/dist/types/gain-aggregator.d.ts +0 -26
  81. package/dist/types/index.d.ts +0 -17
  82. package/dist/types/parser.d.ts +0 -53
  83. package/dist/types/port-conflict.d.ts +0 -16
  84. package/dist/types/server.d.ts +0 -14
  85. package/dist/types/shared-types.d.ts +0 -421
  86. package/dist/types/sync-worker.d.ts +0 -31
  87. package/dist/types/types.d.ts +0 -164
  88. package/dist/types/usage-windows.d.ts +0 -72
  89. 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.7] - 2026-09-01
5
+ ## [1.1.9] - 2026-09-05
6
6
 
7
- - 版本线随 1.1.7 发布对齐(随本体 v18.0.11 同步与主题/网关更新),包内无独立变更。
7
+ - v18.1.10 sync baseline (stats tracking panel updates).
8
8
 
9
- ## [1.1.6] - 2026-08-30
9
+ ## [1.1.8] - 2026-09-04
10
10
 
11
- - 同步上游 OMP v18.0.9(`cc14e04f075d`)。
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
- "type": "module",
3
- "name": "@linxiraos/pi-stats",
4
- "version": "1.1.7",
5
- "description": "Local observability dashboard for pi AI usage statistics",
6
- "homepage": "https://linxira-os.github.io/zeta/",
7
- "author": "Can Boluk",
8
- "license": "MIT",
9
- "repository": {
10
- "type": "git",
11
- "url": "git+https://github.com/Linxira-OS/linxira-zeta.git",
12
- "directory": "packages/stats"
13
- },
14
- "bugs": {
15
- "url": "https://github.com/Linxira-OS/linxira-zeta/issues"
16
- },
17
- "keywords": [
18
- "ai",
19
- "observability",
20
- "metrics",
21
- "dashboard",
22
- "llm",
23
- "statistics"
24
- ],
25
- "main": "./src/index.ts",
26
- "types": "./dist/types/index.d.ts",
27
- "bin": {
28
- "zeta-stats": "./src/index.ts"
29
- },
30
- "scripts": {
31
- "gen:stats": "bun scripts/generate-client-bundle.ts --generate",
32
- "gen:stats:reset": "bun scripts/generate-client-bundle.ts --reset",
33
- "build": "bun run build.ts",
34
- "dev": "bun run src/index.ts",
35
- "check": "biome check . && bun run check:types",
36
- "check:types": "tsgo -p tsconfig.json --noEmit && tsgo -p tsconfig.client.json --noEmit",
37
- "lint": "biome lint .",
38
- "fix": "biome check --write --unsafe .",
39
- "fmt": "biome format --write ."
40
- },
41
- "dependencies": {
42
- "@linxiraos/pi-ai": "1.1.7",
43
- "@linxiraos/pi-catalog": "1.1.7",
44
- "@linxiraos/pi-utils": "1.1.7",
45
- "@tailwindcss/node": "^4.3.2",
46
- "chart.js": "^4.5.1",
47
- "lucide-react": "^1.24.0",
48
- "react": "19.2.7",
49
- "react-chartjs-2": "^5.3.1",
50
- "react-dom": "19.2.7",
51
- "tailwindcss": "^4.3.2"
52
- },
53
- "devDependencies": {
54
- "@types/bun": "^1.3.14",
55
- "@types/react": "^19.2.17",
56
- "@types/react-dom": "^19.2.3",
57
- "postcss": "^8.5.16"
58
- },
59
- "engines": {
60
- "bun": ">=1.3.14"
61
- },
62
- "files": [
63
- "src",
64
- "build.ts",
65
- "tailwind.config.js",
66
- "README.md",
67
- "CHANGELOG.md",
68
- "LICENSE",
69
- "THIRD-PARTY-NOTICES.txt",
70
- "dist/types",
71
- "dist/client"
72
- ],
73
- "exports": {
74
- ".": {
75
- "types": "./dist/types/index.d.ts",
76
- "import": "./src/index.ts"
77
- },
78
- "./*": {
79
- "types": "./dist/types/*.d.ts",
80
- "import": "./src/*.ts"
81
- },
82
- "./client": {
83
- "types": "./dist/types/client/index.d.ts",
84
- "import": "./src/client/index.tsx"
85
- },
86
- "./client/*": {
87
- "types": "./dist/types/client/*.d.ts",
88
- "import": "./src/client/*.ts"
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
  }
@@ -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
+ }
@@ -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 = `/${section}?range=${range}`;
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
- updateHash(newSection, route.range);
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 = `#/${parsed.section}?range=${parsed.range}`;
92
+ const expectedHash = `#${buildHash(parsed.section, parsed.range, parsed.session)}`;
77
93
  if (currentHash !== expectedHash) {
78
- window.location.hash = `/${parsed.section}?range=${parsed.range}`;
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
+ }
@@ -8,3 +8,4 @@ export * from "./ProjectsRoute";
8
8
  export * from "./ProvidersRoute";
9
9
  export * from "./RequestsRoute";
10
10
  export * from "./ToolsRoute";
11
+ export * from "./TracesRoute";