pi-mega-compact 0.8.17 → 0.8.19

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 (93) hide show
  1. package/dist/extensions/dashboard-server/api-contracts/endpoints.js +12 -0
  2. package/dist/extensions/dashboard-server/api-contracts.test.js +19 -6
  3. package/dist/extensions/dashboard-server/helpers.js +37 -0
  4. package/dist/extensions/dashboard-server/html/all-repos-tab.js +26 -0
  5. package/dist/extensions/dashboard-server/html/body-open.js +23 -0
  6. package/dist/extensions/dashboard-server/html/current-repo-tab.js +130 -0
  7. package/dist/extensions/dashboard-server/html/head-open.js +16 -0
  8. package/dist/extensions/dashboard-server/html/high-score-tab.js +25 -0
  9. package/dist/extensions/dashboard-server/html/repo-detail-modal.js +26 -0
  10. package/dist/extensions/dashboard-server/html/script.js +259 -0
  11. package/dist/extensions/dashboard-server/html/styles.js +103 -0
  12. package/dist/extensions/dashboard-server/html/summary-tab.js +19 -0
  13. package/dist/extensions/dashboard-server/html-template.js +41 -0
  14. package/dist/extensions/dashboard-server/server.js +83 -0
  15. package/dist/extensions/mega-events/context-handler.js +22 -8
  16. package/dist/extensions/mega-runtime/state.js +17 -1
  17. package/dist/src/store/sqlite/connection.js +35 -0
  18. package/dist/src/store/sqlite/global-index.js +199 -1
  19. package/dist/src/store/sqlite/global-index.test.js +136 -0
  20. package/dist/src/store/sqlite/index-store.js +167 -0
  21. package/dist/src/store/sqlite/memory.js +54 -0
  22. package/dist/src/store/sqlite/minhash-lsh.js +47 -0
  23. package/dist/src/store/sqlite/sessions.js +39 -0
  24. package/dist/src/store/sqlite/transaction.js +19 -0
  25. package/dist/src/vectorStore/add.js +260 -0
  26. package/dist/src/vectorStore/dedup.js +52 -0
  27. package/dist/src/vectorStore/index.js +10 -0
  28. package/dist/src/vectorStore/queries.js +83 -0
  29. package/dist/src/vectorStore/search.js +95 -0
  30. package/dist/src/vectorStore/session.js +19 -0
  31. package/dist/src/vectorStore/store.js +105 -0
  32. package/dist/src/vectorStore/types.js +6 -0
  33. package/dist/src/vectorStore/utils.js +23 -0
  34. package/extensions/dashboard-client/dist/assets/{AchievementTiles-BozP_h3c.js → AchievementTiles-DVgFowc-.js} +2 -2
  35. package/extensions/dashboard-client/dist/assets/{AchievementTiles-BozP_h3c.js.map → AchievementTiles-DVgFowc-.js.map} +1 -1
  36. package/extensions/dashboard-client/dist/assets/AchievementsTab-Bg86TWPU.js +2 -0
  37. package/extensions/dashboard-client/dist/assets/{AchievementsTab-DKgqx1RQ.js.map → AchievementsTab-Bg86TWPU.js.map} +1 -1
  38. package/extensions/dashboard-client/dist/assets/{CacheTab-CPvso4-0.js → CacheTab-DBzoH_q_.js} +2 -2
  39. package/extensions/dashboard-client/dist/assets/{CacheTab-CPvso4-0.js.map → CacheTab-DBzoH_q_.js.map} +1 -1
  40. package/extensions/dashboard-client/dist/assets/{ConfigTab-DXD7zbqy.js → ConfigTab-Bj12LgTh.js} +2 -2
  41. package/extensions/dashboard-client/dist/assets/ConfigTab-Bj12LgTh.js.map +1 -0
  42. package/extensions/dashboard-client/dist/assets/EventsTab-EC0wMsp_.js +2 -0
  43. package/extensions/dashboard-client/dist/assets/EventsTab-EC0wMsp_.js.map +1 -0
  44. package/extensions/dashboard-client/dist/assets/{GameTab-Qj19W1Y-.js → GameTab-CISQitbu.js} +2 -2
  45. package/extensions/dashboard-client/dist/assets/{GameTab-Qj19W1Y-.js.map → GameTab-CISQitbu.js.map} +1 -1
  46. package/extensions/dashboard-client/dist/assets/{MetricsTab-DA27vA95.js → MetricsTab-us4vVG5D.js} +2 -2
  47. package/extensions/dashboard-client/dist/assets/{MetricsTab-DA27vA95.js.map → MetricsTab-us4vVG5D.js.map} +1 -1
  48. package/extensions/dashboard-client/dist/assets/OverviewTab-eJ7vFgBE.js +2 -0
  49. package/extensions/dashboard-client/dist/assets/OverviewTab-eJ7vFgBE.js.map +1 -0
  50. package/extensions/dashboard-client/dist/assets/{ReposTab-_HNR3qYj.js → ReposTab-CiU9AJkb.js} +2 -2
  51. package/extensions/dashboard-client/dist/assets/{ReposTab-_HNR3qYj.js.map → ReposTab-CiU9AJkb.js.map} +1 -1
  52. package/extensions/dashboard-client/dist/assets/SessionsTab-DaCUvTC7.js +70 -0
  53. package/extensions/dashboard-client/dist/assets/SessionsTab-DaCUvTC7.js.map +1 -0
  54. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-B7aPcBMs.js → TimeSavedCard-BAUSCzq5.js} +2 -2
  55. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-B7aPcBMs.js.map → TimeSavedCard-BAUSCzq5.js.map} +1 -1
  56. package/extensions/dashboard-client/dist/assets/{index-B2JJExE9.js → index-5MPZyayK.js} +10 -10
  57. package/extensions/dashboard-client/dist/assets/{index-B2JJExE9.js.map → index-5MPZyayK.js.map} +1 -1
  58. package/extensions/dashboard-client/dist/assets/{index-Cfxniu2R.css → index-CDkmYP9l.css} +1 -1
  59. package/extensions/dashboard-client/dist/assets/useSSE-BSpmWZZi.js +2 -0
  60. package/extensions/dashboard-client/dist/assets/useSSE-BSpmWZZi.js.map +1 -0
  61. package/extensions/dashboard-client/dist/index.html +2 -2
  62. package/extensions/dashboard-client/package-lock.json +374 -2
  63. package/extensions/dashboard-client/package.json +2 -1
  64. package/extensions/dashboard-client/src/App.tsx +5 -1
  65. package/extensions/dashboard-client/src/api/client.ts +12 -0
  66. package/extensions/dashboard-client/src/components/ActiveSessionsTable.tsx +185 -0
  67. package/extensions/dashboard-client/src/components/EventStream.tsx +3 -0
  68. package/extensions/dashboard-client/src/components/RepoContextStack.tsx +238 -0
  69. package/extensions/dashboard-client/src/components/SessionsMemoryChart.tsx +199 -0
  70. package/extensions/dashboard-client/src/main.tsx +2 -0
  71. package/extensions/dashboard-client/src/styles/repostack.css +87 -0
  72. package/extensions/dashboard-client/src/styles/sessions.css +193 -0
  73. package/extensions/dashboard-client/src/tabs/ConfigTab.tsx +6 -3
  74. package/extensions/dashboard-client/src/tabs/OverviewTab.tsx +49 -1
  75. package/extensions/dashboard-client/src/tabs/SessionsTab.tsx +193 -0
  76. package/extensions/dashboard-server/api-contracts/core.ts +25 -0
  77. package/extensions/dashboard-server/api-contracts/endpoints.ts +102 -0
  78. package/extensions/dashboard-server/api-contracts/index.ts +10 -1
  79. package/extensions/dashboard-server/api-contracts.test.ts +20 -7
  80. package/extensions/dashboard-server/server.ts +100 -0
  81. package/extensions/mega-events/context-handler.ts +40 -18
  82. package/extensions/mega-runtime/state.ts +31 -0
  83. package/package.json +1 -1
  84. package/src/store/sqlite/global-index.test.ts +222 -0
  85. package/src/store/sqlite/global-index.ts +316 -1
  86. package/dist/extensions/dashboard-client/src/hooks/useApi.js +0 -51
  87. package/dist/extensions/dashboard-client/src/hooks/useSSE.js +0 -63
  88. package/extensions/dashboard-client/dist/assets/AchievementsTab-DKgqx1RQ.js +0 -2
  89. package/extensions/dashboard-client/dist/assets/ConfigTab-DXD7zbqy.js.map +0 -1
  90. package/extensions/dashboard-client/dist/assets/EventsTab-BGeuYvM3.js +0 -2
  91. package/extensions/dashboard-client/dist/assets/EventsTab-BGeuYvM3.js.map +0 -1
  92. package/extensions/dashboard-client/dist/assets/OverviewTab-lVGR1Jl_.js +0 -2
  93. package/extensions/dashboard-client/dist/assets/OverviewTab-lVGR1Jl_.js.map +0 -1
