pi-mega-compact 0.8.12 → 0.8.14

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 (76) hide show
  1. package/extensions/dashboard-client/dist/assets/AchievementTiles-BH26e95w.js +2 -0
  2. package/extensions/dashboard-client/dist/assets/AchievementTiles-BH26e95w.js.map +1 -0
  3. package/extensions/dashboard-client/dist/assets/AchievementsTab-B94wvRV_.js +2 -0
  4. package/extensions/dashboard-client/dist/assets/AchievementsTab-B94wvRV_.js.map +1 -0
  5. package/extensions/dashboard-client/dist/assets/CacheTab-DPmw3NHR.js +2 -0
  6. package/extensions/dashboard-client/dist/assets/CacheTab-DPmw3NHR.js.map +1 -0
  7. package/extensions/dashboard-client/dist/assets/ConfigTab-C3g3smRL.js +2 -0
  8. package/extensions/dashboard-client/dist/assets/ConfigTab-C3g3smRL.js.map +1 -0
  9. package/extensions/dashboard-client/dist/assets/EventsTab-B5PicfFw.js +2 -0
  10. package/extensions/dashboard-client/dist/assets/EventsTab-B5PicfFw.js.map +1 -0
  11. package/extensions/dashboard-client/dist/assets/GameTab-CsVwp78a.js +2 -0
  12. package/extensions/dashboard-client/dist/assets/GameTab-CsVwp78a.js.map +1 -0
  13. package/extensions/dashboard-client/dist/assets/MetricsTab-Cf72UewX.js +2 -0
  14. package/extensions/dashboard-client/dist/assets/MetricsTab-Cf72UewX.js.map +1 -0
  15. package/extensions/dashboard-client/dist/assets/OverviewTab-Dh6aG3gy.js +2 -0
  16. package/extensions/dashboard-client/dist/assets/OverviewTab-Dh6aG3gy.js.map +1 -0
  17. package/extensions/dashboard-client/dist/assets/ReposTab-HXf2dorl.js +2 -0
  18. package/extensions/dashboard-client/dist/assets/ReposTab-HXf2dorl.js.map +1 -0
  19. package/extensions/dashboard-client/dist/assets/TimeSavedCard-kUnoospE.js +2 -0
  20. package/extensions/dashboard-client/dist/assets/TimeSavedCard-kUnoospE.js.map +1 -0
  21. package/extensions/dashboard-client/dist/assets/index-Cfxniu2R.css +1 -0
  22. package/extensions/dashboard-client/dist/assets/{index-XAIN8km6.js → index-DMntLu-8.js} +11 -10
  23. package/extensions/dashboard-client/dist/assets/index-DMntLu-8.js.map +1 -0
  24. package/extensions/dashboard-client/dist/index.html +2 -2
  25. package/extensions/dashboard-client/src/App.tsx +18 -4
  26. package/extensions/dashboard-client/src/components/AchievementTiles.tsx +105 -0
  27. package/extensions/dashboard-client/src/components/ActiveReposTable.tsx +116 -0
  28. package/extensions/dashboard-client/src/components/CacheHitsCard.tsx +73 -0
  29. package/extensions/dashboard-client/src/components/CacheStatusPerModel.tsx +78 -35
  30. package/extensions/dashboard-client/src/components/ConfigSummaryCard.tsx +101 -0
  31. package/extensions/dashboard-client/src/components/ContextGauge.tsx +11 -15
  32. package/extensions/dashboard-client/src/components/DataSafetyCard.tsx +42 -33
  33. package/extensions/dashboard-client/src/components/EventCategoryFilter.tsx +46 -9
  34. package/extensions/dashboard-client/src/components/LegendCard.tsx +49 -0
  35. package/extensions/dashboard-client/src/components/ModelBadge.tsx +42 -11
  36. package/extensions/dashboard-client/src/components/PerfCards.tsx +91 -0
  37. package/extensions/dashboard-client/src/components/RepoAllSessionsCard.tsx +102 -0
  38. package/extensions/dashboard-client/src/components/RepoDetailModal.tsx +9 -2
  39. package/extensions/dashboard-client/src/components/RepoTable.tsx +30 -22
  40. package/extensions/dashboard-client/src/components/SavingsByModelTable.tsx +226 -0
  41. package/extensions/dashboard-client/src/components/SessionInfo.tsx +25 -50
  42. package/extensions/dashboard-client/src/components/SummaryTiles.tsx +20 -11
  43. package/extensions/dashboard-client/src/components/TimeSavedCard.tsx +61 -0
  44. package/extensions/dashboard-client/src/components/TriggerStatus.tsx +31 -19
  45. package/extensions/dashboard-client/src/components/VectorStoreCard.tsx +134 -0
  46. package/extensions/dashboard-client/src/main.tsx +5 -0
  47. package/extensions/dashboard-client/src/styles/base.css +76 -9
  48. package/extensions/dashboard-client/src/styles/config.css +149 -0
  49. package/extensions/dashboard-client/src/styles/game-achievements.css +265 -0
  50. package/extensions/dashboard-client/src/styles/metrics-extra.css +69 -0
  51. package/extensions/dashboard-client/src/styles/overview-extra.css +170 -0
  52. package/extensions/dashboard-client/src/styles/repos-extra.css +99 -0
  53. package/extensions/dashboard-client/src/styles/repos-metrics.css +14 -23
  54. package/extensions/dashboard-client/src/tabs/AchievementsTab.tsx +55 -0
  55. package/extensions/dashboard-client/src/tabs/CacheTab.tsx +50 -0
  56. package/extensions/dashboard-client/src/tabs/ConfigTab.tsx +240 -5
  57. package/extensions/dashboard-client/src/tabs/EventsTab.tsx +55 -3
  58. package/extensions/dashboard-client/src/tabs/GameTab.tsx +287 -0
  59. package/extensions/dashboard-client/src/tabs/MetricsTab.tsx +7 -1
  60. package/extensions/dashboard-client/src/tabs/OverviewTab.tsx +103 -17
  61. package/extensions/dashboard-client/src/tabs/ReposTab.tsx +57 -22
  62. package/extensions/dashboard-client/src/utils/format.ts +85 -0
  63. package/extensions/dashboard-client/src/utils/types.ts +27 -0
  64. package/package.json +1 -1
  65. package/extensions/dashboard-client/dist/assets/ConfigTab-RrjXWfvr.js +0 -2
  66. package/extensions/dashboard-client/dist/assets/ConfigTab-RrjXWfvr.js.map +0 -1
  67. package/extensions/dashboard-client/dist/assets/EventsTab-BH_FxJ08.js +0 -2
  68. package/extensions/dashboard-client/dist/assets/EventsTab-BH_FxJ08.js.map +0 -1
  69. package/extensions/dashboard-client/dist/assets/MetricsTab-C-PoFV4p.js +0 -2
  70. package/extensions/dashboard-client/dist/assets/MetricsTab-C-PoFV4p.js.map +0 -1
  71. package/extensions/dashboard-client/dist/assets/OverviewTab-Db-qBqFN.js +0 -2
  72. package/extensions/dashboard-client/dist/assets/OverviewTab-Db-qBqFN.js.map +0 -1
  73. package/extensions/dashboard-client/dist/assets/ReposTab-DhCjJvuj.js +0 -2
  74. package/extensions/dashboard-client/dist/assets/ReposTab-DhCjJvuj.js.map +0 -1
  75. package/extensions/dashboard-client/dist/assets/index-Buer5yxB.css +0 -1
  76. package/extensions/dashboard-client/dist/assets/index-XAIN8km6.js.map +0 -1
