pi-mega-compact 0.8.7 → 0.8.10

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 (110) hide show
  1. package/dist/extensions/dashboard-server/api-contracts/core.js +7 -0
  2. package/dist/extensions/dashboard-server/api-contracts/endpoints.js +104 -0
  3. package/dist/extensions/dashboard-server/api-contracts/game.js +9 -0
  4. package/dist/extensions/dashboard-server/api-contracts/index.js +9 -0
  5. package/dist/extensions/dashboard-server/api-contracts/infrastructure.js +10 -0
  6. package/dist/extensions/dashboard-server/api-contracts/multi-repo.js +9 -0
  7. package/dist/extensions/dashboard-server/api-contracts/snapshot.js +8 -0
  8. package/dist/extensions/dashboard-server/api-contracts.js +8 -0
  9. package/dist/extensions/dashboard-server/api-contracts.test.js +869 -0
  10. package/dist/extensions/dashboard-server/auth.js +18 -0
  11. package/dist/extensions/dashboard-server/helpers.js +37 -0
  12. package/dist/extensions/dashboard-server/html/all-repos-tab.js +26 -0
  13. package/dist/extensions/dashboard-server/html/body-open.js +23 -0
  14. package/dist/extensions/dashboard-server/html/current-repo-tab.js +130 -0
  15. package/dist/extensions/dashboard-server/html/head-open.js +16 -0
  16. package/dist/extensions/dashboard-server/html/high-score-tab.js +25 -0
  17. package/dist/extensions/dashboard-server/html/repo-detail-modal.js +26 -0
  18. package/dist/extensions/dashboard-server/html/script.js +259 -0
  19. package/dist/extensions/dashboard-server/html/styles.js +103 -0
  20. package/dist/extensions/dashboard-server/html/summary-tab.js +19 -0
  21. package/dist/extensions/dashboard-server/html-template.js +41 -0
  22. package/dist/extensions/dashboard-server/html.js +46 -1
  23. package/dist/extensions/dashboard-server/perf-server.test.js +80 -0
  24. package/dist/extensions/dashboard-server/server.js +269 -30
  25. package/dist/extensions/dashboard-server/tailscale.js +8 -0
  26. package/dist/extensions/dashboard-server/types.js +4 -0
  27. package/dist/extensions/mega-dashboard.js +9 -0
  28. package/dist/extensions/mega-events/perf-handler.js +71 -0
  29. package/dist/extensions/mega-events/register.js +2 -0
  30. package/dist/extensions/mega-events.js +1 -0
  31. package/dist/extensions/mega-runtime/state.js +59 -1
  32. package/dist/src/store/sqlite/connection.js +35 -0
  33. package/dist/src/store/sqlite/index-store.js +167 -0
  34. package/dist/src/store/sqlite/memory.js +54 -0
  35. package/dist/src/store/sqlite/minhash-lsh.js +47 -0
  36. package/dist/src/store/sqlite/perf-samples.js +81 -0
  37. package/dist/src/store/sqlite/perf-samples.test.js +54 -0
  38. package/dist/src/store/sqlite/schema.js +14 -0
  39. package/dist/src/store/sqlite/sessions.js +39 -0
  40. package/dist/src/store/sqlite/transaction.js +19 -0
  41. package/dist/src/store/sqlite.js +1 -0
  42. package/dist/src/vectorStore/add.js +260 -0
  43. package/dist/src/vectorStore/dedup.js +52 -0
  44. package/dist/src/vectorStore/index.js +10 -0
  45. package/dist/src/vectorStore/queries.js +83 -0
  46. package/dist/src/vectorStore/search.js +95 -0
  47. package/dist/src/vectorStore/session.js +19 -0
  48. package/dist/src/vectorStore/store.js +105 -0
  49. package/dist/src/vectorStore/types.js +6 -0
  50. package/dist/src/vectorStore/utils.js +23 -0
  51. package/extensions/dashboard-client/package-lock.json +1771 -0
  52. package/extensions/dashboard-client/package.json +27 -0
  53. package/extensions/dashboard-client/src/App.tsx +82 -0
  54. package/extensions/dashboard-client/src/api/client.ts +145 -0
  55. package/extensions/dashboard-client/src/components/CacheStatusPerModel.tsx +44 -0
  56. package/extensions/dashboard-client/src/components/CompressionCard.tsx +61 -0
  57. package/extensions/dashboard-client/src/components/ContextGauge.tsx +59 -0
  58. package/extensions/dashboard-client/src/components/DataSafetyCard.tsx +49 -0
  59. package/extensions/dashboard-client/src/components/ErrorBoundary.tsx +53 -0
  60. package/extensions/dashboard-client/src/components/EventCategoryFilter.tsx +16 -0
  61. package/extensions/dashboard-client/src/components/EventStream.tsx +152 -0
  62. package/extensions/dashboard-client/src/components/LoadingSpinner.tsx +7 -0
  63. package/extensions/dashboard-client/src/components/MemoryStatusCard.tsx +24 -0
  64. package/extensions/dashboard-client/src/components/ModelBadge.tsx +41 -0
  65. package/extensions/dashboard-client/src/components/PerfChart.tsx +160 -0
  66. package/extensions/dashboard-client/src/components/RepoDetailModal.tsx +126 -0
  67. package/extensions/dashboard-client/src/components/RepoTable.tsx +150 -0
  68. package/extensions/dashboard-client/src/components/SessionInfo.tsx +65 -0
  69. package/extensions/dashboard-client/src/components/SummaryTiles.tsx +52 -0
  70. package/extensions/dashboard-client/src/components/TabBar.tsx +34 -0
  71. package/extensions/dashboard-client/src/components/TriggerStatus.tsx +62 -0
  72. package/extensions/dashboard-client/src/hooks/useApi.ts +77 -0
  73. package/extensions/dashboard-client/src/hooks/useSSE.ts +87 -0
  74. package/extensions/dashboard-client/src/index.html +12 -0
  75. package/extensions/dashboard-client/src/main.tsx +23 -0
  76. package/extensions/dashboard-client/src/styles/base.css +121 -0
  77. package/extensions/dashboard-client/src/styles/overview-events.css +318 -0
  78. package/extensions/dashboard-client/src/styles/repos-metrics.css +307 -0
  79. package/extensions/dashboard-client/src/tabs/ConfigTab.tsx +16 -0
  80. package/extensions/dashboard-client/src/tabs/EventsTab.tsx +37 -0
  81. package/extensions/dashboard-client/src/tabs/MetricsTab.tsx +49 -0
  82. package/extensions/dashboard-client/src/tabs/OverviewTab.tsx +80 -0
  83. package/extensions/dashboard-client/src/tabs/ReposTab.tsx +59 -0
  84. package/extensions/dashboard-client/tsconfig.json +28 -0
  85. package/extensions/dashboard-client/vite.config.ts +38 -0
  86. package/extensions/dashboard-server/api-contracts/core.ts +302 -0
  87. package/extensions/dashboard-server/api-contracts/endpoints.ts +476 -0
  88. package/extensions/dashboard-server/api-contracts/game.ts +145 -0
  89. package/extensions/dashboard-server/api-contracts/index.ts +159 -0
  90. package/extensions/dashboard-server/api-contracts/infrastructure.ts +465 -0
  91. package/extensions/dashboard-server/api-contracts/multi-repo.ts +328 -0
  92. package/extensions/dashboard-server/api-contracts/snapshot.ts +386 -0
  93. package/extensions/dashboard-server/api-contracts.test.ts +1050 -0
  94. package/extensions/dashboard-server/api-contracts.ts +96 -0
  95. package/extensions/dashboard-server/auth.ts +20 -0
  96. package/extensions/dashboard-server/html.ts +46 -1
  97. package/extensions/dashboard-server/perf-server.test.ts +101 -0
  98. package/extensions/dashboard-server/server.ts +862 -503
  99. package/extensions/dashboard-server/tailscale.ts +7 -0
  100. package/extensions/dashboard-server/types.ts +23 -114
  101. package/extensions/mega-dashboard.ts +17 -0
  102. package/extensions/mega-events/perf-handler.ts +113 -0
  103. package/extensions/mega-events/register.ts +2 -0
  104. package/extensions/mega-events.ts +1 -0
  105. package/extensions/mega-runtime/state.ts +57 -0
  106. package/package.json +2 -1
  107. package/src/store/sqlite/perf-samples.test.ts +65 -0
  108. package/src/store/sqlite/perf-samples.ts +125 -0
  109. package/src/store/sqlite/schema.ts +14 -0
  110. package/src/store/sqlite.ts +1 -0
