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.
- package/README.md +1 -1
- package/dist/extensions/dashboard-server/api-contracts/endpoints.js +12 -0
- package/dist/extensions/dashboard-server/api-contracts.test.js +19 -6
- package/dist/extensions/dashboard-server/helpers.js +37 -0
- package/dist/extensions/dashboard-server/html/all-repos-tab.js +26 -0
- package/dist/extensions/dashboard-server/html/body-open.js +23 -0
- package/dist/extensions/dashboard-server/html/current-repo-tab.js +130 -0
- package/dist/extensions/dashboard-server/html/head-open.js +16 -0
- package/dist/extensions/dashboard-server/html/high-score-tab.js +25 -0
- package/dist/extensions/dashboard-server/html/repo-detail-modal.js +26 -0
- package/dist/extensions/dashboard-server/html/script.js +259 -0
- package/dist/extensions/dashboard-server/html/styles.js +103 -0
- package/dist/extensions/dashboard-server/html/summary-tab.js +19 -0
- package/dist/extensions/dashboard-server/html-template.js +41 -0
- package/dist/extensions/dashboard-server/server.js +83 -0
- package/dist/extensions/mega-compact-s38.test.js +33 -0
- package/dist/extensions/mega-events/agent-handlers.js +51 -3
- package/dist/extensions/mega-events/compact-handlers.js +10 -4
- package/dist/extensions/mega-events/error-classifier.js +24 -1
- package/dist/extensions/mega-events/send-safe.js +12 -0
- package/dist/extensions/mega-events.js +1 -0
- package/dist/extensions/mega-runtime/state.js +17 -1
- package/dist/src/store/sqlite/connection.js +35 -0
- package/dist/src/store/sqlite/global-index.js +199 -1
- package/dist/src/store/sqlite/global-index.test.js +136 -0
- package/dist/src/store/sqlite/index-store.js +167 -0
- package/dist/src/store/sqlite/memory.js +54 -0
- package/dist/src/store/sqlite/minhash-lsh.js +47 -0
- package/dist/src/store/sqlite/sessions.js +39 -0
- package/dist/src/store/sqlite/transaction.js +19 -0
- package/dist/src/vectorStore/add.js +260 -0
- package/dist/src/vectorStore/dedup.js +52 -0
- package/dist/src/vectorStore/index.js +10 -0
- package/dist/src/vectorStore/queries.js +83 -0
- package/dist/src/vectorStore/search.js +95 -0
- package/dist/src/vectorStore/session.js +19 -0
- package/dist/src/vectorStore/store.js +105 -0
- package/dist/src/vectorStore/types.js +6 -0
- package/dist/src/vectorStore/utils.js +23 -0
- package/extensions/dashboard-client/dist/assets/{AchievementTiles-BozP_h3c.js → AchievementTiles-CNWZ5NjP.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AchievementTiles-BozP_h3c.js.map → AchievementTiles-CNWZ5NjP.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{AchievementsTab-DKgqx1RQ.js → AchievementsTab-BKPifNKj.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AchievementsTab-DKgqx1RQ.js.map → AchievementsTab-BKPifNKj.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{CacheTab-CPvso4-0.js → CacheTab--epzrQHD.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{CacheTab-CPvso4-0.js.map → CacheTab--epzrQHD.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{ConfigTab-DXD7zbqy.js → ConfigTab-D3ralwSj.js} +2 -2
- package/extensions/dashboard-client/dist/assets/ConfigTab-D3ralwSj.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/EventsTab-BKkuVQsM.js +2 -0
- package/extensions/dashboard-client/dist/assets/EventsTab-BKkuVQsM.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{GameTab-Qj19W1Y-.js → GameTab-CgUvITYy.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{GameTab-Qj19W1Y-.js.map → GameTab-CgUvITYy.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MetricsTab-DA27vA95.js → MetricsTab-DrlJqXGL.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MetricsTab-DA27vA95.js.map → MetricsTab-DrlJqXGL.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{OverviewTab-lVGR1Jl_.js → OverviewTab-_gD756wJ.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{OverviewTab-lVGR1Jl_.js.map → OverviewTab-_gD756wJ.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{ReposTab-_HNR3qYj.js → ReposTab-DjEZmqUb.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ReposTab-_HNR3qYj.js.map → ReposTab-DjEZmqUb.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/SessionsTab-DsYMUzoh.js +70 -0
- package/extensions/dashboard-client/dist/assets/SessionsTab-DsYMUzoh.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-B7aPcBMs.js → TimeSavedCard-BbAXT1wO.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-B7aPcBMs.js.map → TimeSavedCard-BbAXT1wO.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{index-Cfxniu2R.css → index-BTft6DM_.css} +1 -1
- package/extensions/dashboard-client/dist/assets/{index-B2JJExE9.js → index-CnRzusfi.js} +10 -10
- package/extensions/dashboard-client/dist/assets/{index-B2JJExE9.js.map → index-CnRzusfi.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/useSSE-Bf-1rZ2N.js +2 -0
- package/extensions/dashboard-client/dist/assets/useSSE-Bf-1rZ2N.js.map +1 -0
- package/extensions/dashboard-client/dist/index.html +2 -2
- package/extensions/dashboard-client/package-lock.json +374 -2
- package/extensions/dashboard-client/package.json +2 -1
- package/extensions/dashboard-client/src/App.tsx +5 -1
- package/extensions/dashboard-client/src/api/client.ts +12 -0
- package/extensions/dashboard-client/src/components/ActiveSessionsTable.tsx +185 -0
- package/extensions/dashboard-client/src/components/EventStream.tsx +3 -0
- package/extensions/dashboard-client/src/components/SessionsMemoryChart.tsx +199 -0
- package/extensions/dashboard-client/src/main.tsx +1 -0
- package/extensions/dashboard-client/src/styles/sessions.css +193 -0
- package/extensions/dashboard-client/src/tabs/ConfigTab.tsx +6 -3
- package/extensions/dashboard-client/src/tabs/SessionsTab.tsx +193 -0
- package/extensions/dashboard-server/api-contracts/core.ts +25 -0
- package/extensions/dashboard-server/api-contracts/endpoints.ts +102 -0
- package/extensions/dashboard-server/api-contracts/index.ts +10 -1
- package/extensions/dashboard-server/api-contracts.test.ts +20 -7
- package/extensions/dashboard-server/server.ts +100 -0
- package/extensions/mega-compact-s38.test.ts +63 -0
- package/extensions/mega-events/agent-handlers.ts +47 -3
- package/extensions/mega-events/compact-handlers.ts +11 -4
- package/extensions/mega-events/error-classifier.ts +25 -1
- package/extensions/mega-events/send-safe.ts +37 -0
- package/extensions/mega-events.ts +1 -0
- package/extensions/mega-runtime/state.ts +31 -0
- package/package.json +1 -1
- package/src/store/sqlite/global-index.test.ts +222 -0
- package/src/store/sqlite/global-index.ts +316 -1
- package/dist/extensions/dashboard-client/src/hooks/useApi.js +0 -51
- package/dist/extensions/dashboard-client/src/hooks/useSSE.js +0 -63
- package/extensions/dashboard-client/dist/assets/ConfigTab-DXD7zbqy.js.map +0 -1
- package/extensions/dashboard-client/dist/assets/EventsTab-BGeuYvM3.js +0 -2
- 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
|
+
}
|
|
@@ -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
|
|
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 = {
|