@@ -1,16 +1,67 @@
1
1
  /**
2
2
  * dashboard-client/src/tabs/EventsTab.tsx — Events tab (C1).
3
3
  *
4
- * Live SSE event stream via useSSE + EventStream with type filters.
5
- * Shows connection status + event count.
4
+ * Live SSE event stream via useSSE + EventStream with category filter chips.
5
+ * Shows connection status + event count. Category filter uses the existing
6
+ * EventCategoryFilter component to filter by all/compact/recall/config/crew/game.
6
7
  */
7
8
 
8
9
  import type React from "react";
10
+ import { useState, useMemo } from "react";
11
+ import type { SseEvent } from "@contracts";
9
12
  import { useSSE } from "../hooks/useSSE";
10
13
  import { EventStream } from "../components/EventStream";
14
+ import {
15
+ EventCategoryFilter,
16
+ type EventCategory,
17
+ } from "../components/EventCategoryFilter";
18
+
19
+ const COMPACT_TYPES = new Set([
20
+ "compact_start",
21
+ "compact_end",
22
+ "compact_trigger",
23
+ "compact_skip",
24
+ ]);
25
+ const CONFIG_TYPES = new Set([
26
+ "config_updated",
27
+ "config_preset",
28
+ "tier_changed",
29
+ "model_changed",
30
+ "pressure_lifted",
31
+ "anchors_updated",
32
+ "checkpoint_persisted",
33
+ ]);
34
+ const CREW_TYPES = new Set([
35
+ "crew_presence_changed",
36
+ "crew_turn_changed",
37
+ "crew_bandit_chosen",
38
+ ]);
39
+ const GAME_TYPES = new Set([
40
+ "game_ritual_start",
41
+ "game_ritual_stage",
42
+ "game_ritual_end",
43
+ "game_mode_changed",
44
+ "game_render",
45
+ ]);
46
+
47
+ function matchesCategory(ev: SseEvent, cat: EventCategory): boolean {
48
+ if (cat === "all") return true;
49
+ const t = ev.type;
50
+ if (cat === "compact") return COMPACT_TYPES.has(t);
51
+ if (cat === "recall") return t === "recall_inject";
52
+ if (cat === "config") return CONFIG_TYPES.has(t);
53
+ if (cat === "crew") return CREW_TYPES.has(t);
54
+ return GAME_TYPES.has(t); // cat === "game"
55
+ }
11
56
 