@@ -0,0 +1,7 @@
1
+ /**
2
+ * dashboard-client/src/components/LoadingSpinner.tsx — loading state.
3
+ */
4
+
5
+ export function LoadingSpinner(): React.ReactElement {
6
+ return <div className="loading-spinner">Loading…</div>;
7
+ }
@@ -0,0 +1,24 @@
1
+ import type React from "react";
2
+ export interface MemoryStatusProps {
3
+ retained: number;
4
+ compressedKiB: number;
5
+ dedupPct: number;
6
+ deleted: number;
7
+ }
8
+ export function MemoryStatusCard({
9
+ retained,
10
+ compressedKiB,
11
+ dedupPct,
12
+ deleted,
13
+ }: MemoryStatusProps): React.ReactElement {
14
+ return (
15
+ <div className="memory-card">
16
+ <h4>Memory Status</h4>
17
+ <div>
18
+ Retained regions: {retained} · Compressed-Original:{" "}
19
+ {compressedKiB.toFixed(1)} KiB · Dedup: {dedupPct}% · Permanently
20
+ deleted: {deleted}
21
+ </div>
22
+ </div>
23
+ );
24
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * dashboard-client/src/components/ModelBadge.tsx — model + provider + rates.
3
+ *
4
+ * Shows the active model name, human-readable provider, and input/output
5
+ * token rates. Used in the Metrics tab header.
6
+ */
7
+
8
+ import type React from "react";
9
+
10
+ export interface ModelBadgeProps {
11
+ /** Model name/identifier. */
12
+ name: string;
13
+ /** Human-readable provider name. */
14
+ providerName: string;
15
+ /** Machine-readable provider identifier. */
16
+ provider: string;
17
+ /** Model input processing rate (tokens per second). */
18
+ inputRate: number;
19
+ /** Model output processing rate (tokens per second). */
20
+ outputRate: number;
21
+ }
22
+
23
+ export function ModelBadge({
24
+ name,
25
+ providerName,
26
+ provider,
27
+ inputRate,
28
+ outputRate,
29
+ }: ModelBadgeProps): React.ReactElement {
30
+ return (
31
+ <div className="model-badge">
32
+ <span className="model-name">{name}</span>
33
+ <span className="model-provider">{providerName}</span>
34
+ <span className="model-provider-id">{provider}</span>
35
+ <div className="model-rates">
36
+ <span className="rate">in {inputRate} tok/s</span>
37
+ <span className="rate">out {outputRate} tok/s</span>
38
+ </div>
39
+ </div>
40
+ );
41
+ }
@@ -0,0 +1,160 @@
1
+ /**
2
+ * dashboard-client/src/components/PerfChart.tsx — perf stat chart.
3
+ *
4
+ * The /api/perf endpoint returns rolling-window aggregates (not time-series),
5
+ * so this renders a stat-card "chart": p50/p95 latency bars, TPS, cache hit %,
6
+ * db recompute + disk write. Each metric shows a normalized bar + numeric.
7
+ */
8
+
9
+ import type React from "react";
10
+ import type { PerfResponse } from "@contracts";
11
+
12
+ export interface PerfChartProps {
13
+ perf: PerfResponse;
14
+ }
15
+
16
+ function fmtMs(n: number): string {
17
+ return n >= 1000 ? `${(n / 1000).toFixed(1)}s` : `${n.toFixed(0)}ms`;
18
+ }
19
+
20
+ interface BarProps {
21
+ /** Current value. */
22
+ value: number;
23
+ /** Value that represents 100% bar fill (the "max" reference). */
24
+ max: number;
25
+ /** Color class. */
26
+ colorClass: string;
27
+ }
28
+
29
+ function Bar({ value, max, colorClass }: BarProps): React.ReactElement {
30
+ const pct = max > 0 ? Math.max(0, Math.min(100, (value / max) * 100)) : 0;
31
+ return (
32
+ <div className="perf-bar-track">
33
+ <div
34
+ className={`perf-bar-fill ${colorClass}`}
35
+ style={{ width: `${pct}%` }}
36
+ />
37
+ </div>
38
+ );
39
+ }
40
+
41
+ export function PerfChart({ perf }: PerfChartProps): React.ReactElement {
42
+ // Reference maxes: p95 is the upper bound for latency bars; rates scale to a
43
+ // reasonable reference (tok/s) so bars are visually meaningful even with
44
+ // small samples. cache_hit_pct is already 0-100.
45
+ const latencyMax = Math.max(
46
+ perf.turn_latency_ms.p95,
47
+ perf.provider_latency_ms.p95,
48
+ 1,
49
+ );
50
+ const tpsRef = Math.max(perf.tps.avg, 1);
51
+
52
+ return (
53
+ <div className="perf-chart">
54
+ <div className="perf-header">
55
+ <h3>Performance</h3>
56
+ <span className="perf-window">
57
+ {perf.windowMinutes}min window · {perf.sampleCount} samples
58
+ </span>
59
+ </div>
60
+ <div className="perf-grid">
61
+ <div className="perf-metric">
62
+ <span className="perf-label">Turn latency p50 / p95</span>
63
+ <span className="perf-value">
64
+ {fmtMs(perf.turn_latency_ms.p50)} /{" "}
65
+ {fmtMs(perf.turn_latency_ms.p95)}
66
+ </span>
67
+ <Bar
68
+ value={perf.turn_latency_ms.p95}
69
+ max={latencyMax}
70
+ colorClass="perf-blue"
71
+ />
72
+ </div>
73
+ <div className="perf-metric">
74
+ <span className="perf-label">Provider latency p50 / p95</span>
75
+ <span className="perf-value">
76
+ {fmtMs(perf.provider_latency_ms.p50)} /{" "}
77
+ {fmtMs(perf.provider_latency_ms.p95)}
78
+ </span>
79
+ <Bar
80
+ value={perf.provider_latency_ms.p95}
81
+ max={latencyMax}
82
+ colorClass="perf-purple"
83
+ />
84
+ </div>
85
+ <div className="perf-metric">
86
+ <span className="perf-label">Tokens/sec (avg)</span>
87
+ <span className="perf-value">{perf.tps.avg.toFixed(1)}</span>
88
+ <Bar value={perf.tps.avg} max={tpsRef} colorClass="perf-green" />
89
+ </div>
90
+ <div className="perf-metric">
91
+ <span className="perf-label">Cache hit (avg / latest)</span>
92
+ <span className="perf-value">
93
+ {perf.cache_hit_pct.avg.toFixed(1)}% /{" "}
94
+ {perf.cache_hit_pct.latest.toFixed(1)}%
95
+ </span>
96
+ <Bar
97
+ value={perf.cache_hit_pct.avg}
98
+ max={100}
99
+ colorClass="perf-green"
100
+ />
101
+ </div>
102
+ <div className="perf-metric">
103
+ <span className="perf-label">DB recompute p95</span>
104
+ <span className="perf-value">{fmtMs(perf.db_recompute_ms.p95)}</span>
105
+ <Bar
106
+ value={perf.db_recompute_ms.p95}
107
+ max={latencyMax}
108
+ colorClass="perf-yellow"
109
+ />
110
+ </div>
111
+ <div className="perf-metric">
112
+ <span className="perf-label">Disk write p95</span>
113
+ <span className="perf-value">{fmtMs(perf.disk_write_ms.p95)}</span>
114
+ <Bar
115
+ value={perf.disk_write_ms.p95}
116
+ max={latencyMax}
117
+ colorClass="perf-yellow"
118
+ />
119
+ </div>
120
+ </div>
121
+ <div className="perf-resources">
122
+ <div className="perf-metric">
123
+ <span className="perf-label">RSS</span>
124
+ <span className="perf-value">{perf.rss_mb.latest.toFixed(0)} MB</span>
125
+ </div>
126
+ <div className="perf-metric">
127
+ <span className="perf-label">Heap</span>
128
+ <span className="perf-value">
129
+ {perf.heap_mb.latest.toFixed(0)} MB
130
+ </span>
131
+ </div>
132
+ <div className="perf-metric">
133
+ <span className="perf-label">CPU user</span>
134
+ <span className="perf-value">
135
+ {(perf.cpu_user_ms.latest / 1000).toFixed(1)}s
136
+ </span>
137
+ </div>
138
+ <div className="perf-metric">
139
+ <span className="perf-label">CPU sys</span>
140
+ <span className="perf-value">
141
+ {(perf.cpu_sys_ms.latest / 1000).toFixed(1)}s
142
+ </span>
143
+ </div>
144
+ </div>
145
+ {perf.diag && (
146
+ <div className="perf-diag">
147
+ <span className="perf-label">
148
+ Fast-gate fires: {perf.diag.ctxFastGate}
149
+ </span>
150
+ <span className="perf-label">
151
+ Live trim fires: {perf.diag.liveTrimFires}
152
+ </span>
153
+ <span className="perf-label">
154
+ Live trim replays: {perf.diag.liveTrimReplays}
155
+ </span>
156
+ </div>
157
+ )}
158
+ </div>
159
+ );
160
+ }
@@ -0,0 +1,126 @@
1
+ /**
2
+ * dashboard-client/src/components/RepoDetailModal.tsx — repo drill-down.
3
+ *
4
+ * Full detail from an /api/index row: token breakdown (kept/dropped), model
5
+ * info, context window, dedup rate, reasoning flag. Click backdrop to close.
6
+ */
7
+
8
+ import type React from "react";
9
+ import { useEffect } from "react";
10
+ import type { IndexesIndexRow } from "@contracts";
11
+
12
+ export interface RepoDetailModalProps {
13
+ repo: IndexesIndexRow;
14
+ onClose: () => void;
15
+ }
16
+
17
+ function fmt(n: number): string {
18
+ if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
19
+ if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
20
+ return String(n);
21
+ }
22
+
23
+ function formatTs(ts: number | null): string {
24
+ if (ts === null || ts === undefined) return "never";
25
+ try {
26
+ return new Date(ts).toLocaleString();
27
+ } catch {
28
+ return String(ts);
29
+ }
30
+ }
31
+
32
+ export function RepoDetailModal({
33
+ repo,
34
+ onClose,
35
+ }: RepoDetailModalProps): React.ReactElement {
36
+ // Close on Escape key.
37
+ useEffect(() => {
38
+ const handler = (e: KeyboardEvent): void => {
39
+ if (e.key === "Escape") onClose();
40
+ };
41
+ window.addEventListener("keydown", handler);
42
+ return () => window.removeEventListener("keydown", handler);
43
+ }, [onClose]);
44
+
45
+ return (
46
+ <div
47
+ className="modal-backdrop"
48
+ onClick={onClose}
49
+ role="dialog"
50
+ aria-modal="true"
51
+ >
52
+ <div className="modal-card" onClick={(e) => e.stopPropagation()}>
53
+ <header className="modal-header">
54
+ <h2>{repo.displayName}</h2>
55
+ <button
56
+ type="button"
57
+ className="modal-close"
58
+ onClick={onClose}
59
+ aria-label="Close"
60
+ >
61
+ ×
62
+ </button>
63
+ </header>
64
+ <p className="modal-path">{repo.repoRoot}</p>
65
+ <div className="modal-grid">
66
+ <div className="modal-stat">
67
+ <span className="modal-label">Checkpoints</span>
68
+ <span className="modal-value">
69
+ {repo.checkpointCount.toLocaleString()}
70
+ </span>
71
+ </div>
72
+ <div className="modal-stat">
73
+ <span className="modal-label">Sessions</span>
74
+ <span className="modal-value">
75
+ {repo.sessions.toLocaleString()}
76
+ </span>
77
+ </div>
78
+ <div className="modal-stat modal-highlight">
79
+ <span className="modal-label">Tokens saved</span>
80
+ <span className="modal-value">{fmt(repo.tokensSaved)}</span>
81
+ </div>
82
+ <div className="modal-stat">
83
+ <span className="modal-label">Tokens kept</span>
84
+ <span className="modal-value">{fmt(repo.tokensKept)}</span>
85
+ </div>
86
+ <div className="modal-stat">
87
+ <span className="modal-label">Tokens dropped</span>
88
+ <span className="modal-value">{fmt(repo.tokensDropped)}</span>
89
+ </div>
90
+ <div className="modal-stat">
91
+ <span className="modal-label">Dedup rate</span>
92
+ <span className="modal-value">—</span>
93
+ </div>
94
+ </div>
95
+ <h3>Model</h3>
96
+ <dl className="modal-dl">
97
+ <dt>Provider</dt>
98
+ <dd>{repo.providerName ?? "—"}</dd>
99
+ <dt>Model</dt>
100
+ <dd>{repo.modelName ?? "—"}</dd>
101
+ <dt>Input rate</dt>
102
+ <dd>{repo.inputRate !== null ? `${repo.inputRate} tok/s` : "—"}</dd>
103
+ <dt>Output rate</dt>
104
+ <dd>{repo.outputRate !== null ? `${repo.outputRate} tok/s` : "—"}</dd>
105
+ <dt>Context window</dt>
106
+ <dd>{repo.contextWindow !== null ? fmt(repo.contextWindow) : "—"}</dd>
107
+ <dt>Max tokens</dt>
108
+ <dd>{repo.maxTokens !== null ? fmt(repo.maxTokens) : "—"}</dd>
109
+ <dt>Reasoning</dt>
110
+ <dd>
111
+ {repo.reasoning === null ? "—" : repo.reasoning ? "on" : "off"}
112
+ </dd>
113
+ </dl>
114
+ <h3>Activity</h3>
115
+ <dl className="modal-dl">
116
+ <dt>Last compacted</dt>
117
+ <dd>{formatTs(repo.lastCompactedAt)}</dd>
118
+ <dt>Last seen</dt>
119
+ <dd>{formatTs(repo.lastSeen)}</dd>
120
+ <dt>Compressed bytes</dt>
121
+ <dd>{fmt(repo.compressedOriginalBytes)}</dd>
122
+ </dl>
123
+ </div>
124
+ </div>
125
+ );
126
+ }
@@ -0,0 +1,150 @@
1
+ /**
2
+ * dashboard-client/src/components/RepoTable.tsx — sortable, searchable repo table.
3
+ *
4
+ * Columns: name, model, checkpoints, tokens saved, last compacted, sessions.
5
+ * Client-side sort (click header to toggle asc/desc). Search by displayName
6
+ * (case-insensitive substring). Default sort: lastSeen DESC.
7
+ */
8
+
9
+ import type React from "react";
10
+ import { useState, useMemo } from "react";
11
+ import type { IndexesIndexRow } from "@contracts";
12
+
13
+ export interface RepoTableProps {
14
+ repos: IndexesIndexRow[];
15
+ /** Called when a row is clicked — opens the detail modal. */
16
+ onSelect: (repo: IndexesIndexRow) => void;
17
+ }
18
+
19
+ type SortKey =
20
+ | "displayName"
21
+ | "modelName"
22
+ | "checkpointCount"
23
+ | "tokensSaved"
24
+ | "lastCompactedAt"
25
+ | "sessions";
26
+
27
+ type SortDir = "asc" | "desc";
28
+
29
+ const COLUMNS: Array<{ key: SortKey; label: string }> = [
30
+ { key: "displayName", label: "Name" },
31
+ { key: "modelName", label: "Model" },
32
+ { key: "checkpointCount", label: "Checkpoints" },
33
+ { key: "tokensSaved", label: "Saved" },
34
+ { key: "lastCompactedAt", label: "Last compacted" },
35
+ { key: "sessions", label: "Sessions" },
36
+ ];
37
+
38
+ function fmt(n: number): string {
39
+ if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
40
+ if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
41
+ return String(n);
42
+ }
43
+
44
+ function formatTs(ts: number | null): string {
45
+ if (ts === null || ts === undefined) return "never";
46
+ try {
47
+ return new Date(ts).toLocaleDateString();
48
+ } catch {
49
+ return String(ts);
50
+ }
51
+ }
52
+
53
+ export function RepoTable({
54
+ repos,
55
+ onSelect,
56
+ }: RepoTableProps): React.ReactElement {
57
+ const [search, setSearch] = useState("");
58
+ const [sortKey, setSortKey] = useState<SortKey>("lastCompactedAt");
59
+ const [sortDir, setSortDir] = useState<SortDir>("desc");
60
+
61
+ const filtered = useMemo(() => {
62
+ const q = search.trim().toLowerCase();
63
+ const list = q
64
+ ? repos.filter((r) => r.displayName.toLowerCase().includes(q))
65
+ : repos.slice();
66
+ list.sort((a, b) => {
67
+ const av = a[sortKey];
68
+ const bv = b[sortKey];
69
+ // nulls last on desc, first on asc (consistent UX)
70
+ const aNull = av === null || av === undefined;
71
+ const bNull = bv === null || bv === undefined;
72
+ if (aNull && bNull) return 0;
73
+ if (aNull) return sortDir === "desc" ? 1 : -1;
74
+ if (bNull) return sortDir === "desc" ? -1 : 1;
75
+ const cmp =
76
+ typeof av === "number" && typeof bv === "number"
77
+ ? av - bv
78
+ : String(av).localeCompare(String(bv));
79
+ return sortDir === "desc" ? -cmp : cmp;
80
+ });
81
+ return list;
82
+ }, [repos, search, sortKey, sortDir]);
83
+
84
+ const handleSort = (key: SortKey): void => {
85
+ if (sortKey === key) {
86
+ setSortDir((d) => (d === "asc" ? "desc" : "asc"));
87
+ } else {
88
+ setSortKey(key);
89
+ setSortDir(
90
+ key === "lastCompactedAt" || key === "tokensSaved" ? "desc" : "asc",
91
+ );
92
+ }
93
+ };
94
+
95
+ return (
96
+ <div className="repo-table-wrap">
97
+ <input
98
+ type="search"
99
+ className="repo-search"
100
+ placeholder="Search repos…"
101
+ value={search}
102
+ onChange={(e) => setSearch(e.target.value)}
103
+ aria-label="Search repos"
104
+ />
105
+ <table className="repo-table">
106
+ <thead>
107
+ <tr>
108
+ {COLUMNS.map((col) => (
109
+ <th key={col.key}>
110
+ <button
111
+ type="button"
112
+ className={`sort-header ${sortKey === col.key ? `active-${sortDir}` : ""}`}
113
+ onClick={() => handleSort(col.key)}
114
+ >
115
+ {col.label}
116
+ {sortKey === col.key && (
117
+ <span className="sort-arrow">
118
+ {sortDir === "asc" ? " ▲" : " ▼"}
119
+ </span>
120
+ )}
121
+ </button>
122
+ </th>
123
+ ))}
124
+ </tr>
125
+ </thead>
126
+ <tbody>
127
+ {filtered.length === 0 && (
128
+ <tr>
129
+ <td colSpan={COLUMNS.length} className="repo-empty">
130
+ {repos.length === 0
131
+ ? "No repos yet."
132
+ : "No repos match your search."}
133
+ </td>
134
+ </tr>
135
+ )}
136
+ {filtered.map((repo) => (
137
+ <tr key={repo.stateDir} onClick={() => onSelect(repo)}>
138
+ <td className="repo-name">{repo.displayName}</td>
139
+ <td className="repo-model">{repo.modelName ?? "—"}</td>
140
+ <td>{repo.checkpointCount.toLocaleString()}</td>
141
+ <td>{fmt(repo.tokensSaved)}</td>
142
+ <td>{formatTs(repo.lastCompactedAt)}</td>
143
+ <td>{repo.sessions.toLocaleString()}</td>
144
+ </tr>
145
+ ))}
146
+ </tbody>
147
+ </table>
148
+ </div>
149
+ );
150
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * dashboard-client/src/components/SessionInfo.tsx — session state + store metrics.
3
+ *
4
+ * State badge, checkpoint count, tokens saved, dedup hit rate.
5
+ */
6
+
7
+ import type React from "react";
8
+
9
+ export interface SessionInfoProps {
10
+ /** Session lifecycle state, or null when no session is active. */
11
+ state: string | null;
12
+ /** Whether a checkpoint has been persisted during this session. */
13
+ persistedThisSession: boolean;
14
+ /** Total checkpoints across all sessions. */
15
+ checkpointCount: number;
16
+ /** Total tokens saved through compaction. */
17
+ tokensSaved: number;
18
+ /** Dedup hit rate (percent, 0–100). */
19
+ dedupHitRate: number;
20
+ }
21
+
22
+ function fmt(n: number): string {
23
+ if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
24
+ if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
25
+ return String(n);
26
+ }
27
+
28
+ export function SessionInfo({
29
+ state,
30
+ persistedThisSession,
31
+ checkpointCount,
32
+ tokensSaved,
33
+ dedupHitRate,
34
+ }: SessionInfoProps): React.ReactElement {
35
+ const badge = state ?? "no session";
36
+ return (
37
+ <div className="card session-info">
38
+ <h3>Session</h3>
39
+ <p>
40
+ <span
41
+ className={`state-badge state-${(state ?? "none").toLowerCase()}`}
42
+ >
43
+ {badge}
44
+ </span>
45
+ {persistedThisSession && (
46
+ <span className="persisted-tag">persisted</span>
47
+ )}
48
+ </p>
49
+ <div className="stat-grid">
50
+ <div className="stat">
51
+ <span className="stat-label">Checkpoints</span>
52
+ <span className="stat-value">{checkpointCount.toLocaleString()}</span>
53
+ </div>
54
+ <div className="stat stat-highlight">
55
+ <span className="stat-label">Saved</span>
56
+ <span className="stat-value">{fmt(tokensSaved)}</span>
57
+ </div>
58
+ <div className="stat">
59
+ <span className="stat-label">Dedup hit</span>
60
+ <span className="stat-value">{dedupHitRate.toFixed(1)}%</span>
61
+ </div>
62
+ </div>
63
+ </div>
64
+ );
65
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * dashboard-client/src/components/SummaryTiles.tsx — aggregate header tiles.
3
+ *
4
+ * Total repos, active repos (24h), total checkpoints, total tokens saved.
5
+ */
6
+
7
+ import type React from "react";
8
+
9
+ export interface SummaryTilesProps {
10
+ /** Total repos in the registry. */
11
+ totalRepos: number;
12
+ /** Repos active within the last 24 hours. */
13
+ activeRepos: number;
14
+ /** Total checkpoints across all repos. */
15
+ totalCheckpoints: number;
16
+ /** Total tokens saved across all repos. */
17
+ totalTokensSaved: number;
18
+ }
19
+
20
+ function fmt(n: number): string {
21
+ if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
22
+ if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
23
+ return String(n);
24
+ }
25
+
26
+ export function SummaryTiles({
27
+ totalRepos,
28
+ activeRepos,
29
+ totalCheckpoints,
30
+ totalTokensSaved,
31
+ }: SummaryTilesProps): React.ReactElement {
32
+ return (
33
+ <div className="summary-tiles">
34
+ <div className="tile">
35
+ <span className="tile-label">Total repos</span>
36
+ <span className="tile-value">{totalRepos.toLocaleString()}</span>
37
+ </div>
38
+ <div className="tile">
39
+ <span className="tile-label">Active (24h)</span>
40
+ <span className="tile-value">{activeRepos.toLocaleString()}</span>
41
+ </div>
42
+ <div className="tile">
43
+ <span className="tile-label">Checkpoints</span>
44
+ <span className="tile-value">{totalCheckpoints.toLocaleString()}</span>
45
+ </div>
46
+ <div className="tile tile-highlight">
47
+ <span className="tile-label">Tokens saved</span>
48
+ <span className="tile-value">{fmt(totalTokensSaved)}</span>
49
+ </div>
50
+ </div>
51
+ );
52
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * dashboard-client/src/components/TabBar.tsx — tab navigation.
3
+ */
4
+
5
+ import type { TabId } from "../App";
6
+
7
+ export interface TabBarProps {
8
+ tabs: Array<{ id: TabId; label: string }>;
9
+ active: TabId;
10
+ onTabChange: (id: TabId) => void;
11
+ }
12
+
13
+ export function TabBar({
14
+ tabs,
15
+ active,
16
+ onTabChange,
17
+ }: TabBarProps): React.ReactElement {
18
+ return (
19
+ <nav className="tab-bar" role="tablist">
20
+ {tabs.map((tab) => (
21
+ <button
22
+ key={tab.id}
23
+ type="button"
24
+ role="tab"
25
+ aria-selected={active === tab.id}
26
+ className={active === tab.id ? "active" : ""}
27
+ onClick={() => onTabChange(tab.id)}
28
+ >
29
+ {tab.label}
30
+ </button>
31
+ ))}
32
+ </nav>
33
+ );
34
+ }