pi-mega-compact 0.8.16 → 0.8.18

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 (98) hide show
  1. package/README.md +1 -1
  2. package/dist/extensions/dashboard-server/api-contracts/endpoints.js +12 -0
  3. package/dist/extensions/dashboard-server/api-contracts.test.js +19 -6
  4. package/dist/extensions/dashboard-server/helpers.js +37 -0
  5. package/dist/extensions/dashboard-server/html/all-repos-tab.js +26 -0
  6. package/dist/extensions/dashboard-server/html/body-open.js +23 -0
  7. package/dist/extensions/dashboard-server/html/current-repo-tab.js +130 -0
  8. package/dist/extensions/dashboard-server/html/head-open.js +16 -0
  9. package/dist/extensions/dashboard-server/html/high-score-tab.js +25 -0
  10. package/dist/extensions/dashboard-server/html/repo-detail-modal.js +26 -0
  11. package/dist/extensions/dashboard-server/html/script.js +259 -0
  12. package/dist/extensions/dashboard-server/html/styles.js +103 -0
  13. package/dist/extensions/dashboard-server/html/summary-tab.js +19 -0
  14. package/dist/extensions/dashboard-server/html-template.js +41 -0
  15. package/dist/extensions/dashboard-server/server.js +83 -0
  16. package/dist/extensions/mega-compact-s38.test.js +33 -0
  17. package/dist/extensions/mega-events/agent-handlers.js +51 -3
  18. package/dist/extensions/mega-events/compact-handlers.js +10 -4
  19. package/dist/extensions/mega-events/error-classifier.js +24 -1
  20. package/dist/extensions/mega-events/send-safe.js +12 -0
  21. package/dist/extensions/mega-events.js +1 -0
  22. package/dist/extensions/mega-runtime/state.js +17 -1
  23. package/dist/src/store/sqlite/connection.js +35 -0
  24. package/dist/src/store/sqlite/global-index.js +199 -1
  25. package/dist/src/store/sqlite/global-index.test.js +136 -0
  26. package/dist/src/store/sqlite/index-store.js +167 -0
  27. package/dist/src/store/sqlite/memory.js +54 -0
  28. package/dist/src/store/sqlite/minhash-lsh.js +47 -0
  29. package/dist/src/store/sqlite/sessions.js +39 -0
  30. package/dist/src/store/sqlite/transaction.js +19 -0
  31. package/dist/src/vectorStore/add.js +260 -0
  32. package/dist/src/vectorStore/dedup.js +52 -0
  33. package/dist/src/vectorStore/index.js +10 -0
  34. package/dist/src/vectorStore/queries.js +83 -0
  35. package/dist/src/vectorStore/search.js +95 -0
  36. package/dist/src/vectorStore/session.js +19 -0
  37. package/dist/src/vectorStore/store.js +105 -0
  38. package/dist/src/vectorStore/types.js +6 -0
  39. package/dist/src/vectorStore/utils.js +23 -0
  40. package/extensions/dashboard-client/dist/assets/{AchievementTiles-BozP_h3c.js → AchievementTiles-CNWZ5NjP.js} +2 -2
  41. package/extensions/dashboard-client/dist/assets/{AchievementTiles-BozP_h3c.js.map → AchievementTiles-CNWZ5NjP.js.map} +1 -1
  42. package/extensions/dashboard-client/dist/assets/{AchievementsTab-DKgqx1RQ.js → AchievementsTab-BKPifNKj.js} +2 -2
  43. package/extensions/dashboard-client/dist/assets/{AchievementsTab-DKgqx1RQ.js.map → AchievementsTab-BKPifNKj.js.map} +1 -1
  44. package/extensions/dashboard-client/dist/assets/{CacheTab-CPvso4-0.js → CacheTab--epzrQHD.js} +2 -2
  45. package/extensions/dashboard-client/dist/assets/{CacheTab-CPvso4-0.js.map → CacheTab--epzrQHD.js.map} +1 -1
  46. package/extensions/dashboard-client/dist/assets/{ConfigTab-DXD7zbqy.js → ConfigTab-D3ralwSj.js} +2 -2
  47. package/extensions/dashboard-client/dist/assets/ConfigTab-D3ralwSj.js.map +1 -0
  48. package/extensions/dashboard-client/dist/assets/EventsTab-BKkuVQsM.js +2 -0
  49. package/extensions/dashboard-client/dist/assets/EventsTab-BKkuVQsM.js.map +1 -0
  50. package/extensions/dashboard-client/dist/assets/{GameTab-Qj19W1Y-.js → GameTab-CgUvITYy.js} +2 -2
  51. package/extensions/dashboard-client/dist/assets/{GameTab-Qj19W1Y-.js.map → GameTab-CgUvITYy.js.map} +1 -1
  52. package/extensions/dashboard-client/dist/assets/{MetricsTab-DA27vA95.js → MetricsTab-DrlJqXGL.js} +2 -2
  53. package/extensions/dashboard-client/dist/assets/{MetricsTab-DA27vA95.js.map → MetricsTab-DrlJqXGL.js.map} +1 -1
  54. package/extensions/dashboard-client/dist/assets/{OverviewTab-lVGR1Jl_.js → OverviewTab-_gD756wJ.js} +2 -2
  55. package/extensions/dashboard-client/dist/assets/{OverviewTab-lVGR1Jl_.js.map → OverviewTab-_gD756wJ.js.map} +1 -1
  56. package/extensions/dashboard-client/dist/assets/{ReposTab-_HNR3qYj.js → ReposTab-DjEZmqUb.js} +2 -2
  57. package/extensions/dashboard-client/dist/assets/{ReposTab-_HNR3qYj.js.map → ReposTab-DjEZmqUb.js.map} +1 -1
  58. package/extensions/dashboard-client/dist/assets/SessionsTab-DsYMUzoh.js +70 -0
  59. package/extensions/dashboard-client/dist/assets/SessionsTab-DsYMUzoh.js.map +1 -0
  60. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-B7aPcBMs.js → TimeSavedCard-BbAXT1wO.js} +2 -2
  61. package/extensions/dashboard-client/dist/assets/{TimeSavedCard-B7aPcBMs.js.map → TimeSavedCard-BbAXT1wO.js.map} +1 -1
  62. package/extensions/dashboard-client/dist/assets/{index-Cfxniu2R.css → index-BTft6DM_.css} +1 -1
  63. package/extensions/dashboard-client/dist/assets/{index-B2JJExE9.js → index-CnRzusfi.js} +10 -10
  64. package/extensions/dashboard-client/dist/assets/{index-B2JJExE9.js.map → index-CnRzusfi.js.map} +1 -1
  65. package/extensions/dashboard-client/dist/assets/useSSE-Bf-1rZ2N.js +2 -0
  66. package/extensions/dashboard-client/dist/assets/useSSE-Bf-1rZ2N.js.map +1 -0
  67. package/extensions/dashboard-client/dist/index.html +2 -2
  68. package/extensions/dashboard-client/package-lock.json +374 -2
  69. package/extensions/dashboard-client/package.json +2 -1
  70. package/extensions/dashboard-client/src/App.tsx +5 -1
  71. package/extensions/dashboard-client/src/api/client.ts +12 -0
  72. package/extensions/dashboard-client/src/components/ActiveSessionsTable.tsx +185 -0
  73. package/extensions/dashboard-client/src/components/EventStream.tsx +3 -0
  74. package/extensions/dashboard-client/src/components/SessionsMemoryChart.tsx +199 -0
  75. package/extensions/dashboard-client/src/main.tsx +1 -0
  76. package/extensions/dashboard-client/src/styles/sessions.css +193 -0
  77. package/extensions/dashboard-client/src/tabs/ConfigTab.tsx +6 -3
  78. package/extensions/dashboard-client/src/tabs/SessionsTab.tsx +193 -0
  79. package/extensions/dashboard-server/api-contracts/core.ts +25 -0
  80. package/extensions/dashboard-server/api-contracts/endpoints.ts +102 -0
  81. package/extensions/dashboard-server/api-contracts/index.ts +10 -1
  82. package/extensions/dashboard-server/api-contracts.test.ts +20 -7
  83. package/extensions/dashboard-server/server.ts +100 -0
  84. package/extensions/mega-compact-s38.test.ts +63 -0
  85. package/extensions/mega-events/agent-handlers.ts +47 -3
  86. package/extensions/mega-events/compact-handlers.ts +11 -4
  87. package/extensions/mega-events/error-classifier.ts +25 -1
  88. package/extensions/mega-events/send-safe.ts +37 -0
  89. package/extensions/mega-events.ts +1 -0
  90. package/extensions/mega-runtime/state.ts +31 -0
  91. package/package.json +1 -1
  92. package/src/store/sqlite/global-index.test.ts +222 -0
  93. package/src/store/sqlite/global-index.ts +316 -1
  94. package/dist/extensions/dashboard-client/src/hooks/useApi.js +0 -51
  95. package/dist/extensions/dashboard-client/src/hooks/useSSE.js +0 -63
  96. package/extensions/dashboard-client/dist/assets/ConfigTab-DXD7zbqy.js.map +0 -1
  97. package/extensions/dashboard-client/dist/assets/EventsTab-BGeuYvM3.js +0 -2
  98. package/extensions/dashboard-client/dist/assets/EventsTab-BGeuYvM3.js.map +0 -1