12
57
  export default function EventsTab(): React.ReactElement {
13
58
  const { events, status, eventCount } = useSSE();
59
+ const [category, setCategory] = useState<EventCategory>("all");
60
+
61
+ const filtered = useMemo(
62
+ () => events.filter((e) => matchesCategory(e, category)),
63
+ [events, category],
64
+ );
14
65
 
15
66
  const statusCls = `sse-status sse-${status}`;
16
67
  const statusLabel =
@@ -31,7 +82,8 @@ export default function EventsTab(): React.ReactElement {
31
82
  </span>
32
83
  <span className="event-count">{eventCount} events received</span>
33
84
  </div>
34
- <EventStream events={events} />
85
+ <EventCategoryFilter active={category} onFilter={setCategory} />
86
+ <EventStream events={filtered} />
35
87
  </div>
36
88
  );
37
89
  }
@@ -0,0 +1,287 @@
1
+ /**
2
+ * dashboard-client/src/tabs/GameTab.tsx — Game tab (NEW).
3
+ *
4
+ * Renders:
5
+ * (1) MEGA CACHE banner (animated gradient, shown when any mega_cache > 100)
6
+ * (2) Opie unlock tile (shown when mega_cache > 100)
7
+ * (3) High Scores header
8
+ * (4) 4 leaderboards in 2×2 grid: Cache %, Dedupe, Turns (LVL badge),
9
+ * MEGA CACHE trophies (firstSeen date meta)
10
+ * (5) Repos count badge via fetchGameScores({metric:'repos',limit:1})
11
+ * (6) Achievements sub-section (AchievementTiles shared component)
12
+ * (7) Game empty state
13
+ * Polls scores + achievements every 15s, game-state every 15s.
14
+ * Applies data-theme accent vars when game_mode_on.
15
+ */
16
+
17
+ import type React from "react";
18
+ import { useState, useEffect, useCallback, useRef } from "react";
19
+ import type { GameScoreRow, AchievementRow } from "@contracts";
20
+ import { fetchGameScores, fetchAchievements, fetchGameState } from "../api/client";
21
+ import { AchievementTiles } from "../components/AchievementTiles";
22
+
23
+ /** Actual flat game state shape from the server (NOT the nested contract). */
24
+ interface FlatGameState {
25
+ game_mode_on: boolean;
26
+ theme: string;
27
+ tui_display_mode: "full" | "minimal";
28
+ }
29
+
30
+ /** Metadata object inside a mega_cache GameScoreRow. */
31
+ interface TrophyMeta {
32
+ firstSeenTs?: number;
33
+ firstSeen?: number;
34
+ }
35
+
36
+ /* ── Format helpers (matching html.ts logic) ─────────────────────────── */
37
+
38
+ function fmtPct(v: number): string {
39
+ return (Math.round(v * 10) / 10).toString() + "%";
40
+ }
41
+
42
+ function fmtDate(ts: number | null | undefined): string {
43
+ return ts != null ? new Date(ts).toLocaleString() : "\u2014";
44
+ }
45
+
46
+ function trophyMeta(m: unknown): TrophyMeta {
47
+ return m && typeof m === "object" ? (m as TrophyMeta) : {};
48
+ }
49
+
50
+ function repoBasename(path: string): string {
51
+ return path.split("/").pop() || path;
52
+ }
53
+
54
+ /* ── Leaderboard card ────────────────────────────────────────────────── */
55
+
56
+ interface LeaderboardCardProps {
57
+ title: string;
58
+ rows: GameScoreRow[];
59
+ badge?: React.ReactNode;
60
+ renderValue: (r: GameScoreRow) => React.ReactNode;
61
+ }
62
+
63
+ function LeaderboardCard({
64
+ title,
65
+ rows,
66
+ badge,
67
+ renderValue,
68
+ }: LeaderboardCardProps): React.ReactElement {
69
+ return (
70
+ <div className="lb-card">
71
+ <h3>
72
+ {title}
73
+ {badge}
74
+ </h3>
75
+ {rows.length === 0 ? (
76
+ <div className="lb-empty">—</div>
77
+ ) : (
78
+ <table>
79
+ <tbody>
80
+ {rows.map((r, i) => (
81
+ <tr key={`${r.repo_root}-${i}`}>
82
+ <td title={r.repo_root}>{repoBasename(r.repo_root)}</td>
83
+ <td className="num">{renderValue(r)}</td>
84
+ </tr>
85
+ ))}
86
+ </tbody>
87
+ </table>
88
+ )}
89
+ </div>
90
+ );
91
+ }
92
+
93
+ /* ── Main GameTab component ──────────────────────────────────────────── */
94
+
95
+ export default function GameTab(): React.ReactElement {
96
+ const [scores, setScores] = useState<
97
+ Record<string, GameScoreRow[]>
98
+ >({});
99
+ const [reposCount, setReposCount] = useState(0);
100
+ const [achievements, setAchievements] = useState<AchievementRow[]>([]);
101
+ const [gameModeOn, setGameModeOn] = useState(false);
102
+ const [loading, setLoading] = useState(true);
103
+ const [megaToast, setMegaToast] = useState<string | null>(null);
104
+ const lastMegaTsRef = useRef(0);
105
+
106
+ const fetchAll = useCallback(async (): Promise<void> => {
107
+ try {
108
+ const [cache, dedupe, turns, megaCache, repos, ach] =
109
+ await Promise.all([
110
+ fetchGameScores({ metric: "cache", limit: 25 }),
111
+ fetchGameScores({ metric: "dedupe", limit: 25 }),
112
+ fetchGameScores({ metric: "turns", limit: 25 }),
113
+ fetchGameScores({ metric: "mega_cache", limit: 25 }),
114
+ fetchGameScores({ metric: "repos", limit: 1 }),
115
+ fetchAchievements(),
116
+ ]);
117
+
118
+ /* Mega-cache toast: new row with value > 100 since last poll */
119
+ const newRow = megaCache.find(
120
+ (r) => r.ts > lastMegaTsRef.current && r.value > 100,
121
+ );
122
+ if (lastMegaTsRef.current > 0 && newRow) {
123
+ setMegaToast(
124
+ `oopsie! cache went to ${Math.round(newRow.value)}% — MEGA CACHE \uD83E\uDD67`,
125
+ );
126
+ setTimeout(() => setMegaToast(null), 4000);
127
+ }
128
+ if (megaCache.length > 0) {
129
+ lastMegaTsRef.current = Math.max(
130
+ ...megaCache.map((r) => r.ts),
131
+ );
132
+ }
133
+
134
+ setScores({ cache, dedupe, turns, mega_cache: megaCache });
135
+ setReposCount(repos[0]?.value ?? 0);
136
+ setAchievements(ach);
137
+ } catch {
138
+ /* non-fatal */
139
+ } finally {
140
+ setLoading(false);
141
+ }
142
+ }, []);
143
+
144
+ /* Poll scores + achievements every 15s. */
145
+ useEffect(() => {
146
+ let active = true;
147
+ const doFetch = async (): Promise<void> => {
148
+ if (active) await fetchAll();
149
+ };
150
+ void doFetch();
151
+ const timer = setInterval(() => void doFetch(), 15000);
152
+ return () => {
153
+ active = false;
154
+ clearInterval(timer);
155
+ };
156
+ }, [fetchAll]);
157
+
158
+ /* Poll game state every 15s for styling. */
159
+ useEffect(() => {
160
+ let active = true;
161
+ const doFetch = async (): Promise<void> => {
162
+ try {
163
+ const gs = (await fetchGameState()) as unknown as FlatGameState;
164
+ if (active) {
165
+ setGameModeOn(!!gs.game_mode_on);
166
+ if (gs.theme)
167
+ document.documentElement.dataset.theme = gs.theme;
168
+ }
169
+ } catch {
170
+ /* non-fatal */
171
+ }
172
+ };
173
+ void doFetch();
174
+ const timer = setInterval(() => void doFetch(), 15000);
175
+ return () => {
176
+ active = false;
177
+ clearInterval(timer);
178
+ };
179
+ }, []);
180
+
181
+ /* Compute mega-cache banner data */
182
+ const megaRows = scores.mega_cache ?? [];
183
+ let bestMega: number | null = null;
184
+ let firstSeen: number | null = null;
185
+ for (const r of megaRows) {
186
+ if (bestMega == null || r.value > bestMega) bestMega = r.value;
187
+ const m = trophyMeta(r.meta);
188
+ const fs = m.firstSeenTs ?? m.firstSeen ?? r.ts;
189
+ if (firstSeen == null || fs < firstSeen) firstSeen = fs;
190
+ }
191
+ const showMega = bestMega != null && bestMega > 100;
192
+
193
+ /* Turns level badge */
194
+ const turnsRows = scores.turns ?? [];
195
+ const maxTurns = turnsRows.reduce((mx, r) => Math.max(mx, r.value), 0);
196
+ const lvl = Math.floor(Math.log2(maxTurns + 1)) + 1;
197
+
198
+ /* Game empty state */
199
+ const hasData =
200
+ (scores.cache?.length ?? 0) > 0 ||
201
+ (scores.dedupe?.length ?? 0) > 0 ||
202
+ turnsRows.length > 0 ||
203
+ megaRows.length > 0 ||
204
+ reposCount > 0;
205
+
206
+ return (
207
+ <div className={`game-tab${gameModeOn ? " game-mode-on" : ""}`}>
208
+ {megaToast != null && (
209
+ <div className="mega-cache-toast show">{megaToast}</div>
210
+ )}
211
+
212
+ {showMega && (
213
+ <div className="mega-cache-banner">
214
+ {"\uD83E\uDD67"} MEGA CACHE! peak {fmtPct(bestMega!)} — first
215
+ reached {fmtDate(firstSeen)}
216
+ </div>
217
+ )}
218
+
219
+ {showMega && (
220
+ <div className="achievement-tile unlocked">
221
+ {"\uD83C\uDFC6"} Opie's Wild Ride
222
+ <span className="ach-detail">
223
+ best {fmtPct(bestMega!)} · first {fmtDate(firstSeen)}
224
+ </span>
225
+ </div>
226
+ )}
227
+
228
+ <h2>High Scores</h2>
229
+
230
+ {!hasData && !loading && (
231
+ <div className="game-empty">
232
+ No scores yet — run a session with game mode on.
233
+ </div>
234
+ )}
235
+
236
+ {hasData && (
237
+ <div className="game-leaderboards">
238
+ <LeaderboardCard
239
+ title="Cache %"
240
+ rows={scores.cache ?? []}
241
+ renderValue={(r) => fmtPct(r.value)}
242
+ />
243
+ <LeaderboardCard
244
+ title="Dedupe (collapsed)"
245
+ rows={scores.dedupe ?? []}
246
+ renderValue={(r) => r.value.toLocaleString()}
247
+ />
248
+ <LeaderboardCard
249
+ title="Turns"
250
+ rows={turnsRows}
251
+ badge={
252
+ turnsRows.length > 0 ? (
253
+ <span className="lvl-badge">LVL {lvl}</span>
254
+ ) : undefined
255
+ }
256
+ renderValue={(r) => r.value.toLocaleString()}
257
+ />
258
+ <LeaderboardCard
259
+ title="MEGA CACHE trophies"
260
+ rows={megaRows}
261
+ renderValue={(r) => (
262
+ <>
263
+ {fmtPct(r.value)}
264
+ <span className="lb-meta">
265
+ {fmtDate(
266
+ trophyMeta(r.meta).firstSeenTs ??
267
+ trophyMeta(r.meta).firstSeen ??
268
+ r.ts,
269
+ )}
270
+ </span>
271
+ </>
272
+ )}
273
+ />
274
+ </div>
275
+ )}
276
+
277
+ {reposCount > 0 && (
278
+ <div className="repos-badge-line">
279
+ <span className="repos-badge">{reposCount} repos</span>
280
+ </div>
281
+ )}
282
+
283
+ <h3>Achievements</h3>
284
+ <AchievementTiles achievements={achievements} />
285
+ </div>
286
+ );
287
+ }
@@ -2,7 +2,8 @@
2
2
  * dashboard-client/src/tabs/MetricsTab.tsx — Metrics tab (C2).