@@ -0,0 +1,87 @@
1
+ /**
2
+ * dashboard-client/src/styles/repostack.css — Stacked per-repo context bar (S40).
3
+ *
4
+ * Horizontal stacked bar: one colored segment per active repo (aggregating
5
+ * token counts across that repo's active sessions). Depends on base.css
6
+ * tokens (--panel, --border, --text, --muted, --accent, --ok, --warn, --err).
7
+ */
8
+
9
+ .repo-stack-card {
10
+ display: flex;
11
+ flex-direction: column;
12
+ gap: 10px;
13
+ }
14
+
15
+ .repo-stack-card h3 {
16
+ margin: 0;
17
+ font-size: 14px;
18
+ font-weight: 600;
19
+ color: var(--text);
20
+ }
21
+
22
+ .repo-stack-bar {
23
+ display: flex;
24
+ width: 100%;
25
+ height: 22px;
26
+ border-radius: 6px;
27
+ overflow: hidden;
28
+ border: 1px solid var(--border);
29
+ background: var(--bg);
30
+ }
31
+
32
+ .repo-stack-segment {
33
+ height: 100%;
34
+ min-width: 2px;
35
+ transition: opacity 0.15s, filter 0.15s;
36
+ }
37
+
38
+ .repo-stack-segment:hover {
39
+ opacity: 0.85;
40
+ filter: brightness(1.15);
41
+ }
42
+
43
+ .repo-stack-total {
44
+ font-size: 13px;
45
+ color: var(--text);
46
+ font-variant-numeric: tabular-nums;
47
+ }
48
+
49
+ .repo-stack-total .pct {
50
+ color: var(--muted);
51
+ }
52
+
53
+ .repo-stack-legend {
54
+ display: grid;
55
+ grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
56
+ gap: 6px;
57
+ }
58
+
59
+ .repo-stack-legend-item {
60
+ display: flex;
61
+ align-items: center;
62
+ gap: 6px;
63
+ font-size: 12px;
64
+ color: var(--muted);
65
+ font-variant-numeric: tabular-nums;
66
+ }
67
+
68
+ .repo-stack-legend-swatch {
69
+ flex-shrink: 0;
70
+ width: 12px;
71
+ height: 12px;
72
+ border-radius: 3px;
73
+ border: 1px solid var(--border);
74
+ }
75
+
76
+ .repo-stack-legend-name {
77
+ color: var(--text);
78
+ overflow: hidden;
79
+ text-overflow: ellipsis;
80
+ white-space: nowrap;
81
+ }
82
+
83
+ @media (max-width: 640px) {
84
+ .repo-stack-legend {
85
+ grid-template-columns: 1fr;
86
+ }
87
+ }
@@ -0,0 +1,193 @@
1
+ /**
2
+ * dashboard-client/src/styles/sessions.css — Sessions tab (S39).
3
+ *
4
+ * Stacked memory graph + active sessions table. Depends on base.css
5
+ * tokens (--panel, --border, --text, --muted, --accent, --ok, --warn,
6
+ * --err). Mirrors the conventions in repos-metrics.css + metrics-extra.css.
7
+ */
8
+
9
+ .sessions-tab {
10
+ display: flex;
11
+ flex-direction: column;
12
+ gap: 14px;
13
+ }
14
+
15
+ .sessions-toolbar {
16
+ display: flex;
17
+ flex-wrap: wrap;
18
+ align-items: center;
19
+ gap: 8px;
20
+ padding: 8px 0;
21
+ }
22
+
23
+ .sessions-toolbar h2 {
24
+ margin: 0;
25
+ font-size: 14px;
26
+ font-weight: 600;
27
+ color: var(--text);
28
+ }
29
+
30
+ .sessions-window-select {
31
+ display: inline-flex;
32
+ gap: 2px;
33
+ padding: 2px;
34
+ background: var(--panel);
35
+ border: 1px solid var(--border);
36
+ border-radius: 6px;
37
+ }
38
+
39
+ .sessions-window-btn {
40
+ padding: 4px 10px;
41
+ background: transparent;
42
+ border: none;
43
+ border-radius: 4px;
44
+ color: var(--muted);
45
+ font-size: 12px;
46
+ cursor: pointer;
47
+ transition: background 0.15s, color 0.15s;
48
+ }
49
+
50
+ .sessions-window-btn:hover {
51
+ color: var(--text);
52
+ }
53
+
54
+ .sessions-window-btn.active {
55
+ background: var(--accent);
56
+ color: #0d1117;
57
+ font-weight: 600;
58
+ }
59
+
60
+ .sessions-summary-tiles {
61
+ display: grid;
62
+ grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
63
+ gap: 10px;
64
+ }
65
+
66
+ .sessions-chart {
67
+ background: var(--panel);
68
+ border: 1px solid var(--border);
69
+ border-radius: 8px;
70
+ padding: 12px;
71
+ }
72
+
73
+ .sessions-chart-empty,
74
+ .sessions-empty {
75
+ display: flex;
76
+ align-items: center;
77
+ justify-content: center;
78
+ min-height: 160px;
79
+ color: var(--muted);
80
+ font-size: 14px;
81
+ background: var(--panel);
82
+ border: 1px solid var(--border);
83
+ border-radius: 8px;
84
+ padding: 32px;
85
+ text-align: center;
86
+ }
87
+
88
+ .sessions-table-scroll {
89
+ overflow-x: auto;
90
+ background: var(--panel);
91
+ border: 1px solid var(--border);
92
+ border-radius: 8px;
93
+ }
94
+
95
+ .sessions-table {
96
+ width: 100%;
97
+ border-collapse: collapse;
98
+ font-size: 13px;
99
+ }
100
+
101
+ .sessions-table th,
102
+ .sessions-table td {
103
+ padding: 8px 10px;
104
+ text-align: left;
105
+ border-bottom: 1px solid var(--border);
106
+ white-space: nowrap;
107
+ }
108
+
109
+ .sessions-table th {
110
+ font-size: 11px;
111
+ font-weight: 600;
112
+ text-transform: uppercase;
113
+ letter-spacing: 0.5px;
114
+ color: var(--muted);
115
+ background: rgba(255, 255, 255, 0.02);
116
+ position: sticky;
117
+ top: 0;
118
+ }
119
+
120
+ .sessions-table td.num {
121
+ text-align: right;
122
+ font-variant-numeric: tabular-nums;
123
+ }
124
+
125
+ .sessions-table tr:last-child td {
126
+ border-bottom: none;
127
+ }
128
+
129
+ .sessions-table tr:hover td {
130
+ background: rgba(88, 166, 255, 0.06);
131
+ }
132
+
133
+ .sessions-empty-row td {
134
+ color: var(--muted);
135
+ font-style: italic;
136
+ text-align: center;
137
+ }
138
+
139
+ .sessions-pct-pill {
140
+ display: inline-block;
141
+ padding: 2px 8px;
142
+ border-radius: 10px;
143
+ font-size: 11px;
144
+ font-weight: 600;
145
+ font-variant-numeric: tabular-nums;
146
+ }
147
+
148
+ .sessions-pct-ok {
149
+ background: rgba(63, 185, 80, 0.15);
150
+ color: var(--ok);
151
+ }
152
+
153
+ .sessions-pct-warn {
154
+ background: rgba(210, 153, 34, 0.15);
155
+ color: var(--warn);
156
+ }
157
+
158
+ .sessions-pct-err {
159
+ background: rgba(248, 81, 73, 0.15);
160
+ color: var(--err);
161
+ }
162
+
163
+ .sessions-sparkline {
164
+ display: inline-block;
165
+ vertical-align: middle;
166
+ }
167
+
168
+ .sessions-sparkline-empty {
169
+ color: var(--muted);
170
+ font-size: 11px;
171
+ }
172
+
173
+ .sessions-meta {
174
+ font-size: 11px;
175
+ color: var(--muted);
176
+ }
177
+
178
+ .sessions-footer {
179
+ font-size: 11px;
180
+ color: var(--muted);
181
+ margin-top: 6px;
182
+ }
183
+
184
+ @media (max-width: 640px) {
185
+ .sessions-summary-tiles {
186
+ grid-template-columns: 1fr;
187
+ }
188
+ .sessions-table th,
189
+ .sessions-table td {
190
+ font-size: 12px;
191
+ padding: 6px 8px;
192
+ }
193
+ }
@@ -29,10 +29,13 @@ interface FlatGameState {
29
29
  }
