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
package/README.md CHANGED
@@ -80,7 +80,7 @@ Detailed architecture: [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md)
80
80
 
81
81
  ```bash
82
82
  npm run build # TypeScript compile
83
- npm test # Build + 353 tests
83
+ npm test # Build + 609 tests
84
84
  npm run lint # Type check + guardrails scan
85
85
  ```
86
86
 
@@ -101,4 +101,16 @@ export const ENDPOINTS = {
101
101
  path: '/api/achievements',
102
102
  description: 'All achievement rows with unlock state.',
103
103
  },
104
+ /** GET /api/sessions — Active sessions with latest token usage (S39). */
105
+ sessions: {
106
+ method: 'GET',
107
+ path: '/api/sessions',
108
+ description: 'Active pi sessions with latest token usage + heartbeat.',
109
+ },
110
+ /** GET /api/sessions/timeseries — Stacked per-session token timeseries (S39). */
111
+ sessionTimeseries: {
112
+ method: 'GET',
113
+ path: '/api/sessions/timeseries',
114
+ description: 'Recharts-ready per-session token timeseries for the stacked memory graph.',
115
+ },
104
116
  };
@@ -73,9 +73,10 @@ void _c_gameScores;
73
73
  void _c_perf;
74
74
  void _c_achievements;
75
75
  // ─── Compile-Time: ENDPOINTS path/method consistency ─────────────────────────
76
- // Verify the ENDPOINTS registry has exactly 13 entries with correct paths.
76
+ // Verify the ENDPOINTS registry has exactly 15 entries with correct paths.
77
+ // (Sprint 39 added /api/sessions + /api/sessions/timeseries.)
77
78
  const ENDPOINT_KEYS = Object.keys(ENDPOINTS);
78
- const EXPECTED_ENDPOINT_COUNT = 13;
79
+ const EXPECTED_ENDPOINT_COUNT = 15;
79
80
  /** All `/api/*` paths served by server.ts (extracted from the route handlers). */