3
3
  *
4
4
  * Fetches /api/perf (rolling-window aggregates) + /api/snapshot (for model).
5
- * Renders PerfChart + ModelBadge + resource gauges. Polls every 10s.
5
+ * Renders ModelBadge + PerfChart + 5 perf cards (latency, throughput, process,
6
+ * snapshot cost, TUI lag proxy). Polls every 10s.
6
7
  */
7
8
 
8
9
  import type React from "react";
@@ -11,6 +12,7 @@ import { useApi } from "../hooks/useApi";
11
12
  import { fetchPerf, fetchSnapshot } from "../api/client";
12
13
  import type { PerfResponse, SnapshotResponse } from "@contracts";
13
14
  import { PerfChart } from "../components/PerfChart";
15
+ import { PerfCards } from "../components/PerfCards";
14
16
  import { ModelBadge } from "../components/ModelBadge";
15
17
 
16
18
  export default function MetricsTab(): React.ReactElement {
@@ -44,6 +46,10 @@ export default function MetricsTab(): React.ReactElement {
44
46
  />
45
47
  )}
46
48
  <PerfChart perf={perf} />
49
+ <PerfCards perf={perf} />
50
+ <div className="perf-footer">
51
+ {perf.sampleCount} samples · updated {perf.updatedAt}
52
+ </div>
47
53
  </div>