@@ -0,0 +1,185 @@
1
+ /**
2
+ * dashboard-client/src/components/ActiveSessionsTable.tsx — Active sessions table (S39).
3
+ *
4
+ * Per-session rows: PID, Repo, Model, Context %, Tokens/Window,
5
+ * Heartbeat age, State dir, Sparkline.
6
+ *
7
+ * Data from /api/sessions (active sessions list) and /api/sessions/timeseries
8
+ * (per-session data points used to render inline sparklines).
9
+ */
10
+
11
+ import type React from "react";
12
+ import type { ActiveSession, SessionSeries } from "@contracts";
13
+
14
+ export interface ActiveSessionsTableProps {
15
+ /** Active sessions, sorted by lastSeen descending (from /api/sessions). */
16
+ sessions: ActiveSession[];
17
+ /** Per-session timeseries used to render the inline sparkline column. */
18
+ series: SessionSeries[];
19
+ }
20
+
21
+ const HEADERS = [
22
+ "PID",
23
+ "Repo",
24
+ "Model",
25
+ "Context %",
26
+ "Tokens / Window",
27
+ "Last Heartbeat",
28
+ "State Dir",
29
+ "Trend",
30
+ ] as const;
31
+
32
+ /** Format a duration in milliseconds → human-readable age string. */
33
+ function fmtAge(ms: number): string {
34
+ const s = Math.max(0, Math.round(ms / 1000));
35
+ if (s < 60) return `${s}s ago`;
36
+ const m = Math.floor(s / 60);
37
+ if (m < 60) return `${m}m ${s % 60}s ago`;
38
+ const h = Math.floor(m / 60);
39
+ return `${h}h ${m % 60}m ago`;
40
+ }
41
+
42
+ function fmtTokens(n: number | null): string {
43
+ if (n == null) return "\u2014";
44
+ if (n >= 100_000) return `${(n / 1000).toFixed(0)}k`;
45
+ if (n >= 1000) return `${(n / 1000).toFixed(1)}k`;
46
+ return n.toLocaleString();
47
+ }
48
+
49
+ function pctClass(pct: number | null): string {
50
+ if (pct == null) return "sessions-pct-ok";
51
+ if (pct >= 85) return "sessions-pct-err";
52
+ if (pct >= 60) return "sessions-pct-warn";
53
+ return "sessions-pct-ok";
54
+ }
55
+
56
+ function fmtPct(pct: number | null): string {
57
+ if (pct == null) return "\u2014";
58
+ return `${Math.round(pct)}%`;
59
+ }
60
+
61
+ function fmtWindow(tokens: number | null, window: number): string {
62
+ if (tokens == null) return "\u2014";
63
+ return `${fmtTokens(tokens)} / ${fmtTokens(window)}`;
64
+ }
65
+
66
+ interface SparklineProps {
67
+ /** Token counts over time (oldest → newest). */
68
+ points: number[];
69
+ /** Stable hex color for the stroke. */
70
+ color: string;
71
+ }
72
+
73
+ function Sparkline({ points, color }: SparklineProps): React.ReactElement {
74
+ if (points.length < 2) {
75
+ return <span className="sessions-sparkline-empty">{"\u2014"}</span>;
76
+ }
77
+ const min = Math.min(...points);
78
+ const max = Math.max(...points);
79
+ const range = max - min || 1;
80
+ const w = 64;
81
+ const h = 18;
82
+ const stepX = w / (points.length - 1);
83
+ const segments = points.map((p, i) => {
84
+ const x = i * stepX;
85
+ const y = h - ((p - min) / range) * h;
86
+ return `${i === 0 ? "M" : "L"} ${x.toFixed(1)} ${y.toFixed(1)}`;
87
+ });
88
+ return (
89
+ <svg
90
+ className="sessions-sparkline"
91
+ width={w}
92
+ height={h}
93
+ viewBox={`0 0 ${w} ${h}`}
94
+ role="img"
95
+ aria-label="token trend"
96
+ >
97
+ <path
98
+ d={segments.join(" ")}
99
+ fill="none"
100
+ stroke={color}
101
+ strokeWidth="1.5"
102
+ strokeLinejoin="round"
103
+ strokeLinecap="round"
104
+ />
105
+ </svg>
106
+ );
107
+ }
108
+
109
+ /** Map sessionId → series for the sparkline column lookup. */
110
+ function indexSeries(
111
+ series: SessionSeries[],
112
+ ): Map<string, SessionSeries> {
113
+ const m = new Map<string, SessionSeries>();
114
+ for (const s of series) m.set(s.sessionId, s);
115
+ return m;
116
+ }
117
+
118
+ export function ActiveSessionsTable({
119
+ sessions,
120
+ series,
121
+ }: ActiveSessionsTableProps): React.ReactElement {
122
+ const seriesById = indexSeries(series);
123
+ const now = Date.now();
124
+
125
+ return (
126
+ <div className="sessions-table-scroll">
127
+ <table className="sessions-table">
128
+ <thead>
129
+ <tr>
130
+ {HEADERS.map((h, i) => (
131
+ <th
132
+ key={h}
133
+ className={i >= 3 && i !== 6 ? "num" : undefined}
134
+ >
135
+ {h}
136
+ </th>
137
+ ))}
138
+ </tr>
139
+ </thead>
140
+ <tbody>
141
+ {sessions.length === 0 && (
142
+ <tr className="sessions-empty-row">
143
+ <td colSpan={HEADERS.length}>No active sessions.</td>
144
+ </tr>
145
+ )}
146
+ {sessions.map((s) => {
147
+ const srs = seriesById.get(s.sessionId);
148
+ const sparkPts =
149
+ srs?.data.map((d) => d.tokens) ?? [];
150
+ const color = srs?.color ?? "#58a6ff";
151
+ return (
152
+ <tr key={`${s.pid}:${s.sessionId}`}>
153
+ <td className="num">{s.pid}</td>
154
+ <td title={s.repoRoot ?? undefined}>
155
+ {s.displayName || s.repoRoot || "\u2014"}
156
+ </td>
157
+ <td>{s.model ?? "\u2014"}</td>
158
+ <td className="num">
159
+ <span className={`sessions-pct-pill ${pctClass(s.percent)}`}>
160
+ {fmtPct(s.percent)}
161
+ </span>
162
+ </td>
163
+ <td className="num">
164
+ {fmtWindow(s.tokens, s.ctxWindow)}
165
+ </td>
166
+ <td className="num">{fmtAge(now - s.lastSeen)}</td>
167
+ <td
168
+ title={s.stateDir ?? undefined}
169
+ className="sessions-meta"
170
+ >
171
+ {s.stateDir
172
+ ? s.stateDir.split("/").pop() || s.stateDir
173
+ : "\u2014"}
174
+ </td>
175
+ <td>
176
+ <Sparkline points={sparkPts} color={color} />
177
+ </td>
178
+ </tr>
179
+ );
180
+ })}
181
+ </tbody>
182
+ </table>
183
+ </div>
184
+ );
185
+ }
@@ -30,6 +30,7 @@ const TYPE_COLORS: Record<string, string> = {
30
30
  crew_presence_changed: "ev-crew",
31
31
  crew_turn_changed: "ev-crew",
32
32
  crew_bandit_chosen: "ev-crew",
33
+ session_sample: "ev-recall",
33
34
  game_ritual_start: "ev-game",
34
35
  game_ritual_stage: "ev-game",
35
36
  game_ritual_end: "ev-game",
@@ -81,6 +82,8 @@ function summarize(ev: SseEvent): string {
81
82
  return `turn=${ev.turnIndex} agent=${ev.agentName}`;
82
83
  case "crew_bandit_chosen":
83
84
  return `agent=${ev.chosenAgent} score=${ev.score} regret=${ev.regret}`;
85
+ case "session_sample":
86
+ return `sess=${ev.sessionId.slice(0, 8)} tok=${ev.tokens.toLocaleString()} ctx=${Math.round(ev.percent)}%`;
84
87
  case "game_ritual_start":
85
88
  case "game_ritual_stage":
86
89
  case "game_ritual_end":
@@ -0,0 +1,199 @@
1
+ /**
2
+ * dashboard-client/src/components/SessionsMemoryChart.tsx — Stacked memory graph (S39).
3
+ *
4
+ * Recharts stacked `<Area>` per session + total `<Line>` + `<Tooltip>` +
5
+ * `<Legend>` + `<ResponsiveContainer>`. Data comes from `/api/sessions/timeseries`
6
+ * which returns `SessionSeries[]` (per-session data points + stable hex color)
7
+ * and a `totals[]` array (sum across all sessions per timestamp).
8
+ *
9
+ * PREVENT-PI-004: recharts is bundled into this localhost-served static
10
+ * dashboard bundle; it makes no runtime network calls. Chart data arrives via
11
+ * same-origin fetch to the dashboard server (loopback, ports 9320–9329).
12
+ */
13
+
14
+ import type React from "react";
15
+ import { useMemo } from "react";
16
+ import {
17
+ ResponsiveContainer,
18
+ AreaChart,
19
+ Area,
20
+ Line,
21
+ XAxis,
22
+ YAxis,
23
+ CartesianGrid,
24
+ Tooltip,
25
+ Legend,
26
+ } from "recharts";
27
+ import type { SessionSeries } from "@contracts";
28
+
29
+ export interface SessionsMemoryChartProps {
30
+ /** Per-session series with stable colors (from /api/sessions/timeseries). */
31
+ series: SessionSeries[];
32
+ /** Totals (sum across all sessions) per timestamp. */
33
+ totals: ReadonlyArray<{ ts: number; tokens: number }>;
34
+ }
35
+
36
+ /** A row in the pivoted data table passed to recharts `<AreaChart>`. */
37
+ interface PivotedRow {
38
+ /** Unix timestamp (ms). */
39
+ ts: number;
40
+ /** Total tokens across all sessions at this timestamp. */
41
+ total: number;
42
+ /** Per-session token count keyed by sessionId. */
43
+ [sessionId: string]: number;
44
+ }
45
+
46
+ /**
47
+ * Pivot the per-session series + totals into recharts' expected flat shape:
48
+ * one row per unique timestamp, with each session's token count as a column
49
+ * keyed by sessionId. Missing values for a session at a given timestamp are
50
+ * filled with zero so the stack renders continuously.
51
+ */
52
+ function pivot(
53
+ series: SessionSeries[],
54
+ totals: ReadonlyArray<{ ts: number; tokens: number }>,
55
+ ): PivotedRow[] {
56
+ const map = new Map<number, PivotedRow>();
57
+ const sessionIds = series.map((s) => s.sessionId);
58
+
59
+ for (const s of series) {
60
+ for (const pt of s.data) {
61
+ let row = map.get(pt.ts);
62
+ if (!row) {
63
+ row = { ts: pt.ts, total: 0 } as PivotedRow;
64
+ for (const sid of sessionIds) row[sid] = 0;
65
+ map.set(pt.ts, row);
66
+ }
67
+ row[s.sessionId] = pt.tokens;
68
+ }
69
+ }
70
+ for (const t of totals) {
71
+ let row = map.get(t.ts);
72
+ if (!row) {
73
+ row = { ts: t.ts, total: 0 } as PivotedRow;
74
+ for (const sid of sessionIds) row[sid] = 0;
75
+ map.set(t.ts, row);
76
+ }
77
+ row.total = t.tokens;
78
+ }
79
+
80
+ return [...map.values()].sort((a, b) => a.ts - b.ts);
81
+ }
82
+
83
+ function fmtTs(ts: number): string {
84
+ return new Date(ts).toLocaleTimeString([], {
85
+ hour: "2-digit",
86
+ minute: "2-digit",
87
+ second: "2-digit",
88
+ });
89
+ }
90
+
91
+ function fmtTokens(value: number | string): string {
92
+ const n = Number(value);
93
+ return Number.isFinite(n) ? n.toLocaleString() : String(value);
94
+ }
95
+
96
+ /** Sanitize an arbitrary sessionId for use in an SVG gradient id. */
97
+ function gradId(sessionId: string): string {
98
+ const safe = sessionId.replace(/[^a-zA-Z0-9_-]/g, "_");
99
+ return `grad-${safe}`;
100
+ }
101
+
102
+ export function SessionsMemoryChart({
103
+ series,
104
+ totals,
105
+ }: SessionsMemoryChartProps): React.ReactElement {
106
+ const data = useMemo(
107
+ () => pivot(series, totals),
108
+ [series, totals],
109
+ );
110
+
111
+ if (series.length === 0) {
112
+ return (
113
+ <div className="sessions-chart-empty">
114
+ No token samples in this window yet.
115
+ </div>
116
+ );
117
+ }
118
+
119
+ return (
120
+ <div className="sessions-chart">
121
+ <ResponsiveContainer width="100%" height={360}>
122
+ <AreaChart
123
+ data={data}
124
+ margin={{ top: 8, right: 16, left: 0, bottom: 0 }}
125
+ >
126
+ <defs>
127
+ {series.map((s) => (
128
+ <linearGradient
129
+ key={`${s.sessionId}-grad`}
130
+ id={gradId(s.sessionId)}
131
+ x1="0"
132
+ y1="0"
133
+ x2="0"
134
+ y2="1"
135
+ >
136
+ <stop offset="5%" stopColor={s.color} stopOpacity={0.7} />
137
+ <stop offset="95%" stopColor={s.color} stopOpacity={0.25} />
138
+ </linearGradient>
139
+ ))}
140
+ </defs>
141
+ <CartesianGrid strokeDasharray="3 3" stroke="#30363d" />
142
+ <XAxis
143
+ dataKey="ts"
144
+ type="number"
145
+ domain={["dataMin", "dataMax"]}
146
+ tickFormatter={(ts: number) => fmtTs(ts)}
147
+ stroke="#8b949e"
148
+ tick={{ fontSize: 11 }}
149
+ scale="time"
150
+ />
151
+ <YAxis
152
+ stroke="#8b949e"
153
+ tick={{ fontSize: 11 }}
154
+ width={56}
155
+ tickFormatter={(v: number) => fmtTokens(v)}
156
+ />
157
+ <Tooltip
158
+ labelFormatter={(ts: number) => fmtTs(ts)}
159
+ formatter={(value) =>
160
+ [fmtTokens(value as number), "tokens"] as [string, string]
161
+ }
162
+ contentStyle={{
163
+ background: "#161b22",
164
+ border: "1px solid #30363d",
165
+ borderRadius: "6px",
166
+ color: "#e6edf3",
167
+ }}
168
+ labelStyle={{ color: "#8b949e", fontSize: 12 }}
169
+ />
170
+ <Legend wrapperStyle={{ fontSize: 12 }} />
171
+ {series.map((s) => (
172
+ <Area
173
+ key={s.sessionId}
174
+ type="monotone"
175
+ dataKey={s.sessionId}
176
+ name={s.label}
177
+ stackId="1"
178
+ stroke={s.color}
179
+ fill={`url(#${gradId(s.sessionId)})`}
180
+ fillOpacity={1}
181
+ isAnimationActive={false}
182
+ dot={false}
183
+ />
184
+ ))}
185
+ <Line
186
+ type="monotone"
187
+ dataKey="total"
188
+ name="Total"
189
+ stroke="#e6edf3"
190
+ strokeWidth={1.5}
191
+ strokeDasharray="4 2"
192
+ dot={false}
193
+ isAnimationActive={false}
194
+ />
195
+ </AreaChart>
196
+ </ResponsiveContainer>
197
+ </div>
198
+ );
199
+ }
@@ -15,6 +15,7 @@ import "./styles/overview-extra.css";
15
15
  import "./styles/config.css";
16
16
  import "./styles/metrics-extra.css";
17
17
  import "./styles/game-achievements.css";
18
+ import "./styles/sessions.css";
18
19
 
19
20
  const rootEl = document.getElementById("root");
20
21
  if (!rootEl) {
@@ -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 = {