@linxiraos/pi-stats 1.1.6 → 1.1.8

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 (50) hide show
  1. package/CHANGELOG.md +2 -2
  2. package/dist/client/index.css +1 -1
  3. package/dist/client/index.js +101 -101
  4. package/dist/client/styles.css +362 -98
  5. package/dist/types/client/api.d.ts +7 -1
  6. package/dist/types/client/app/routes.d.ts +1 -1
  7. package/dist/types/client/data/useHashRoute.d.ts +2 -0
  8. package/dist/types/client/routes/TracesRoute.d.ts +11 -0
  9. package/dist/types/client/routes/index.d.ts +1 -0
  10. package/dist/types/client/traces/AggregatesPanel.d.ts +9 -0
  11. package/dist/types/client/traces/Minimap.d.ts +14 -0
  12. package/dist/types/client/traces/SpanDrawer.d.ts +14 -0
  13. package/dist/types/client/traces/SummaryStrip.d.ts +9 -0
  14. package/dist/types/client/traces/TimelineCanvas.d.ts +25 -0
  15. package/dist/types/client/traces/TraceView.d.ts +11 -0
  16. package/dist/types/client/traces/TranscriptList.d.ts +24 -0
  17. package/dist/types/client/traces/time-scale.d.ts +40 -0
  18. package/dist/types/client/traces/trace-colors.d.ts +31 -0
  19. package/dist/types/db.d.ts +24 -1
  20. package/dist/types/index.d.ts +1 -1
  21. package/dist/types/parser.d.ts +8 -0
  22. package/dist/types/server.d.ts +3 -2
  23. package/dist/types/shared-types.d.ts +130 -0
  24. package/dist/types/trace.d.ts +33 -0
  25. package/dist/types/types.d.ts +35 -1
  26. package/package.json +4 -4
  27. package/src/client/App.tsx +11 -1
  28. package/src/client/api.ts +23 -0
  29. package/src/client/app/routes.ts +7 -0
  30. package/src/client/data/useHashRoute.ts +28 -10
  31. package/src/client/routes/ProvidersRoute.tsx +1 -0
  32. package/src/client/routes/TracesRoute.tsx +182 -0
  33. package/src/client/routes/index.ts +1 -0
  34. package/src/client/styles.css +433 -111
  35. package/src/client/traces/AggregatesPanel.tsx +88 -0
  36. package/src/client/traces/Minimap.tsx +177 -0
  37. package/src/client/traces/SpanDrawer.tsx +309 -0
  38. package/src/client/traces/SummaryStrip.tsx +37 -0
  39. package/src/client/traces/TimelineCanvas.tsx +768 -0
  40. package/src/client/traces/TraceView.tsx +344 -0
  41. package/src/client/traces/TranscriptList.tsx +166 -0
  42. package/src/client/traces/time-scale.ts +250 -0
  43. package/src/client/traces/trace-colors.ts +77 -0
  44. package/src/db.ts +77 -0
  45. package/src/index.ts +1 -6
  46. package/src/parser.ts +55 -2
  47. package/src/server.ts +69 -17
  48. package/src/shared-types.ts +138 -0
  49. package/src/trace.ts +1002 -0
  50. package/src/types.ts +45 -1
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Virtual time axis for the trace flamegraph.
3
+ *
4
+ * All three axis modes are piecewise-linear monotonic maps over global wall
5
+ * time, so cross-track alignment is preserved in every mode:
6
+ * - `time`: identity, or idle-compressed (long gaps collapse to fixed bridges)
7
+ * - `turns`: each main-track turn interval gets equal virtual width
8
+ * - `calls`: each interval between consecutive span boundaries gets equal width
9
+ */
10
+ import type { TraceTrack } from "../types.js";
11
+ export type AxisMode = "time" | "turns" | "calls";
12
+ export interface TraceScale {
13
+ /** ms epoch → virtual axis; monotonic non-decreasing. */
14
+ toU(t: number): number;
15
+ /** Inverse (clamped to domain). */
16
+ toT(u: number): number;
17
+ /** Virtual axis domain. */
18
+ domain: [number, number];
19
+ /** Idle bridges (time mode + compression): real gap [t0,t1] at virtual uMid. */
20
+ gaps: Array<{
21
+ t0: number;
22
+ t1: number;
23
+ uMid: number;
24
+ }>;
25
+ }
26
+ /** Build the axis map for the given mode over all tracks. */
27
+ export declare function buildScale(tracks: TraceTrack[], mode: AxisMode, compressIdle: boolean): TraceScale;
28
+ export interface Tick {
29
+ u: number;
30
+ label: string;
31
+ major: boolean;
32
+ }
33
+ /** Offset label from trace start: `+mm:ss.mmm`, hours prepended when needed. */
34
+ export declare function formatOffset(deltaMs: number): string;
35
+ /**
36
+ * Ruler ticks inside `[u0, u1]`. Ticks are placed at nice wall-time steps in
37
+ * the underlying t-space (per linear segment in compressed/sequence modes),
38
+ * with major ticks labelled as local wall clock and minors as offsets.
39
+ */
40
+ export declare function buildTicks(scale: TraceScale, u0: number, u1: number, widthPx: number): Tick[];
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Category palette + canvas chrome tokens for the trace flamegraph, keyed by
3
+ * resolved theme. Hues align with the dashboard chip palette; chrome tokens
4
+ * mirror `CHART_THEMES` so the canvas matches the chart.js surfaces.
5
+ */
6
+ import type { TraceSpanKind } from "../types.js";
7
+ export interface TraceTheme {
8
+ /** Span fill per category. */
9
+ category: Record<TraceSpanKind, string>;
10
+ /** Error fill tint + left-edge accent. */
11
+ error: string;
12
+ /** Marker diamond fill. */
13
+ marker: string;
14
+ /** Alternating turn-band fill. */
15
+ turnBand: string;
16
+ /** Canvas background. */
17
+ surface: string;
18
+ /** Ruler/lane hairlines. */
19
+ grid: string;
20
+ /** Ruler tick labels. */
21
+ tick: string;
22
+ /** Span label text drawn inside blocks. */
23
+ spanText: string;
24
+ /** Selection outline (brand cyan focus ring). */
25
+ selection: string;
26
+ tooltipBackground: string;
27
+ tooltipTitle: string;
28
+ tooltipBody: string;
29
+ tooltipBorder: string;
30
+ }
31
+ export declare const TRACE_THEMES: Record<"dark" | "light", TraceTheme>;
@@ -1,5 +1,5 @@
1
1
  import { Database } from "bun:sqlite";