48
54
  );
49
55
  }
@@ -1,16 +1,36 @@
1
1
  /**
2
- * dashboard-client/src/tabs/OverviewTab.tsx — Overview tab (C1).
2
+ * dashboard-client/src/tabs/OverviewTab.tsx — Overview tab (full spec).
3
3
  *
4
- * 4-card grid: ContextGauge, CompressionCard, TriggerStatus, SessionInfo.
5
- * Header: tier pill + last updated timestamp. Polls /api/snapshot.
4
+ * Renders all 11 cards from the spec:
5
+ * 1. Context Window (ContextGauge)
6
+ * 2. Trigger Status (TriggerStatus)
7
+ * 3. Vector Store (session) (VectorStoreCard)
8
+ * 4. Repo (all sessions) (RepoAllSessionsCard)
9
+ * 5. Data Safety (DataSafetyCard)
10
+ * 6. Configuration (ConfigSummaryCard)
11
+ * 7. Model & Cost Savings (CacheStatusPerModel)
12
+ * 8. Crew / Agents (SessionInfo)
13
+ * 9. Cache Hits & Compactions (CacheHitsCard)
14
+ * 10. Time Saved (TimeSavedCard)
15
+ * 11. Legend (LegendCard)
16
+ *
17
+ * All data comes from the snapshot prop passed by App.tsx.
6
18
  */