30
30
  type FlatGameStatePatch = Partial<FlatGameState>;
31
31
 
32
- /** Extended snapshot config — the runtime includes tierPct (not in contract). */
33
- interface ConfigExt extends SnapshotResponse["config"] {
32
+ /** Extended snapshot config — the runtime includes tierPct (not in contract).
33
+ * Implemented as a type intersection rather than `interface extends` because
34
+ * TypeScript (TS2499) forbids `interface extends T["k"]` indexed-access
35
+ * parents; the resulting type is structurally identical. */
36
+ type ConfigExt = SnapshotResponse["config"] & {
34
37
  tierPct?: number | null;
35
- }
38
+ };
36
39
 
37
40
  /* Tooltips copied verbatim from extensions/dashboard-server/html.ts */
38
41
  const TOOLTIPS = {
@@ -18,9 +18,19 @@
18
18
  */
19
19
 
20
20
  import type React from "react";
21
- import type { SnapshotResponse } from "@contracts";
21
+ import { useCallback, useEffect, useMemo } from "react";
22
+ import { useApi } from "../hooks/useApi";
23
+ import { useSSE } from "../hooks/useSSE";
24
+ import { fetchSessions } from "../api/client";
25
+ import type {
26
+ SnapshotResponse,
27
+ SseEvent,
28
+ SseSessionSample,
29
+ SessionsResponse,
30
+ } from "@contracts";
22
31
  import type { RuntimeSnapshot } from "../utils/types";
23
32
  import { ContextGauge } from "../components/ContextGauge";
33
+ import { RepoContextStack } from "../components/RepoContextStack";
24
34
  import { TriggerStatus } from "../components/TriggerStatus";
25
35
  import { VectorStoreCard } from "../components/VectorStoreCard";
26
36
  import { RepoAllSessionsCard } from "../components/RepoAllSessionsCard";
@@ -32,6 +42,11 @@ import { CacheHitsCard } from "../components/CacheHitsCard";
32
42
  import { TimeSavedCard } from "../components/TimeSavedCard";
33
43
  import { LegendCard } from "../components/LegendCard";
34
44
 
45
+ /** Type guard: narrows an SSE event to a session_sample event. */
46
+ function isSessionSample(e: SseEvent): e is SseSessionSample {
47
+ return e.type === "session_sample";
48
+ }
49
+
35
50
  export interface OverviewTabProps {
36
51
  snapshot: SnapshotResponse | null;
37
52
  loading: boolean;
@@ -52,6 +67,34 @@ export default function OverviewTab({
52
67
  loading,
53
68
  error,
54
69
  }: OverviewTabProps): React.ReactElement {
70
+ // S40: real-time per-repo context via /api/sessions + SSE. Mirrors the
71
+ // SessionsTab pattern: 2s poll + session_sample SSE refetch.
72
+ const {
73
+ data: sessionsData,
74
+ error: sessionsErr,
75
+ loading: sessionsLoading,
76
+ refetch: refetchSessions,
77
+ } = useApi<SessionsResponse>(
78
+ useCallback(() => fetchSessions(), []),
79
+ { pollInterval: 2_000 },
80
+ );
81
+
82
+ const { events } = useSSE();
83
+
84
+ const sampleEvents = useMemo(
85
+ () => events.filter(isSessionSample),
86
+ [events],
87
+ );
88
+ const lastSampleTs = useMemo(() => {
89
+ if (sampleEvents.length === 0) return null;
90
+ return sampleEvents[sampleEvents.length - 1].ts;
91
+ }, [sampleEvents]);
92
+
93
+ useEffect(() => {
94
+ if (lastSampleTs == null) return;
95
+ refetchSessions();
96
+ }, [lastSampleTs, refetchSessions]);
97
+
55
98
  if (loading && !snapshot)
56
99
  return <div className="tab-stub">Loading snapshot…</div>;
57
100
  if (error && !snapshot)
@@ -83,6 +126,11 @@ export default function OverviewTab({
83
126
  percent={context.percent}
84
127
  contextWindow={context.contextWindow}
85
128
  />
129
+ <RepoContextStack
130
+ sessions={sessionsData}
131
+ loading={sessionsLoading}
132
+ error={sessionsErr}
133
+ />
86
134
  <TriggerStatus
87
135
  armed={trigger.armed}
88
136
  ready={trigger.ready}
@@ -0,0 +1,193 @@
1
+ /**
2
+ * dashboard-client/src/tabs/SessionsTab.tsx — Sessions tab (S39).
3
+ *
4
+ * Renders: window selector (5/15/30/60 min), sessions summary tiles,
5
+ * the stacked-memory graph (SessionsMemoryChart), and the active sessions
6
+ * table (ActiveSessionsTable).
7
+ *
8
+ * Data sources: fetchSessions() + fetchSessionTimeseries(minutes). Both poll
9
+ * every 2s for near-real-time updates. useSSE() listens for session_sample
10
+ * events emitted by appendTokenSample (Step 5) so the chart refetches
11
+ * instantly between polls when a new token sample is appended to events.log.
12
+ *
13
+ * Empty state: shows "No active sessions." when the /api/sessions response
14
+ * returns an empty array (e.g. no pi processes are running yet).
15
+ */
16
+
17
+ import type React from "react";
18
+ import { useCallback, useEffect, useMemo, useState } from "react";
19
+ import { useApi } from "../hooks/useApi";
20
+ import { useSSE } from "../hooks/useSSE";
21
+ import { fetchSessions, fetchSessionTimeseries } from "../api/client";
22
+ import type { SseEvent, SseSessionSample, SessionsResponse, SessionTimeseriesResponse } from "@contracts";
23
+ import { SessionsMemoryChart } from "../components/SessionsMemoryChart";
24
+ import { ActiveSessionsTable } from "../components/ActiveSessionsTable";
25
+
26
+ /**
27
+ * Shape of the {@link SseSessionSample} SSE event appended to events.log by
28
+ * `appendTokenSample` (src/store/sqlite/global-index.ts). The contract now
29
+ * defines this variant as part of the {@link SseEvent} union (Step 5); the
30
+ * local alias keeps the surface terser in this file.
31
+ */
32
+ type SessionSampleEvent = SseSessionSample;
33
+
34
+ /** Type guard: narrows an SSE event to a session_sample event. */
35
+ function isSessionSample(
36
+ e: SseEvent,
37
+ ): e is SessionSampleEvent {
38
+ return e.type === "session_sample";
39
+ }
40
+
41
+ const WINDOWS: ReadonlyArray<{ value: number; label: string }> = [
42
+ { value: 5, label: "5m" },
43
+ { value: 15, label: "15m" },
44
+ { value: 30, label: "30m" },
45
+ { value: 60, label: "60m" },
46
+ ] as const;
47
+
48
+ interface SummaryTilesProps {
49
+ sessions: SessionsResponse;
50
+ }
51
+
52
+ function SummaryTiles({ sessions }: SummaryTilesProps): React.ReactElement {
53
+ const active = sessions.sessions.length;
54
+ const combinedTokens = sessions.sessions.reduce(
55
+ (sum, s) => sum + (s.tokens ?? 0),
56
+ 0,
57
+ );
58
+ const maxWindow = sessions.sessions.reduce(
59
+ (max, s) => Math.max(max, s.ctxWindow),
60
+ 0,
61
+ );
62
+ const combinedPct = maxWindow > 0 ? (combinedTokens / maxWindow) * 100 : 0;
63
+ return (
64
+ <div className="sessions-summary-tiles">
65
+ <div className="tile">
66
+ <span className="tile-label">Active sessions</span>
67
+ <span className="tile-value">{active}</span>
68
+ </div>
69
+ <div className="tile">
70
+ <span className="tile-label">Combined tokens</span>
71
+ <span className="tile-value">
72
+ {combinedTokens >= 100_000
73
+ ? `${(combinedTokens / 1000).toFixed(0)}k`
74
+ : combinedTokens.toLocaleString()}
75
+ </span>
76
+ </div>
77
+ <div className="tile">
78
+ <span className="tile-label">Combined % of max window</span>
79
+ <span className="tile-value">{Math.round(combinedPct)}%</span>
80
+ </div>
81
+ <div className="tile">
82
+ <span className="tile-label">Pruned (stale)</span>
83
+ <span className="tile-value">{sessions.pruned}</span>
84
+ </div>
85
+ </div>
86
+ );
87
+ }
88
+
89
+ export default function SessionsTab(): React.ReactElement {
90
+ const [minutes, setMinutes] = useState(30);
91
+
92
+ const {
93
+ data: sessionsData,
94
+ error: sessionsErr,
95
+ refetch: refetchSessions,
96
+ } = useApi<SessionsResponse>(
97
+ useCallback(() => fetchSessions(), []),
98
+ { pollInterval: 2_000 },
99
+ );
100
+
101
+ const {
102
+ data: timeseriesData,
103
+ error: timeseriesErr,
104
+ refetch: refetchTimeseries,
105
+ } = useApi<SessionTimeseriesResponse>(
106
+ useCallback(() => fetchSessionTimeseries(minutes), [minutes]),
107
+ { pollInterval: 2_000 },
108
+ );
109
+
110
+ const { events } = useSSE();
111
+
112
+ // Filter SSE events for session_sample pushes (Step 5) and trigger an
113
+ // immediate refetch when a new sample arrives, so the chart updates
114
+ // between the 2s polls. The events array grows as new samples arrive; the
115
+ // length-based dependency means the effect re-runs when a new sample event
116
+ // lands (not on every poll). We debounce via a microtask guard.
117
+ const sampleEvents = useMemo(
118
+ () => events.filter(isSessionSample),
119
+ [events],
120
+ );
121
+
122
+ const lastSampleTs = useMemo(() => {
123
+ if (sampleEvents.length === 0) return null;
124
+ return sampleEvents[sampleEvents.length - 1].ts;
125
+ }, [sampleEvents]);
126
+
127
+ useEffect(() => {
128
+ if (lastSampleTs == null) return;
129
+ refetchSessions();
130
+ refetchTimeseries();
131
+ }, [lastSampleTs, refetchSessions, refetchTimeseries]);
132
+
133
+ if (sessionsErr && !sessionsData) {
134
+ return (
135
+ <div className="tab-stub">
136
+ Error loading sessions: {sessionsErr.message}
137
+ </div>
138
+ );
139
+ }
140
+ if (timeseriesErr && !timeseriesData) {
141
+ return (
142
+ <div className="tab-stub">
143
+ Error loading timeseries: {timeseriesErr.message}
144
+ </div>
145
+ );
146
+ }
147
+ if (!sessionsData) {
148
+ return <div className="tab-stub">Loading sessions…</div>;
149
+ }
150
+
151
+ const sessions = sessionsData.sessions;
152
+ const series = timeseriesData?.series ?? [];
153
+ const totals = timeseriesData?.totals ?? [];
154
+ const windowLabel =
155
+ WINDOWS.find((w) => w.value === minutes)?.label ?? `${minutes}m`;
156
+
157
+ return (
158
+ <div className="sessions-tab">
159
+ <div className="sessions-toolbar">
160
+ <h2>Session Memory Graph</h2>
161
+ <div className="sessions-window-select">
162
+ {WINDOWS.map((w) => (
163
+ <button
164
+ key={w.value}
165
+ type="button"
166
+ className={`sessions-window-btn ${minutes === w.value ? "active" : ""}`}
167
+ onClick={() => setMinutes(w.value)}
168
+ aria-pressed={minutes === w.value}
169
+ >
170
+ {w.label}
171
+ </button>
172
+ ))}
173
+ </div>
174
+ </div>
175
+
176
+ {sessions.length === 0 ? (
177
+ <div className="sessions-empty">No active sessions.</div>
178
+ ) : (
179
+ <>
180
+ <SummaryTiles sessions={sessionsData} />
181
+ <SessionsMemoryChart series={series} totals={totals} />
182
+ <h2 className="section-header">Active Sessions</h2>
183
+ <ActiveSessionsTable sessions={sessions} series={series} />
184
+ <div className="sessions-footer">
185
+ {series.length} series · {totals.length} timestamps ·{" "}
186
+ {windowLabel} window · updated{" "}
187
+ {timeseriesData?.updatedAt ?? sessionsData.updatedAt}
188
+ </div>
189
+ </>
190
+ )}
191
+ </div>
192
+ );
193
+ }
@@ -300,3 +300,28 @@ export interface SseCrewBanditChosen {
300
300
  /** Cumulative regret of the bandit policy (dimensionless). */
301
301
  regret: number;
302
302
  }
303
+
304
+ /**
305
+ * SSE event appened to events.log by `appendTokenSample`
306
+ * (src/store/sqlite/global-index.ts) on every material snapshot. The existing
307
+ * `GET /api/events` SSE handler tails events.log line-by-line, so these
308
+ * `session_sample` lines stream to the dashboard client for free
309
+ * (real-time chart updates between the 2s polls in the Sessions tab).
310
+ *
311
+ * Wire shape mirrors {@link SseCompactStart} etc. with `ts` (ISO 8601 string)
312
+ * plus `type` (`'session_sample'`) and the per-session payload.
313
+ *
314
+ * Served via `GET /api/events` (Server-Sent Events stream).
315
+ */
316
+ export interface SseSessionSample {
317
+ /** Discriminator. Always `'session_sample'`. */
318
+ type: 'session_sample';
319
+ /** ISO 8601 timestamp of the sample (coincides with the token_samples row `ts`). */
320
+ ts: string;
321
+ /** Session identifier the sample belongs to. */
322
+ sessionId: string;
323
+ /** Context window token count at this sample (tokens). */
324
+ tokens: number;
325
+ /** Context pressure percentage (0–100). */
326
+ percent: number;
327
+ }