2
- import type { AgentTypeStats, AggregatedStats, BehaviorModelStats, BehaviorOverallStats, BehaviorTimeSeriesPoint, CostTimeSeriesPoint, FolderStats, MessageStats, ModelPerformancePoint, ModelStats, ModelTimeSeriesPoint, ProviderAggregate, ProviderHourlyPoint, ProviderTimeSeriesPoint, TimeSeriesPoint, ToolCallStats, ToolModelStats, ToolResultLink, ToolTimeSeriesPoint, ToolUsageStats, UserMessageLink, UserMessageStats } from "./types.js";
2
+ import type { AgentTypeStats, AggregatedStats, BehaviorModelStats, BehaviorOverallStats, BehaviorTimeSeriesPoint, CostTimeSeriesPoint, DailyActivityPoint, FolderStats, MessageStats, ModelPerformancePoint, ModelStats, ModelTimeSeriesPoint, ProviderAggregate, ProviderHourlyPoint, ProviderTimeSeriesPoint, TimeSeriesPoint, ToolCallStats, ToolModelStats, ToolResultLink, ToolTimeSeriesPoint, ToolUsageStats, UserMessageLink, UserMessageStats } from "./types.js";
3
3
  /**
4
4
  * Initialize the database and create tables.
5
5
  */
@@ -90,10 +90,33 @@ export declare function closeDb(): void;
90
90
  export declare function getRecentRequests(limit?: number): MessageStats[];
91
91
  export declare function getRecentErrors(limit?: number, cutoff?: number | null): MessageStats[];
92
92
  export declare function getMessageById(id: number): MessageStats | null;
93
+ /** Per-transcript-file rollup for the Traces session list. */
94
+ export interface SessionRollupRow {
95
+ sessionFile: string;
96
+ requests: number;
97
+ startedAt: number;
98
+ endedAt: number;
99
+ totalTokens: number;
100
+ costTotal: number;
101
+ /** Comma-joined DISTINCT models. */
102
+ models: string;
103
+ }
104
+ /** Aggregate every synced transcript file into one row (subagents unfolded). */
105
+ export declare function getSessionRollups(): SessionRollupRow[];
106
+ /** Tool-call counts keyed by transcript file, for the Traces session list. */
107
+ export declare function getToolCallCountsBySession(): Map<string, number>;
93
108
  /**
94
109
  * Get daily cost time series data for the last N days, broken down by model.
95
110
  */