80
81
  const SERVER_TS_PATHS = [
81
82
  "/api/snapshot",
@@ -90,10 +91,12 @@ const SERVER_TS_PATHS = [
90
91
  "/api/game-scores",
91
92
  "/api/perf",
92
93
  "/api/achievements",
94
+ "/api/sessions",
95
+ "/api/sessions/timeseries",
93
96
  ];
94
97
  // ─── Tests ───────────────────────────────────────────────────────────────────
95
98
  describe("ENDPOINTS registry", () => {
96
- test("has exactly 13 endpoint entries", () => {
99
+ test("has exactly 15 endpoint entries", () => {
97
100
  assert.equal(ENDPOINT_KEYS.length, EXPECTED_ENDPOINT_COUNT, `ENDPOINTS must have ${EXPECTED_ENDPOINT_COUNT} entries, got ${ENDPOINT_KEYS.length}`);
98
101
  });
99
102
  test("every server.ts /api/* path has a corresponding ENDPOINTS entry", () => {
@@ -649,7 +652,7 @@ describe("GET /api/achievements", () => {
649
652
  // Validate the SseEvent union by checking each variant has a `type` field
650
653
  // matching its discriminator.
651
654
  describe("SSE /api/events — SseEvent union discriminator validation", () => {
652
- /** All 20 SseEvent variants with their discriminator `type` value and a minimal payload. */
655
+ /** All 21 SseEvent variants with their discriminator `type` value and a minimal payload. */
653
656
  const SSE_VARIANTS = [
654
657
  {
655
658
  discriminator: "compact_start",
@@ -839,9 +842,19 @@ describe("SSE /api/events — SseEvent union discriminator validation", () => {
839
842
  frame: "\x1b[2J\x1b[H",
840
843
  }),
841
844
  },
845
+ {
846
+ discriminator: "session_sample",
847
+ payload: JSON.stringify({
848
+ type: "session_sample",
849
+ ts: "2025-01-01T00:00:00Z",
850
+ sessionId: "sess-abc",
851
+ tokens: 42_000,
852
+ percent: 35.5,
853
+ }),
854
+ },
842
855
  ];
843
- test("all 20 SseEvent variants have a type field matching their discriminator", () => {
844
- assert.equal(SSE_VARIANTS.length, 20, "SseEvent union should have 20 variants");
856
+ test("all 21 SseEvent variants have a type field matching their discriminator", () => {
857
+ assert.equal(SSE_VARIANTS.length, 21, "SseEvent union should have 21 variants");
845
858
  for (const variant of SSE_VARIANTS) {
846
859
  const obj = JSON.parse(variant.payload);
847
860
  assert.ok("type" in obj, `SSE variant "${variant.discriminator}" must have a "type" field`);
@@ -0,0 +1,37 @@
1
+ /**
2
+ * File-reading helper functions for the dashboard server.
3
+ */
4
+ import { readFileSync } from "node:fs";
5
+ export function readSnapshot(snapshotPath) {
6
+ try {
7
+ const raw = readFileSync(snapshotPath, "utf-8");
8
+ return JSON.parse(raw);
9
+ }
10
+ catch {
11
+ return {
12
+ version: 1,
13
+ updatedAt: null,
14
+ tier: "unknown",
15
+ config: { fastGatePct: 80, thresholdTokens: 100_000, anchorUserMessages: 1, preserveRecent: 2, auto: true, autoInlineK: 3 },
16
+ session: { id: null, state: null, persistedThisSession: false, lastCheckpointId: null, lastCompactedFrom: 0 },
17
+ context: { tokens: null, percent: null, contextWindow: 0 },
18
+ trigger: { armed: false, ready: false, currentTokens: null, thresholdTokens: 100_000, fastGatePct: 80 },
19
+ store: { checkpointCount: 0, totalTokenEstimate: 0, originalTokens: 0, tokensSaved: 0, injectedCount: 0, dedupHitRate: 0, storageDedupRate: 0, dedupCollapsed: 0 },
20
+ crew: { activeAgents: 0, currentTurn: 0 },
21
+ repo: { checkpointCount: 0, totalTokenEstimate: 0, originalTokens: 0, tokensSaved: 0, sessionCount: 0, dedupAttempts: 0, dedupCollapsed: 0, storageDedupRate: 0 },
22
+ integrity: { regionsRetained: 0, compressedOriginalBytes: 0, duplicatesCollapsed: 0, bytesPermanentlyDeleted: 0 },
23
+ model: undefined,
24
+ };
25
+ }
26
+ }
27
+ export function readFrom(path, charOffset) {
28
+ try {
29
+ const content = readFileSync(path, "utf-8");
30
+ if (content.length <= charOffset)
31
+ return { data: "", offset: charOffset };
32
+ return { data: content.slice(charOffset), offset: content.length };
33
+ }
34
+ catch {
35
+ return { data: "", offset: charOffset };
36
+ }
37
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * "All repos" tab panel — machine-wide registry table (index.sqlite).
3
+ *
4
+ * Rows are populated by the index poller in `script.ts` (shared with the
5
+ * in-panel table on the Current-repo tab via the same render call).
6
+ */
7
+ export function allReposTab() {
8
+ return `<!-- All repos (machine-wide registry from index.sqlite) -->
9
+ <div class="tab-panel" id="panel-all">
10
+ <table class="repos">
11
+ <thead>
12
+ <tr>
13
+ <th>Repo</th><th>Model</th>
14
+ <th style="text-align:right">Checkpoints</th>
15
+ <th style="text-align:right">Tokens Saved</th>
16
+ <th style="text-align:right">Retained</th>
17
+ <th style="text-align:right">Last Compacted</th>
18
+ </tr>
19
+ </thead>
20
+ <tbody id="all-rows"><tr><td colspan="6" class="repo-none">loading…</td></tr></tbody>
21
+ </table>
22
+ <div class="updated" id="all-updated"></div>
23
+ </div>
24
+
25
+ `;
26
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Body opening: </head><body>, offline banner, page heading, and the tab nav.
3
+ *
4
+ * `tierName` is interpolated into the heading tier badge.
5
+ * Includes the new "High Score" future-tab button (next project).
6
+ */
7
+ export function bodyOpen(tierName) {
8
+ return `</head>
9
+ <body>
10
+
11
+ <div class="offline-banner" id="offline-banner">Dashboard data unavailable — waiting for a pi session to write snapshot...</div>
12
+
13
+ <h1><span>mega-compact</span><span class="tier">${tierName}</span><span class="model-pill" id="hdr-model">—</span></h1>
14
+
15
+ <nav class="tabs">
16
+ <button class="tab active" data-tab="current">Current repo</button>
17
+ <button class="tab" data-tab="all">All repos</button>
18
+ <button class="tab" data-tab="summary">Summary</button>
19
+ <button class="tab future" data-tab="highscore">High Score<span class="soon">soon</span></button>
20
+ </nav>
21
+
22
+ `;
23
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * "Current repo" tab panel — the per-repo dashboard view.
3
+ *
4
+ * Contains: context-window meter, trigger status, vector-store stats,
5
+ * repo-wide stats, data-safety card, configuration, model & cost savings,
6
+ * crew/agents, a legend, the live event stream, and the in-panel all-repos
7
+ * table (shared rows with the All-repos tab via the index poller).
8
+ *
9
+ * `tierName` is interpolated into the Configuration card.
10
+ */
11
+ export function currentRepoTab(tierName) {
12
+ return `<!-- Current repo (existing single-repo view) -->
13
+ <div class="tab-panel" id="panel-current">
14
+ <div class="grid">
15
+ <div class="card">
16
+ <h2>Context Window</h2>
17
+ <div class="meter-label" id="ctx-pct">—</div>
18
+ <div class="meter-track"><div class="meter-fill" id="ctx-bar" style="width:0%"></div></div>
19
+ <div class="meter-sub" id="ctx-sub">waiting for data</div>
20
+ </div>
21
+ <div class="card">
22
+ <h2>Trigger Status</h2>
23
+ <div class="status-row"><div class="bullet" id="tr-armed"></div><span>Armed (context ≥ fast gate)</span></div>
24
+ <div class="status-row"><div class="bullet" id="tr-ready"></div><span>Ready (tokens ≥ threshold)</span></div>
25
+ <div class="state-text" id="tr-state">waiting</div>
26
+ </div>
27
+ <div class="card">
28
+ <h2>Vector Store</h2>
29
+ <div class="stat-grid">
30
+ <span class="label" title="A saved summary of a chunk of your conversation that was compacted to free up space.">Checkpoints</span><span class="value" id="st-count">0</span>
31
+ <span class="label" title="How much conversation we are currently holding as compact summaries (the 'memory' this extension keeps). Smaller is better.">Tokens Stored</span><span class="value" id="st-tokens">0</span>
32
+ <span class="label" title="Total size of the original conversation text before it was compacted.">Original Tokens</span><span class="value" id="st-orig">0</span>
33
+ <span class="label" title="How much conversation space we have freed up for you (original size minus the compact summary we kept).">Tokens Saved</span><span class="value" id="st-saved">0</span>
34
+ <span class="label" title="How many times old context was automatically brought back into the conversation because it was relevant to what you were doing.">Injected</span><span class="value" id="st-injected">0</span>
35
+ <span class="label" title="Of the times we recalled old context, how often it was actually on-topic.">Recall Relevance</span><span class="value" id="st-dedup">0%</span>
36
+ <span class="label" title="How often new content matched something we already had, so we skipped storing a duplicate copy. Higher = less wasted space.">Storage Dedup</span><span class="value" id="st-sdedup">0%</span>
37
+ <span class="label" title="How many duplicate chunks we collapsed into one instead of storing separately.">Collapsed</span><span class="value" id="st-collapsed">0</span>
38
+ <span class="label" title="The ID of the most recent saved checkpoint.">Last ID</span><span class="value" id="st-lastid">—</span>
39
+ </div>
40
+ </div>
41
+ <div class="card">
42
+ <h2>Repo (all sessions)</h2>
43
+ <div class="stat-grid">
44
+ <span class="label">Checkpoints</span><span class="value" id="rp-count">0</span>
45
+ <span class="label">Tokens Stored</span><span class="value" id="rp-tokens">0</span>
46
+ <span class="label">Original Tokens</span><span class="value" id="rp-orig">0</span>
47
+ <span class="label">Tokens Saved</span><span class="value" id="rp-saved">0</span>
48
+ <span class="label">Sessions</span><span class="value" id="rp-sessions">0</span>
49
+ <span class="label">Collapsed</span><span class="value" id="rp-collapsed">0</span>
50
+ <span class="label">Storage Dedup</span><span class="value" id="rp-sdedup">0%</span>
51
+ </div>
52
+ </div>
53
+ <div class="card safe">
54
+ <h2>🛡 Data Safety</h2>
55
+ <div class="stat-grid">
56
+ <span class="label">Regions Retained</span><span class="value" id="ig-retained">0</span>
57
+ <span class="label">Compressed-Original</span><span class="value" id="ig-bytes">0 B</span>
58
+ <span class="label">Dedup Duplicates</span><span class="value" id="ig-dupes">0</span>
59
+ <span class="label">Permanently Deleted</span><span class="value ok" id="ig-deleted">0 B</span>
60
+ </div>
61
+ <p class="safe-note">Every compacted region is kept verbatim (compressed). "Drop" = removed from the live window only. We never delete your data.</p>
62
+ </div>
63
+ <div class="card">
64
+ <h2>Configuration</h2>
65
+ <div class="conf-grid">
66
+ <span class="label">Tier</span><span class="value" id="cf-tier">${tierName}</span>
67
+ <span class="label">Threshold</span><span class="value" id="cf-threshold">—</span>
68
+ <span class="label">Fast Gate</span><span class="value" id="cf-gate">—</span>
69
+ <span class="label">Auto</span><span class="value" id="cf-auto">—</span>
70
+ <span class="label">Anchor</span><span class="value" id="cf-anchor">—</span>
71
+ </div>
72
+ </div>
73
+ <div class="card cost">
74
+ <h2>💰 Model &amp; Cost Savings</h2>
75
+ <div class="cost-usd" id="cost-usd">≈ $0.00 saved</div>
76
+ <div class="cost-sub" id="cost-windows">0 context-windows extended</div>
77
+ <div class="stat-grid" style="margin-top:12px">
78
+ <span class="label" title="The model pi is currently using — its pricing drives the cost figure.">Model</span><span class="value" id="md-name">—</span>
79
+ <span class="label" title="The provider serving the model.">Provider</span><span class="value" id="md-provider">—</span>
80
+ <span class="label" title="USD per input token, from the model's pricing.">Input Rate</span><span class="value" id="md-input">—</span>
81
+ <span class="label" title="USD per output token, from the model's pricing.">Output Rate</span><span class="value" id="md-output">—</span>
82
+ </div>
83
+ </div>
84
+ <div class="card">
85
+ <h2>Crew / Agents</h2>
86
+ <div class="stat-grid">
87
+ <span class="label">Active Agents</span><span class="value" id="cr-agents">0</span>
88
+ <span class="label">Current Turn</span><span class="value" id="cr-turn">0</span>
89
+ <span class="label">Status</span><span class="value" id="cr-status">idle</span>
90
+ </div>
91
+ </div>
92
+ <div class="card legend">
93
+ <h2>What these numbers mean</h2>
94
+ <ul class="legend-list">
95
+ <li><b>Tokens saved</b> — conversation space this extension has freed up for you (it compacted old text into short summaries).</li>
96
+ <li><b>Tokens stored</b> — how much "memory" (compact summaries) the extension is currently holding for this repo.</li>
97
+ <li><b>Injected</b> — times old context was automatically pasted back in because it was relevant to your current task.</li>
98
+ <li><b>Recall relevance</b> — of those, how often the recalled context was actually on-topic.</li>
99
+ <li><b>Storage dedup</b> — how often new content matched something already saved, so a duplicate copy was skipped (saves space).</li>
100
+ <li><b>Data safety</b> — every compacted region is kept verbatim (compressed). Nothing is permanently deleted; you can restore any of it.</li>
101
+ </ul>
102
+ <p class="legend-note">Hover any label above for a quick explanation.</p>
103
+ </div>
104
+ </div>
105
+
106
+ <div class="events">
107
+ <h2>Event Stream</h2>
108
+ <div class="events-wrap" id="events"><div class="empty">connecting…</div></div>
109
+ </div>
110
+
111
+ <h2 style="margin-top:24px;font-size:13px;color:#8b949e;text-transform:uppercase;letter-spacing:.5px">All Repositories</h2>
112
+ <table class="repos">
113
+ <thead>
114
+ <tr>
115
+ <th>Repo</th><th>Model</th>
116
+ <th style="text-align:right">Checkpoints</th>
117
+ <th style="text-align:right">Tokens Saved</th>
118
+ <th style="text-align:right">Retained</th>
119
+ <th style="text-align:right">Last Compacted</th>
120
+ </tr>
121
+ </thead>
122
+ <tbody id="cur-rows"><tr><td colspan="6" class="repo-none">loading…</td></tr></tbody>
123
+ </table>
124
+ <div class="updated" id="cur-updated"></div>
125
+
126
+ <div class="updated" id="updated"></div>
127
+ </div><!-- /panel-current -->
128
+
129
+ `;
130
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Document opening: DOCTYPE, <html>, <head> opening + meta/title.
3
+ *
4
+ * Styles are injected between this fragment and `bodyOpen()` by the
5
+ * `dashboardHtml` composer in `../html-template.ts`.
6
+ */
7
+ export function headOpen() {
8
+ return `<!DOCTYPE html>
9
+ <html lang="en">
10
+ <head>
11
+ <meta charset="utf-8">
12
+ <meta name="viewport" content="width=device-width, initial-scale=1">
13
+ <title>mega-compact dashboard</title>
14
+ `;
15
+ // trailing \n preserves the original blank-line separation to <style> in the composer
16
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * "High Score" tab — FUTURE / PLACEHOLDER.
3
+ *
4
+ * Reserved for the next project milestone: a per-repo + global
5
+ * compaction-savings leaderboard ("high score" — most tokens saved, best
6
+ * recall relevance, longest context-window extension streak, etc.).
7
+ *
8
+ * Ships as a visible-but-inert stub so the tab nav, routing, and styles are
9
+ * in place; the data model + scoring logic land in a follow-up. The composer
10
+ * in `../html-template.ts` includes this panel; `script.ts` wires
11
+ * `highscore` into the tab-switch `panels` map so clicking the tab reveals
12
+ * this placeholder.
13
+ */
14
+ export function highScoreTab() {
15
+ return `<!-- High Score (FUTURE / placeholder for the next project milestone) -->
16
+ <div class="tab-panel" id="panel-highscore">
17
+ <div class="placeholder">
18
+ <div class="em">🏆</div>
19
+ <h2>High Score</h2>
20
+ <p>A compaction-savings leaderboard is coming in the next project milestone — per-repo and global rankings for tokens saved, recall relevance, and context-window extension streaks. Stay tuned.</p>
21
+ </div>
22
+ </div>
23
+
24
+ `;
25
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Per-repo detail modal — fixed overlay opened when a repo row is clicked.
3
+ *
4
+ * Position-independent in the DOM; the modal is toggled via the `.open`
5
+ * class by `script.ts`. Sits between the tab panels and the script so it
6
+ * overlays whichever panel is active.
7
+ */
8
+ export function repoDetailModal() {
9
+ return `<!-- Per-repo detail modal -->
10
+ <div class="repo-detail" id="repo-detail">
11
+ <div class="repo-detail-box">
12
+ <h2><span id="rd-name">Repo</span><button class="repo-close" id="rd-close" title="Close">×</button></h2>
13
+ <div class="repo-path" id="rd-path"></div>
14
+ <div class="stat-grid">
15
+ <span class="label">Model</span><span class="value" id="rd-model">—</span>
16
+ <span class="label">Checkpoints</span><span class="value" id="rd-cp">0</span>
17
+ <span class="label">Tokens Saved</span><span class="value" id="rd-saved">0</span>
18
+ <span class="label">Compressed-Original</span><span class="value" id="rd-bytes">0 B</span>
19
+ <span class="label">Last Compacted</span><span class="value" id="rd-when">—</span>
20
+ <span class="label">Provider</span><span class="value" id="rd-provider">—</span>
21
+ </div>
22
+ </div>
23
+ </div>
24
+
25
+ `;
26
+ }
@@ -0,0 +1,259 @@
1
+ /**
2
+ * Client-side script: snapshot polling, SSE event stream, multi-repo index
3
+ * polling, per-repo detail modal, and tab switching.
4
+ *
5
+ * One IIFE — the logic is cross-tab (the index poller writes to both the
6
+ * Current-repo table and the All-repos tab) so it stays cohesive rather than
7
+ * being split per-tab. `</body></html>` closes the document.
8
+ *
9
+ * PREVENT-PI-004: the two `fetch(...)` calls below are the dashboard's
10
+ * browser-side localhost probes and carry inline `// guardrails-allow
11
+ * PREVENT-PI-004: <reason>` annotations (scanner-enforced).
12
+ */
13
+ export function script() {
14
+ return `<script>
15
+ (function() {
16
+ var evBox = document.getElementById('events');
17
+ var evBuffer = [];
18
+ var MAX_EV = 50;
19
+ var offlineBanner = document.getElementById('offline-banner');
20
+
21
+ function bullet(el, on, na) {
22
+ el.className = 'bullet ' + (na ? 'bullet-na' : on ? 'bullet-on' : 'bullet-off');
23
+ }
24
+
25
+ function renderSnapshot(d) {
26
+ if (!d || !d.updatedAt) { offlineBanner.style.display = 'block'; return; }
27
+ offlineBanner.style.display = 'none';
28
+
29
+ var pct = d.context.percent || 0;
30
+ document.getElementById('ctx-pct').textContent = pct + '%';
31
+ var bar = document.getElementById('ctx-bar');
32
+ bar.style.width = Math.max(pct, 1) + '%';
33
+ bar.className = 'meter-fill ' + (pct >= 90 ? 'meter-red' : pct >= 70 ? 'meter-yellow' : 'meter-green');
34
+ var tok = d.context.tokens != null ? d.context.tokens.toLocaleString() : '?';
35
+ var win = d.context.contextWindow ? d.context.contextWindow.toLocaleString() : '?';
36
+ document.getElementById('ctx-sub').textContent = tok + ' / ' + win + ' tokens';
37
+
38
+ bullet(document.getElementById('tr-armed'), d.trigger.armed, false);
39
+ bullet(document.getElementById('tr-ready'), d.trigger.ready, !d.trigger.armed);
40
+ var state = d.trigger.ready ? 'THRESHOLD EXCEEDED — compacting next event' :
41
+ d.trigger.armed ? 'past fast gate — monitoring token count' : 'idle — below fast gate';
42
+ document.getElementById('tr-state').textContent = state;
43
+
44
+ document.getElementById('st-count').textContent = d.store.checkpointCount;
45
+ document.getElementById('st-tokens').textContent = d.store.totalTokenEstimate.toLocaleString();
46
+ document.getElementById('st-orig').textContent = (d.store.originalTokens || 0).toLocaleString();
47
+ document.getElementById('st-saved').textContent = (d.store.tokensSaved || 0).toLocaleString();
48
+ document.getElementById('st-injected').textContent = d.store.injectedCount;
49
+ document.getElementById('st-dedup').textContent = Math.round(d.store.dedupHitRate * 100) + '%';
50
+ var sdr = d.store.storageDedupRate || 0;
51
+ document.getElementById('st-sdedup').textContent = (sdr * 100 >= 10 ? Math.round(sdr * 100) : (sdr * 100).toFixed(1)) + '%';
52
+ document.getElementById('st-collapsed').textContent = d.store.dedupCollapsed || 0;
53
+ document.getElementById('st-lastid').textContent = d.session.lastCheckpointId || '—';
54
+
55
+ // Repo-wide (all sessions in this repo's SQLite store).
56
+ var repo = d.repo || { checkpointCount: 0, totalTokenEstimate: 0, originalTokens: 0, tokensSaved: 0, sessionCount: 0, dedupCollapsed: 0, storageDedupRate: 0 };
57
+ document.getElementById('rp-count').textContent = repo.checkpointCount;
58
+ document.getElementById('rp-tokens').textContent = repo.totalTokenEstimate.toLocaleString();
59
+ document.getElementById('rp-orig').textContent = (repo.originalTokens || 0).toLocaleString();
60
+ document.getElementById('rp-saved').textContent = (repo.tokensSaved || 0).toLocaleString();
61
+ document.getElementById('rp-sessions').textContent = repo.sessionCount || 0;
62
+ document.getElementById('rp-collapsed').textContent = repo.dedupCollapsed || 0;
63
+ var rsdr = repo.storageDedupRate || 0;
64
+ document.getElementById('rp-sdedup').textContent = (rsdr * 100 >= 10 ? Math.round(rsdr * 100) : (rsdr * 100).toFixed(1)) + '%';
65
+
66
+ // Data-safety invariant (Phase 0 — trust foundation).
67
+ var ig = d.integrity || { regionsRetained: 0, compressedOriginalBytes: 0, duplicatesCollapsed: 0, bytesPermanentlyDeleted: 0 };
68
+ function fmtBytes(b) {
69
+ b = b || 0;
70
+ if (b >= 1048576) return (b / 1048576).toFixed(1) + ' MiB';
71
+ if (b >= 1024) return (b / 1024).toFixed(1) + ' KiB';
72
+ return b + ' B';
73
+ }
74
+ document.getElementById('ig-retained').textContent = (ig.regionsRetained || 0).toLocaleString();
75
+ document.getElementById('ig-bytes').textContent = fmtBytes(ig.compressedOriginalBytes);
76
+ document.getElementById('ig-dupes').textContent = (ig.duplicatesCollapsed || 0).toLocaleString();
77
+ document.getElementById('ig-deleted').textContent = fmtBytes(ig.bytesPermanentlyDeleted);
78
+
79
+ // Crew / agents (live sub-agent activity + turn).
80
+ var crew = d.crew || { activeAgents: 0, currentTurn: 0 };
81
+ document.getElementById('cr-agents').textContent = crew.activeAgents || 0;
82
+ document.getElementById('cr-turn').textContent = crew.currentTurn || 0;
83
+ document.getElementById('cr-status').textContent = (crew.activeAgents > 0)
84
+ ? ('▶ ' + crew.activeAgents + ' running') : 'idle';
85
+
86
+ document.getElementById('cf-tier').textContent = d.tier;
87
+ document.getElementById('cf-threshold').textContent = d.config.thresholdTokens.toLocaleString();
88
+ document.getElementById('cf-gate').textContent = d.config.fastGatePct + '%';
89
+ document.getElementById('cf-auto').textContent = d.config.auto ? 'enabled' : 'disabled';
90
+ document.getElementById('cf-anchor').textContent = d.config.anchorUserMessages;
91
+
92
+ // --- Active model + cost savings (same calc as /mega-status) ---------------
93
+ var model = d.model;
94
+ document.getElementById('hdr-model').textContent = model && model.name ? model.name : '—';
95
+ document.getElementById('md-name').textContent = model && model.name ? model.name : '—';
96
+ document.getElementById('md-provider').textContent = model && model.providerName ? model.providerName : (model && model.provider ? model.provider : '—');
97
+ document.getElementById('md-input').textContent = model && model.inputRate ? '$' + (model.inputRate).toFixed(6) : '—';
98
+ document.getElementById('md-output').textContent = model && model.outputRate ? '$' + (model.outputRate).toFixed(6) : '—';
99
+ if (model && model.inputRate && repo.tokensSaved > 0) {
100
+ var usd = (repo.tokensSaved * model.inputRate);
101
+ var win = d.context.contextWindow || 0;
102
+ var windows = win > 0 ? (repo.tokensSaved / win).toFixed(1) : '0';
103
+ document.getElementById('cost-usd').textContent = '≈ $' + usd.toFixed(4) + ' saved';
104
+ document.getElementById('cost-windows').textContent = windows + ' context-windows extended';
105
+ } else {
106
+ document.getElementById('cost-usd').textContent = '≈ $0.00 saved';
107
+ document.getElementById('cost-windows').textContent = '0 context-windows extended';
108
+ }
109
+
110
+ document.getElementById('updated').textContent = 'Updated ' + new Date(d.updatedAt).toLocaleTimeString();
111
+ }
112
+
113
+ function sanitize(s) {
114
+ return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
115
+ }
116
+
117
+ function renderEvent(ev) {
118
+ evBuffer.unshift(ev);
119
+ if (evBuffer.length > MAX_EV) evBuffer.length = MAX_EV;
120
+ evBox.innerHTML = evBuffer.map(function(e) {
121
+ var t = e.ts ? new Date(e.ts).toLocaleTimeString() : '';
122
+ var detail = '';
123
+ if (e.data) {
124
+ if (e.data.checkpointId) detail = sanitize(e.data.checkpointId);
125
+ else if (e.data.query) detail = sanitize(e.data.query.slice(0, 80));
126
+ if (e.data.tokenEstimate != null) detail += ' ' + e.data.tokenEstimate + ' tok';
127
+ if (e.data.deduped) detail += ' (deduped)';
128
+ if (e.data.injected != null) detail = 'injected: ' + e.data.injected + (e.data.empty ? ' (empty)' : '');
129
+ }
130
+ return '<div class="ev">' +
131
+ '<span class="ev-time">' + t + '</span>' +
132
+ '<span class="ev-type ev-type-' + sanitize(e.type) + '">' + sanitize(e.type) + '</span>' +
133
+ '<span class="ev-detail">' + detail + '</span></div>';
134
+ }).join('');
135
+ }
136
+
137
+ // Poll snapshot every 2s
138
+ function pollSnapshot() {
139
+ fetch('/api/snapshot').then(function(r) { return r.json(); }).then(renderSnapshot).catch(function() {}); // guardrails-allow PREVENT-PI-004: dashboard browser-side fetch (localhost)
140
+ }
141
+ pollSnapshot();
142
+ setInterval(pollSnapshot, 2000);
143
+
144
+ // SSE for events
145
+ function connectSSE() {
146
+ var es = new EventSource('/api/events');
147
+ es.onmessage = function(msg) {
148
+ try { renderEvent(JSON.parse(msg.data)); } catch(e) {}
149
+ };
150
+ es.onerror = function() {
151
+ es.close();
152
+ setTimeout(connectSSE, 3000);
153
+ };
154
+ }
155
+ connectSSE();
156
+
157
+ // --- Multi-repo (index.sqlite via /api/index) ---------------------------
158
+ function fmtBytesTop(b) {
159
+ b = b || 0;
160
+ if (b >= 1048576) return (b / 1048576).toFixed(1) + ' MiB';
161
+ if (b >= 1024) return (b / 1024).toFixed(1) + ' KiB';
162
+ return b + ' B';
163
+ }
164
+ function renderIndex(d) {
165
+ d = d || { updatedAt: null, summary: null, repos: [] };
166
+ var repos = d.repos || [];
167
+ var s = d.summary || { totalRepos: 0, totalCheckpoints: 0, totalTokensSaved: 0, totalCompressedOriginalBytes: 0 };
168
+ document.getElementById('sm-repos').textContent = (s.totalRepos || 0).toLocaleString();
169
+ document.getElementById('sm-checkpoints').textContent = (s.totalCheckpoints || 0).toLocaleString();
170
+ document.getElementById('sm-saved').textContent = (s.totalTokensSaved || 0).toLocaleString();
171
+ document.getElementById('sm-bytes').textContent = fmtBytesTop(s.totalCompressedOriginalBytes);
172
+
173
+ // Shared clickable-row renderer for both the in-current table and the
174
+ // All-repos tab — each row opens the per-repo detail modal.
175
+ function rowsHtml() {
176
+ if (!repos.length) return '<tr><td colspan="6" class="repo-none">No repositories registered yet.</td></tr>';
177
+ return repos.map(function(r) {
178
+ var model = r.modelName
179
+ ? '<span class="repo-model">' + sanitize(r.modelName) + '</span>'
180
+ : '<span class="repo-none">—</span>';
181
+ var when = r.lastCompactedAt ? new Date(r.lastCompactedAt).toLocaleString() : '—';
182
+ return '<tr class="repo-link" data-repo="' + sanitize(r.repoRoot) + '">' +
183
+ '<td title="' + sanitize(r.repoRoot) + '">' + sanitize(r.displayName || r.repoRoot) + '</td>' +
184
+ '<td>' + model + '</td>' +
185
+ '<td class="num">' + (r.checkpointCount || 0).toLocaleString() + '</td>' +
186
+ '<td class="num">' + (r.tokensSaved || 0).toLocaleString() + '</td>' +
187
+ '<td class="num">' + fmtBytesTop(r.compressedOriginalBytes) + '</td>' +
188
+ '<td class="num">' + sanitize(when) + '</td>' +
189
+ '</tr>';
190
+ }).join('');
191
+ }
192
+ document.getElementById('cur-rows').innerHTML = rowsHtml();
193
+ document.getElementById('all-rows').innerHTML = rowsHtml();
194
+ bindRepoRows();
195
+
196
+ var stamp = d.updatedAt ? 'Updated ' + new Date(d.updatedAt).toLocaleTimeString() : '';
197
+ document.getElementById('cur-updated').textContent = stamp;
198
+ document.getElementById('all-updated').textContent = stamp;
199
+ document.getElementById('sm-updated').textContent = stamp;
200
+ }
201
+
202
+ // Per-repo detail modal ---------------------------------------------------
203
+ var detailEl = document.getElementById('repo-detail');
204
+ var indexCache = { repos: [] };
205
+ function openRepoDetail(root) {
206
+ var r = null;
207
+ for (var i = 0; i < indexCache.repos.length; i++) {
208
+ if (indexCache.repos[i].repoRoot === root) { r = indexCache.repos[i]; break; }
209
+ }
210
+ if (!r) return;
211
+ document.getElementById('rd-name').textContent = r.displayName || r.repoRoot;
212
+ document.getElementById('rd-path').textContent = r.repoRoot;
213
+ document.getElementById('rd-model').textContent = r.modelName || '—';
214
+ document.getElementById('rd-provider').textContent = r.providerName || (r.provider || '—');
215
+ document.getElementById('rd-cp').textContent = (r.checkpointCount || 0).toLocaleString();
216
+ document.getElementById('rd-saved').textContent = (r.tokensSaved || 0).toLocaleString();
217
+ document.getElementById('rd-bytes').textContent = fmtBytesTop(r.compressedOriginalBytes);
218
+ document.getElementById('rd-when').textContent = r.lastCompactedAt ? new Date(r.lastCompactedAt).toLocaleString() : '—';
219
+ detailEl.classList.add('open');
220
+ }
221
+ document.getElementById('rd-close').addEventListener('click', function() { detailEl.classList.remove('open'); });
222
+ detailEl.addEventListener('click', function(e) { if (e.target === detailEl) detailEl.classList.remove('open'); });
223
+ function bindRepoRows() {
224
+ var rows = document.querySelectorAll('.repo-link');
225
+ for (var i = 0; i < rows.length; i++) {
226
+ rows[i].addEventListener('click', function() { openRepoDetail(this.getAttribute('data-repo')); });
227
+ }
228
+ }
229
+ function pollIndex() {
230
+ fetch('/api/index').then(function(r) { return r.json(); }).then(function(d) { // guardrails-allow PREVENT-PI-004: dashboard browser-side fetch (localhost)
231
+ indexCache = d && d.repos ? d : indexCache;
232
+ renderIndex(d);
233
+ }).catch(function() {});
234
+ }
235
+ pollIndex();
236
+ setInterval(pollIndex, 5000);
237
+
238
+ // --- Tab switching ------------------------------------------------------
239
+ var tabs = document.querySelectorAll('.tab');
240
+ var panels = { current: 'panel-current', all: 'panel-all', summary: 'panel-summary', highscore: 'panel-highscore' };
241
+ for (var i = 0; i < tabs.length; i++) {
242
+ tabs[i].addEventListener('click', function() {
243
+ var name = this.getAttribute('data-tab');
244
+ for (var j = 0; j < tabs.length; j++) tabs[j].classList.remove('active');
245
+ this.classList.add('active');
246
+ for (var k in panels) {
247
+ if (Object.prototype.hasOwnProperty.call(panels, k)) {
248
+ var el = document.getElementById(panels[k]);
249
+ if (el) el.classList.toggle('active', k === name);
250
+ }
251
+ }
252
+ if (name === 'all' || name === 'summary') pollIndex();
253
+ });
254
+ }
255
+ })();
256
+ </script>
257
+ </body>
258
+ </html>`;
259
+ }