7
19
 
8
20
  import type React from "react";
9
21
  import type { SnapshotResponse } from "@contracts";
22
+ import type { RuntimeSnapshot } from "../utils/types";
10
23
  import { ContextGauge } from "../components/ContextGauge";
11
- import { CompressionCard } from "../components/CompressionCard";
12
24
  import { TriggerStatus } from "../components/TriggerStatus";
25
+ import { VectorStoreCard } from "../components/VectorStoreCard";
26
+ import { RepoAllSessionsCard } from "../components/RepoAllSessionsCard";
27
+ import { DataSafetyCard } from "../components/DataSafetyCard";
28
+ import { ConfigSummaryCard } from "../components/ConfigSummaryCard";
29
+ import { CacheStatusPerModel } from "../components/CacheStatusPerModel";
13
30
  import { SessionInfo } from "../components/SessionInfo";
31
+ import { CacheHitsCard } from "../components/CacheHitsCard";
32
+ import { TimeSavedCard } from "../components/TimeSavedCard";
33
+ import { LegendCard } from "../components/LegendCard";
14
34
 
15
35
  export interface OverviewTabProps {
16
36
  snapshot: SnapshotResponse | null;
@@ -38,8 +58,18 @@ export default function OverviewTab({
38
58
  return <div className="tab-stub">Error: {error.message}</div>;
39
59
  if (!snapshot) return <div className="tab-stub">No snapshot data.</div>;
40
60
 
61
+ // Cast to RuntimeSnapshot for tierPct (runtime-only field).
62
+ const d = snapshot as RuntimeSnapshot;
41
63
  const { context, compression, trigger, session, store, tier, updatedAt } =
42
- snapshot;
64
+ d;
65
+ const cfg = d.config;
66
+ const crew = d.crew;
67
+ const integrity = d.integrity;
68
+ const cacheHits = d.cacheHits;
69
+ const compacts = d.compacts;
70
+ const timeSaved = d.timeSaved;
71
+ const repo = d.repo;
72
+ const model = d.model;
43
73
 
44
74
  return (
45
75
  <div className="overview-tab">
@@ -47,19 +77,12 @@ export default function OverviewTab({
47
77
  <span className="tier-pill">{tier}</span>
48
78
  <span className="updated">updated {formatUpdatedAt(updatedAt)}</span>
49
79
  </div>
50
- <div className="card-grid">
80
+ <div className="card-grid overview-card-grid">
51
81
  <ContextGauge
52
82
  tokens={context.tokens}
53
83
  percent={context.percent}
54
84
  contextWindow={context.contextWindow}
55
85
  />
56
- <CompressionCard
57
- tokensIn={compression.repo.tokensIn}
58
- tokensOut={compression.repo.tokensOut}
59
- tokensFreed={compression.repo.tokensFreed}
60
- compressionPct={compression.repo.compressionPct}
61
- dedupPct={compression.repo.dedupPct}
62
- />
63
86
  <TriggerStatus
64
87
  armed={trigger.armed}
65
88
  ready={trigger.ready}
@@ -67,13 +90,76 @@ export default function OverviewTab({
67
90
  thresholdTokens={trigger.thresholdTokens}
68
91
  fastGatePct={trigger.fastGatePct}
69
92
  />
70
- <SessionInfo
71
- state={session.state}
72
- persistedThisSession={session.persistedThisSession}
93
+ <VectorStoreCard
73
94
  checkpointCount={store.checkpointCount}
74
- tokensSaved={store.tokensSaved}
95
+ tokensIn={compression.session.tokensIn}
96
+ tokensOut={compression.session.tokensOut}
97
+ tokensFreed={compression.session.tokensFreed}
98
+ injectedCount={store.injectedCount}
75
99
  dedupHitRate={store.dedupHitRate}
100
+ storageDedupRate={store.storageDedupRate}
101
+ dedupCollapsed={store.dedupCollapsed}
102
+ lastCheckpointId={session.lastCheckpointId}
103
+ compressionPct={compression.session.compressionPct}
104
+ dedupPct={compression.session.dedupPct}
105
+ />
106
+ <RepoAllSessionsCard
107
+ checkpointCount={repo.checkpointCount}
108
+ tokensIn={compression.repo.tokensIn}
109
+ tokensOut={compression.repo.tokensOut}
110
+ tokensFreed={compression.repo.tokensFreed}
111
+ sessionCount={repo.sessionCount}
112
+ dedupCollapsed={repo.dedupCollapsed}
113
+ storageDedupRate={repo.storageDedupRate}
114
+ compressionPct={compression.repo.compressionPct}
115
+ dedupPct={compression.repo.dedupPct}
116
+ />
117
+ <DataSafetyCard
118
+ regionsRetained={integrity.regionsRetained}
119
+ compressedOriginalBytes={integrity.compressedOriginalBytes}
120
+ duplicatesCollapsed={integrity.duplicatesCollapsed}
121
+ bytesPermanentlyDeleted={integrity.bytesPermanentlyDeleted}
122
+ />
123
+ <ConfigSummaryCard
124
+ tier={tier}
125
+ presetTier={d.presetTier}
126
+ pressure={d.pressure}
127
+ thresholdTokens={cfg.thresholdTokens}
128
+ tierPct={cfg.tierPct}
129
+ contextWindow={context.contextWindow}
130
+ fastGatePct={cfg.fastGatePct}
131
+ auto={cfg.auto}
132
+ anchorUserMessages={cfg.anchorUserMessages}
133
+ />
134
+ {model && (
135
+ <CacheStatusPerModel
136
+ name={model.name}
137
+ provider={model.providerName || model.provider}
138
+ inputRate={model.inputRate}
139
+ outputRate={model.outputRate}
140
+ repoTokensFreed={compression.repo.tokensFreed}
141
+ contextWindow={context.contextWindow}
142
+ />
143
+ )}
144
+ <SessionInfo
145
+ activeAgents={crew.activeAgents}
146
+ currentTurn={crew.currentTurn}
147
+ />
148
+ <CacheHitsCard
149
+ cacheHitsSession={cacheHits.session}
150
+ cacheHitsTotal={cacheHits.total}
151
+ tokensSavedSession={cacheHits.sessionTokensSaved}
152
+ tokensSavedTotal={cacheHits.totalTokensSaved}
153
+ compactionsSession={compacts.session}
154
+ compactionsTotal={compacts.total}
155
+ />
156
+ <TimeSavedCard
157
+ compactSessionSec={timeSaved.compact.sessionSec}
158
+ compactTotalSec={timeSaved.compact.totalSec}
159
+ cacheHitSessionSec={timeSaved.cacheHit.sessionSec}
160
+ cacheHitTotalSec={timeSaved.cacheHit.totalSec}
76
161
  />
162
+ <LegendCard />
77
163
  </div>
78
164
  </div>
79
165
  );
@@ -1,56 +1,91 @@
1
1
  /**
2
- * dashboard-client/src/tabs/ReposTab.tsx — Repos tab (C2).
2
+ * dashboard-client/src/tabs/ReposTab.tsx — Repos tab (C2 fleshed out).
3
3
  *
4
- * Fetches /api/repos + /api/summary. Renders SummaryTiles + RepoTable.
5
- * Row click opens RepoDetailModal. Polls every 10s.
4
+ * Renders: SummaryTiles (4 tiles), All Repositories table (RepoTable),
5
+ * Active Repos table (ActiveReposTable), Savings by Model table
6
+ * (SavingsByModelTable), and per-repo detail modal (RepoDetailModal).
7
+ *
8
+ * Data sources: fetchIndex() for summary + all repos + savings-by-model;
9
+ * fetchServers() for active repos. Both polled every 10s.
6
10
  */
7
11
 
8
12
  import type React from "react";
9
13
  import { useCallback, useState } from "react";
10
14
  import { useApi } from "../hooks/useApi";
11
- import { fetchRepos, fetchSummary } from "../api/client";
12
- import type {
13
- ReposResponse,
14
- SummaryResponse,
15
- IndexesIndexRow,
16
- } from "@contracts";
15
+ import { fetchIndex, fetchServers } from "../api/client";
16
+ import type { IndexesIndexRow, ServersResponse } from "@contracts";
17
17
  import { SummaryTiles } from "../components/SummaryTiles";
18
18
  import { RepoTable } from "../components/RepoTable";
19
19
  import { RepoDetailModal } from "../components/RepoDetailModal";
20
+ import { ActiveReposTable } from "../components/ActiveReposTable";
21
+ import { SavingsByModelTable } from "../components/SavingsByModelTable";
22
+
23
+ /**
24
+ * Actual runtime shape of /api/index. The contract's IndexesSummaryResponse
25
+ * puts summary fields at the top level, but the real server nests them under
26
+ * `summary` (matching IndexFallbackResponse shape with non-null summary).
27
+ */
28
+ interface DashboardIndexSummary {
29
+ totalRepos: number;
30
+ totalCheckpoints: number;
31
+ totalTokensSaved: number;
32
+ totalCompressedOriginalBytes: number;
33
+ }
34
+
35
+ interface DashboardIndexResponse {
36
+ updatedAt: string | null;
37
+ summary: DashboardIndexSummary | null;
38
+ repos: IndexesIndexRow[];
39
+ }
40
+
41
+ /** Fetch /api/index and cast to the actual runtime shape. */
42
+ async function fetchIndexTyped(): Promise<DashboardIndexResponse> {
43
+ return (await fetchIndex()) as unknown as DashboardIndexResponse;
44
+ }
20
45
 
21
46
  export default function ReposTab(): React.ReactElement {
22
47
  const [selected, setSelected] = useState<IndexesIndexRow | null>(null);
23
48
 
24
- const { data: reposResp, error: reposErr } = useApi<ReposResponse>(
25
- useCallback(() => fetchRepos(), []),
26
- { pollInterval: 10_000 },
27
- );
28
- const { data: summaryResp } = useApi<SummaryResponse>(
29
- useCallback(() => fetchSummary(), []),
49
+ const { data: indexData, error: indexErr } =
50
+ useApi<DashboardIndexResponse>(
51
+ useCallback(() => fetchIndexTyped(), []),
52
+ { pollInterval: 10_000 },
53
+ );
54
+
55
+ const { data: serversData } = useApi<ServersResponse>(
56
+ useCallback(() => fetchServers(), []),
30
57
  { pollInterval: 10_000 },
31
58
  );
32
59
 
33
- if (reposErr && !reposResp) {
60
+ if (indexErr && !indexData) {
34
61
  return (
35
- <div className="tab-stub">Error loading repos: {reposErr.message}</div>
62
+ <div className="tab-stub">Error loading repos: {indexErr.message}</div>
36
63
  );
37
64
  }
38
- if (!reposResp) {
65
+ if (!indexData) {
39
66
  return <div className="tab-stub">Loading repos…</div>;
40
67
  }
41
68
 
42
- const repos = reposResp.repos;
43
- const summary = summaryResp?.summary;
69
+ const summary = indexData.summary;
70
+ const repos = indexData.repos;
71
+ const servers = serversData?.servers ?? [];
44
72
 
45
73
  return (
46
74
  <div className="repos-tab">
47
75
  <SummaryTiles
48
- totalRepos={summaryResp?.totalRepos ?? 0}
49
- activeRepos={summaryResp?.activeRepos ?? 0}
76
+ totalRepos={summary?.totalRepos ?? 0}
50
77
  totalCheckpoints={summary?.totalCheckpoints ?? 0}
51
78
  totalTokensSaved={summary?.totalTokensSaved ?? 0}
79
+ compressedOriginalBytes={summary?.totalCompressedOriginalBytes ?? 0}
52
80
  />
81
+ <h2 className="section-header">All Repositories</h2>
53
82
  <RepoTable repos={repos} onSelect={setSelected} />
83
+ <h2 className="section-header">
84
+ Active Repos — Live Cache Hits &amp; Compactions
85
+ </h2>
86
+ <ActiveReposTable servers={servers} />
87
+ <h2 className="section-header">Savings by Model</h2>
88
+ <SavingsByModelTable repos={repos} />
54
89
  {selected && (
55
90
  <RepoDetailModal repo={selected} onClose={() => setSelected(null)} />
56
91
  )}