96
111
  export declare function getCostTimeSeries(days?: number, cutoff?: number | null): CostTimeSeriesPoint[];
112
+ /**
113
+ * Per-local-day activity aggregates for the last `days` days, oldest first.
114
+ * Self-initializing (opens the stats DB on first use) so the coding-agent TUI
115
+ * can query without the dashboard server's init flow. Days use the machine's
116
+ * timezone — this is a localhost tool, same rationale as
117
+ * {@link getProviderHourlyBurn}.
118
+ */
119
+ export declare function getDailyActivity(days?: number): Promise<DailyActivityPoint[]>;
97
120
  /**
98
121
  * Settle every full-session backfill after a successful sync pass.
99
122
  */
@@ -3,7 +3,7 @@ export { getDashboardStats, getToolDashboardStats, getTotalMessageCount, type Sy
3
3
  export { closeDb } from "./db.js";
4
4
  export { getGainDashboardStats } from "./gain-aggregator.js";
5
5
  export { formatStatsDashboardUrl, startServer } from "./server.js";
6
- export type { GainDashboardStats, GainSource, GainSourceTotals, GainTimeSeriesPoint, } from "./shared-types.js";
6
+ export type { GainDashboardStats, GainSource, GainSourceTotals, GainTimeSeriesPoint } from "./shared-types.js";
7
7
  export type { AggregatedStats, DashboardStats, FolderStats, MessageStats, ModelPerformancePoint, ModelStats, ModelTimeSeriesPoint, TimeSeriesPoint, ToolDashboardStats, ToolModelStats, ToolTimeSeriesPoint, ToolUsageStats, } from "./types.js";
8
8
  /** Parsed arguments for the standalone `omp-stats` entry point. */
9
9
  export interface StandaloneStatsArgs {
@@ -10,6 +10,14 @@ import type { AgentType, MessageStats, SessionEntry, ToolCallStats, ToolResultLi
10
10
  * is a task `subagent`.
11
11
  */
12
12
  export declare function classifyAgentType(sessionPath: string): AgentType;
13
+ /**
14
+ * Extract folder name from session filename.
15
+ * Session files are named like: --work--pi--/timestamp_uuid.jsonl
16
+ * The folder part uses -- as path separator.
17
+ */
18
+ export declare function extractFolderFromPath(sessionPath: string): string;
19
+ /** Parse every well-formed entry in a transcript buffer (malformed lines skipped). */
20
+ export declare function parseAllSessionEntries(bytes: Uint8Array): SessionEntry[];
13
21
  /**
14
22
  * Parse a session file and extract all assistant message stats.
15
23
  * Uses incremental reading with offset tracking.
@@ -7,8 +7,9 @@ export declare function formatStatsDashboardUrl(hostname: string, port: number):
7
7
  /**
8
8
  * Start the HTTP server, reusing a live dashboard or reclaiming a stale omp listener.
9
9
  */
10
- export declare function startServer(port?: number, hostname?: string): Promise<{
10
+ export interface StatsServerHandle {
11
11
  hostname: string;
12
12
  port: number;
13
13
  stop: () => void;
14
- }>;
14
+ }
15
+ export declare function startServer(port?: number, hostname?: string): Promise<StatsServerHandle>;
@@ -127,6 +127,20 @@ export interface CostTimeSeriesPoint {
127
127
  /** Request count */
128
128
  requests: number;
129
129
  }
130
+ /**
131
+ * One local calendar day of aggregate request activity, for the `/usage`
132
+ * activity heatmap in the coding-agent TUI.
133
+ */
134
+ export interface DailyActivityPoint {
135
+ /** Local calendar date, `YYYY-MM-DD`. */
136
+ day: string;
137
+ /** Summed API-equivalent cost for the day. */
138
+ cost: number;
139
+ /** Request count for the day. */
140
+ requests: number;
141
+ /** Total tokens (input + output + cache) for the day. */
142
+ totalTokens: number;
143
+ }
130
144
  /**
131
145
  * Overall dashboard stats.
132
146
  */
@@ -419,3 +433,119 @@ export interface ProviderDashboardStats {
419
433
  usageSeries: UsageWindowSeries[];
420
434
  windowInsights: ProviderWindowInsight[];
421
435
  }
436
+ /**
437
+ * One row of the Traces session list: a root session with every child
438
+ * transcript (task subagents, advisors) folded in.
439
+ */
440
+ export interface SessionSummary {
441
+ /** Absolute root session file path (trace key). */
442
+ file: string;
443
+ /** Decoded project path (e.g. `/work/pi`). */
444
+ folder: string;
445
+ title: string | null;
446
+ /** ms epoch of first activity. */
447
+ startedAt: number;
448
+ /** ms epoch of last activity, children included. */
449
+ endedAt: number;
450
+ /** Assistant messages, children folded in. */
451
+ requests: number;
452
+ toolCalls: number;
453
+ /** Child transcript count. */
454
+ subagents: number;
455
+ totalTokens: number;
456
+ costTotal: number;
457
+ models: string[];
458
+ }
459
+ export type TraceSpanKind = "turn" | "model" | "tool" | "subagent" | "background";
460
+ /** One rendered block on a trace track lane. */
461
+ export interface TraceSpan {
462
+ /** `${track.id}:${entryId}` (+`:${toolCallId}` for tool spans); stable across refetch. */
463
+ id: string;
464
+ kind: TraceSpanKind;
465
+ /** ms epoch. */
466
+ start: number;
467
+ /** ms epoch, >= start. */
468
+ end: number;
469
+ /** ≤80 chars: tool name / model id / user-text head / agent name. */
470
+ label: string;
471
+ /** ≤160 chars: args projection / result head / task text. */
472
+ detail?: string;
473
+ /** Journal entry id for /api/session/entry. */
474
+ entryId?: string;
475
+ toolCallId?: string;
476
+ model?: string;
477
+ /** usage.totalTokens (model spans). */
478
+ tokens?: number;
479
+ /** usage.cost.total (model spans). */
480
+ cost?: number;
481
+ /** Time to first token, ms offset from start. */
482
+ ttft?: number;
483
+ isError?: boolean;
484
+ /** End synthesized (no result / pending at session exit). */
485
+ unterminated?: boolean;
486
+ /** Set on `subagent` spans whose child transcript became a track. */
487
+ childTrackId?: string;
488
+ }
489
+ /** Point event drawn on a track header row. */
490
+ export interface TraceMarker {
491
+ /** ms epoch. */
492
+ time: number;
493
+ kind: "compaction" | "model_change" | "mode_change" | "reset" | "session_exit";
494
+ /** e.g. "compaction 142k→38k", model id, mode name, exit kind. */
495
+ label: string;
496
+ }
497
+ /** One transcript (main session, subagent, advisor) in a trace. */
498
+ export interface TraceTrack {
499
+ /** "main" or slash-joined child key: "Scout1", "Scout1/Nested2", "__advisor". */
500
+ id: string;
501
+ parentId: string | null;
502
+ label: string;
503
+ /** session_init.agent when recorded. */
504
+ agent: string | null;
505
+ /** session_init.resolvedModel ?? first assistant model. */
506
+ model: string | null;
507
+ /** Absolute transcript path (for entry fetch). */
508
+ file: string;
509
+ /** Sorted by start. */
510
+ spans: TraceSpan[];
511
+ markers: TraceMarker[];
512
+ }
513
+ /** Per-tool duration aggregate across all tracks of one trace. */
514
+ export interface TraceToolStat {
515
+ tool: string;
516
+ calls: number;
517
+ errors: number;
518
+ totalMs: number;
519
+ maxMs: number;
520
+ }
521
+ /** Headline aggregates for one trace. */
522
+ export interface TraceSummary {
523
+ wallMs: number;
524
+ /** Summed model-span duration on the main track. */
525
+ modelMs: number;
526
+ /** Summed tool-span duration on the main track. */
527
+ toolMs: number;
528
+ /** Wall time not covered by any span on any track. */
529
+ idleMs: number;
530
+ turns: number;
531
+ requests: number;
532
+ toolCalls: number;
533
+ subagents: number;
534
+ totalTokens: number;
535
+ costTotal: number;
536
+ /** Sorted totalMs desc. */
537
+ toolStats: TraceToolStat[];
538
+ }
539
+ /** Complete span tree for one session, `/api/session/trace` payload. */
540
+ export interface SessionTrace {
541
+ file: string;
542
+ title: string | null;
543
+ cwd: string | null;
544
+ startedAt: number;
545
+ endedAt: number;
546
+ /** Root transcript mtime; doubles as the ETag. */
547
+ mtimeMs: number;
548
+ /** DFS order, main first. */
549
+ tracks: TraceTrack[];
550
+ summary: TraceSummary;
551
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Session trace assembly for the Traces dashboard section.
3
+ *
4
+ * Builds a span tree (turns, model calls, tool calls, subagents, background
5
+ * jobs) directly from raw session JSONL — the only source with tool start/end
6
+ * times — while stats.db supplies the session-list rollups. Entry shapes are
7
+ * mirrored structurally from the coding-agent journal (`tool_execution_start`,
8
+ * `session_exit`, task results, async-result batches); stats never imports
9
+ * coding-agent.
10
+ */
11
+ import type { SessionEntry, SessionSummary, SessionTrace } from "./types.js";
12
+ /** Client-supplied path escaped the sessions root or is not a transcript. Maps to HTTP 400. */
13
+ export declare class TracePathError extends Error {
14
+ }
15
+ /**
16
+ * Trace payload schema/assembly revision, folded into the HTTP ETag. Bump when
17
+ * span assembly changes so browsers don't revalidate stale cached bodies
18
+ * against an unchanged transcript mtime.
19
+ */
20
+ export declare const TRACE_ETAG_VERSION = 2;
21
+ /**
22
+ * Assemble the full span tree for one root session transcript.
23
+ * Throws {@link TracePathError} for paths outside the sessions root; ENOENT
24
+ * passes through for the caller's 404 mapping.
25
+ */
26
+ export declare function buildSessionTrace(fileParam: string): Promise<SessionTrace>;
27
+ /** Fetch one full journal entry by id from a transcript, for the span drawer. */
28
+ export declare function getTraceEntry(fileParam: string, entryId: string): Promise<SessionEntry | null>;
29
+ /**
30
+ * List root sessions for the Traces section, folding every synced child
31
+ * transcript (subagents, advisors) into its root row.
32
+ */
33
+ export declare function listSessionSummaries(limit?: number, q?: string): Promise<SessionSummary[]>;
@@ -70,7 +70,41 @@ export interface SessionServiceTierChangeEntry {
70
70
  timestamp: string;
71
71
  serviceTier: ServiceTierByFamily | ServiceTier | null;
72
72
  }
73
- export type SessionEntry = SessionHeader | SessionMessageEntry | SessionServiceTierChangeEntry | {
73
+ export interface SessionModelUsageEntry {
74
+ type: "model_usage";
75
+ id: string;
76
+ parentId: string | null;
77
+ timestamp: string;
78
+ purpose: string;
79
+ role?: string;
80
+ api: string;
81
+ provider: string;
82
+ model: string;
83
+ usage: Usage;
84
+ stopReason?: StopReason;
85
+ errorMessage?: string;
86
+ }
87
+ /**
88
+ * Custom journal entry (`tool_execution_start`, `session_exit`, …). Mirrors
89
+ * the coding-agent shape structurally — stats never imports coding-agent.
90
+ */
91
+ export interface SessionCustomEntry {
92
+ type: "custom";
93
+ id?: string;
94
+ parentId?: string | null;
95
+ timestamp?: string;
96
+ customType: string;
97
+ data?: Record<string, unknown>;
98
+ }
99
+ /** Structural variants the trace builder matches on beyond messages. */
100
+ export interface SessionTypedEntry {
101
+ type: "session_init" | "compaction" | "model_change" | "mode_change" | "reset_boundary";
102
+ id?: string;
103
+ parentId?: string | null;
104
+ timestamp?: string;
105
+ [key: string]: unknown;
106
+ }
107
+ export type SessionEntry = SessionHeader | SessionMessageEntry | SessionServiceTierChangeEntry | SessionModelUsageEntry | SessionCustomEntry | SessionTypedEntry | {
74
108
  type: string;
75
109
  };
76
110
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@linxiraos/pi-stats",
4
- "version": "1.1.6",
4
+ "version": "1.1.8",
5
5
  "description": "Local observability dashboard for pi AI usage statistics",
6
6
  "homepage": "https://linxira-os.github.io/zeta/",
7
7
  "author": "Can Boluk",
@@ -39,9 +39,9 @@
39
39
  "fmt": "biome format --write ."
40
40
  },
41
41
  "dependencies": {
42
- "@linxiraos/pi-ai": "1.1.6",
43
- "@linxiraos/pi-catalog": "1.1.6",
44
- "@linxiraos/pi-utils": "1.1.6",
42
+ "@linxiraos/pi-ai": "1.1.8",
43
+ "@linxiraos/pi-catalog": "1.1.8",
44
+ "@linxiraos/pi-utils": "1.1.8",
45
45
  "@tailwindcss/node": "^4.3.2",
46
46
  "chart.js": "^4.5.1",
47
47
  "lucide-react": "^1.24.0",
@@ -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;