pi-mega-compact 0.4.17 → 0.4.19

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.
@@ -13,7 +13,81 @@
13
13
 
14
14
  import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
15
15
  import { existsSync, mkdirSync, readFileSync, unlinkSync, watch, writeFileSync } from "node:fs";
16
+ import { homedir } from "node:os";
16
17
  import { join } from "node:path";
18
+ import Database from "better-sqlite3";
19
+
20
+ // --- Multi-repo index (Phase 5b) ------------------------------------------------
21
+ // The extension writes a machine-wide repo registry into a single SQLite DB
22
+ // (<indexDir>/index.sqlite) as the concurrency-safe write path; the dashboard
23
+ // reads that table directly (one read-only connection, opened per request so a
24
+ // concurrent writer's WAL never blocks the request). All registry data lives in
25
+ // SQLite (the project's one-store invariant) — there is no JSON mirror. Same
26
+ // index-dir resolution as src/store/sqlite.ts getIndexDir().
27
+ function getIndexDir(): string {
28
+ const override = process.env.MEGACOMPACT_INDEX_DIR;
29
+ if (override && override.trim() !== "") return override;
30
+ try {
31
+ return join(homedir(), ".mega-compact-index");
32
+ } catch {
33
+ return join("/tmp", ".mega-compact-index");
34
+ }
35
+ }
36
+
37
+ interface IndexRepo {
38
+ repoRoot: string;
39
+ displayName: string;
40
+ checkpointCount: number;
41
+ tokensSaved: number;
42
+ compressedOriginalBytes: number;
43
+ lastCompactedAt: number | null;
44
+ provider: string | null;
45
+ providerName: string | null;
46
+ modelName: string | null;
47
+ inputRate: number | null;
48
+ outputRate: number | null;
49
+ lastSeen: number;
50
+ }
51
+
52
+ /** Read the machine-wide repo registry from SQLite (read-only, single shot). */
53
+ function readIndex(): { updatedAt: string; summary: unknown; repos: unknown[] } | null {
54
+ const indexPath = join(getIndexDir(), "index.sqlite");
55
+ if (!existsSync(indexPath)) return null;
56
+ let db: Database.Database | undefined;
57
+ try {
58
+ // Read-only + immutable WAL so a concurrent writer's WAL never blocks us.
59
+ db = new Database(indexPath, { readonly: true, fileMustExist: true });
60
+ db.pragma("journal_mode = WAL");
61
+ const rows = db
62
+ .prepare("SELECT * FROM repo_registry ORDER BY last_seen DESC")
63
+ .all() as Record<string, unknown>[];
64
+ const repos: IndexRepo[] = rows.map((r) => ({
65
+ repoRoot: String(r.repo_root ?? ""),
66
+ displayName: String(r.display_name ?? ""),
67
+ checkpointCount: Number(r.checkpoint_count ?? 0),
68
+ tokensSaved: Number(r.tokens_saved ?? 0),
69
+ compressedOriginalBytes: Number(r.compressed_original_bytes ?? 0),
70
+ lastCompactedAt: (r.last_compacted_at as number | null) ?? null,
71
+ provider: (r.provider as string | null) ?? null,
72
+ providerName: (r.provider_name as string | null) ?? null,
73
+ modelName: (r.model_name as string | null) ?? null,
74
+ inputRate: (r.input_rate as number | null) ?? null,
75
+ outputRate: (r.output_rate as number | null) ?? null,
76
+ lastSeen: Number(r.last_seen ?? 0),
77
+ }));
78
+ const summary = {
79
+ totalRepos: repos.length,
80
+ totalCheckpoints: repos.reduce((a, r) => a + r.checkpointCount, 0),
81
+ totalTokensSaved: repos.reduce((a, r) => a + r.tokensSaved, 0),
82
+ totalCompressedOriginalBytes: repos.reduce((a, r) => a + r.compressedOriginalBytes, 0),
83
+ };
84
+ return { updatedAt: new Date().toISOString(), summary, repos };
85
+ } catch {
86
+ return null;
87
+ } finally {
88
+ try { db?.close(); } catch { /* ignore */ }
89
+ }
90
+ }
17
91
 
18
92
  // ---------------------------------------------------------------------------
19
93
  // Types
@@ -74,6 +148,19 @@ interface Snapshot {
74
148
  dedupCollapsed: number;
75
149
  storageDedupRate: number;
76
150
  };
151
+ integrity: {
152
+ regionsRetained: number;
153
+ compressedOriginalBytes: number;
154
+ duplicatesCollapsed: number;
155
+ bytesPermanentlyDeleted: number;
156
+ };
157
+ model?: {
158
+ name: string;
159
+ provider: string;
160
+ providerName: string;
161
+ inputRate: number;
162
+ outputRate: number;
163
+ };
77
164
  }
78
165
 
79
166
  // ---------------------------------------------------------------------------
@@ -97,6 +184,7 @@ function readSnapshot(snapshotPath: string) {
97
184
  crew: { activeAgents: 0, currentTurn: 0 },
98
185
  repo: { checkpointCount: 0, totalTokenEstimate: 0, originalTokens: 0, tokensSaved: 0, sessionCount: 0, dedupAttempts: 0, dedupCollapsed: 0, storageDedupRate: 0 },
99
186
  integrity: { regionsRetained: 0, compressedOriginalBytes: 0, duplicatesCollapsed: 0, bytesPermanentlyDeleted: 0 },
187
+ model: undefined,
100
188
  } as Snapshot;
101
189
  }
102
190
  }
@@ -175,14 +263,54 @@ function dashboardHtml(tierName: string): string {
175
263
  .updated { font-size: 11px; color: #484f58; margin-top: 16px; text-align: right; }
176
264
  .empty { color: #484f58; font-style: italic; font-size: 13px; padding: 8px 0; }
177
265
  .offline-banner { background: #f8514922; border: 1px solid #f85149; border-radius: 6px; padding: 10px 16px; margin-bottom: 16px; font-size: 13px; color: #f85149; display: none; }
266
+ .tabs { display: flex; gap: 8px; margin-bottom: 20px; }
267
+ .tab { background: #161b22; color: #8b949e; border: 1px solid #30363d; border-radius: 6px; padding: 8px 16px; font-size: 13px; font-weight: 600; cursor: pointer; transition: all .15s ease; }
268
+ .tab:hover { color: #c9d1d9; border-color: #484f58; }
269
+ .tab.active { background: #1f6feb; color: #fff; border-color: #1f6feb; }
270
+ .tab-panel { display: none; }
271
+ .tab-panel.active { display: block; }
272
+ .summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 16px; margin-bottom: 20px; }
273
+ .summary-card { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 16px; }
274
+ .summary-card .num { font-size: 24px; font-weight: 700; color: #f0f6fc; }
275
+ .summary-card .lbl { font-size: 12px; color: #8b949e; text-transform: uppercase; letter-spacing: .5px; margin-top: 4px; }
276
+ table.repos { width: 100%; border-collapse: collapse; background: #161b22; border: 1px solid #30363d; border-radius: 8px; overflow: hidden; }
277
+ table.repos th, table.repos td { text-align: left; padding: 10px 14px; font-size: 13px; border-bottom: 1px solid #21262d; }
278
+ table.repos th { color: #8b949e; text-transform: uppercase; letter-spacing: .5px; font-size: 11px; background: #0d1117; }
279
+ table.repos td.num { font-family: monospace; color: #f0f6fc; text-align: right; }
280
+ table.repos tr:last-child td { border-bottom: none; }
281
+ table.repos tr:hover td { background: #1c2128; }
282
+ .repo-model { color: #a371f7; }
283
+ .repo-none { color: #484f58; font-style: italic; }
284
+ .updated { font-size: 11px; color: #484f58; margin-top: 16px; text-align: right; }
285
+ .model-pill { background: #6e40c9; color: #fff; font-size: 11px; font-weight: 700; padding: 2px 8px; border-radius: 10px; text-transform: uppercase; letter-spacing: .5px; }
286
+ .card.cost h2 { color: #a371f7; }
287
+ .cost-usd { font-size: 22px; font-weight: 700; color: #3fb950; }
288
+ .cost-sub { font-size: 12px; color: #8b949e; margin-top: 4px; }
289
+ .repo-link { cursor: pointer; }
290
+ .repo-link:hover td { color: #58a6ff; }
291
+ .repo-detail { position: fixed; inset: 0; background: rgba(0,0,0,.6); display: none; align-items: center; justify-content: center; z-index: 50; }
292
+ .repo-detail.open { display: flex; }
293
+ .repo-detail-box { background: #161b22; border: 1px solid #30363d; border-radius: 10px; padding: 24px; width: 560px; max-width: 92vw; max-height: 86vh; overflow-y: auto; }
294
+ .repo-detail-box h2 { font-size: 14px; color: #f0f6fc; margin-bottom: 14px; display: flex; justify-content: space-between; align-items: center; }
295
+ .repo-close { cursor: pointer; color: #8b949e; font-size: 20px; line-height: 1; border: none; background: none; padding: 0 4px; }
296
+ .repo-close:hover { color: #f0f6fc; }
297
+ .repo-path { font-size: 11px; color: #484f58; word-break: break-all; margin: -8px 0 12px; }
178
298
  </style>
179
299
  </head>
180
300
  <body>
181
301
 
182
302
  <div class="offline-banner" id="offline-banner">Dashboard data unavailable — waiting for a pi session to write snapshot...</div>
183
303
 
184
- <h1><span>mega-compact</span><span class="tier">${tierName}</span></h1>
304
+ <h1><span>mega-compact</span><span class="tier">${tierName}</span><span class="model-pill" id="hdr-model">—</span></h1>
185
305
 
306
+ <nav class="tabs">
307
+ <button class="tab active" data-tab="current">Current repo</button>
308
+ <button class="tab" data-tab="all">All repos</button>
309
+ <button class="tab" data-tab="summary">Summary</button>
310
+ </nav>
311
+
312
+ <!-- Current repo (existing single-repo view) -->
313
+ <div class="tab-panel" id="panel-current">
186
314
  <div class="grid">
187
315
  <div class="card">
188
316
  <h2>Context Window</h2>
@@ -242,6 +370,17 @@ function dashboardHtml(tierName: string): string {
242
370
  <span class="label">Anchor</span><span class="value" id="cf-anchor">—</span>
243
371
  </div>
244
372
  </div>
373
+ <div class="card cost">
374
+ <h2>💰 Model &amp; Cost Savings</h2>
375
+ <div class="cost-usd" id="cost-usd">≈ $0.00 saved</div>
376
+ <div class="cost-sub" id="cost-windows">0 context-windows extended</div>
377
+ <div class="stat-grid" style="margin-top:12px">
378
+ <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>
379
+ <span class="label" title="The provider serving the model.">Provider</span><span class="value" id="md-provider">—</span>
380
+ <span class="label" title="USD per input token, from the model's pricing.">Input Rate</span><span class="value" id="md-input">—</span>
381
+ <span class="label" title="USD per output token, from the model's pricing.">Output Rate</span><span class="value" id="md-output">—</span>
382
+ </div>
383
+ </div>
245
384
  <div class="card">
246
385
  <h2>Crew / Agents</h2>
247
386
  <div class="stat-grid">
@@ -269,7 +408,67 @@ function dashboardHtml(tierName: string): string {
269
408
  <div class="events-wrap" id="events"><div class="empty">connecting…</div></div>
270
409
  </div>
271
410
 
411
+ <h2 style="margin-top:24px;font-size:13px;color:#8b949e;text-transform:uppercase;letter-spacing:.5px">All Repositories</h2>
412
+ <table class="repos">
413
+ <thead>
414
+ <tr>
415
+ <th>Repo</th><th>Model</th>
416
+ <th style="text-align:right">Checkpoints</th>
417
+ <th style="text-align:right">Tokens Saved</th>
418
+ <th style="text-align:right">Retained</th>
419
+ <th style="text-align:right">Last Compacted</th>
420
+ </tr>
421
+ </thead>
422
+ <tbody id="cur-rows"><tr><td colspan="6" class="repo-none">loading…</td></tr></tbody>
423
+ </table>
424
+ <div class="updated" id="cur-updated"></div>
425
+
272
426
  <div class="updated" id="updated"></div>
427
+ </div><!-- /panel-current -->
428
+
429
+ <!-- Per-repo detail modal -->
430
+ <div class="repo-detail" id="repo-detail">
431
+ <div class="repo-detail-box">
432
+ <h2><span id="rd-name">Repo</span><button class="repo-close" id="rd-close" title="Close">×</button></h2>
433
+ <div class="repo-path" id="rd-path"></div>
434
+ <div class="stat-grid">
435
+ <span class="label">Model</span><span class="value" id="rd-model">—</span>
436
+ <span class="label">Checkpoints</span><span class="value" id="rd-cp">0</span>
437
+ <span class="label">Tokens Saved</span><span class="value" id="rd-saved">0</span>
438
+ <span class="label">Compressed-Original</span><span class="value" id="rd-bytes">0 B</span>
439
+ <span class="label">Last Compacted</span><span class="value" id="rd-when">—</span>
440
+ <span class="label">Provider</span><span class="value" id="rd-provider">—</span>
441
+ </div>
442
+ </div>
443
+ </div>
444
+
445
+ <!-- All repos (machine-wide registry from index.sqlite) -->
446
+ <div class="tab-panel" id="panel-all">
447
+ <table class="repos">
448
+ <thead>
449
+ <tr>
450
+ <th>Repo</th><th>Model</th>
451
+ <th style="text-align:right">Checkpoints</th>
452
+ <th style="text-align:right">Tokens Saved</th>
453
+ <th style="text-align:right">Retained</th>
454
+ <th style="text-align:right">Last Compacted</th>
455
+ </tr>
456
+ </thead>
457
+ <tbody id="all-rows"><tr><td colspan="6" class="repo-none">loading…</td></tr></tbody>
458
+ </table>
459
+ <div class="updated" id="all-updated"></div>
460
+ </div>
461
+
462
+ <!-- Summary (aggregate across all repos) -->
463
+ <div class="tab-panel" id="panel-summary">
464
+ <div class="summary-grid">
465
+ <div class="summary-card"><div class="num" id="sm-repos">0</div><div class="lbl">Repositories</div></div>
466
+ <div class="summary-card"><div class="num" id="sm-checkpoints">0</div><div class="lbl">Total Checkpoints</div></div>
467
+ <div class="summary-card"><div class="num" id="sm-saved">0</div><div class="lbl">Total Tokens Saved</div></div>
468
+ <div class="summary-card"><div class="num" id="sm-bytes">0 B</div><div class="lbl">Compressed-Original</div></div>
469
+ </div>
470
+ <div class="updated" id="sm-updated"></div>
471
+ </div>
273
472
 
274
473
  <script>
275
474
  (function() {
@@ -349,6 +548,24 @@ function dashboardHtml(tierName: string): string {
349
548
  document.getElementById('cf-auto').textContent = d.config.auto ? 'enabled' : 'disabled';
350
549
  document.getElementById('cf-anchor').textContent = d.config.anchorUserMessages;
351
550
 
551
+ // --- Active model + cost savings (same calc as /mega-status) ---------------
552
+ var model = d.model;
553
+ document.getElementById('hdr-model').textContent = model && model.name ? model.name : '—';
554
+ document.getElementById('md-name').textContent = model && model.name ? model.name : '—';
555
+ document.getElementById('md-provider').textContent = model && model.providerName ? model.providerName : (model && model.provider ? model.provider : '—');
556
+ document.getElementById('md-input').textContent = model && model.inputRate ? '$' + (model.inputRate).toFixed(6) : '—';
557
+ document.getElementById('md-output').textContent = model && model.outputRate ? '$' + (model.outputRate).toFixed(6) : '—';
558
+ if (model && model.inputRate && repo.tokensSaved > 0) {
559
+ var usd = (repo.tokensSaved * model.inputRate);
560
+ var win = d.context.contextWindow || 0;
561
+ var windows = win > 0 ? (repo.tokensSaved / win).toFixed(1) : '0';
562
+ document.getElementById('cost-usd').textContent = '≈ $' + usd.toFixed(4) + ' saved';
563
+ document.getElementById('cost-windows').textContent = windows + ' context-windows extended';
564
+ } else {
565
+ document.getElementById('cost-usd').textContent = '≈ $0.00 saved';
566
+ document.getElementById('cost-windows').textContent = '0 context-windows extended';
567
+ }
568
+
352
569
  document.getElementById('updated').textContent = 'Updated ' + new Date(d.updatedAt).toLocaleTimeString();
353
570
  }
354
571
 
@@ -395,6 +612,105 @@ function dashboardHtml(tierName: string): string {
395
612
  };
396
613
  }
397
614
  connectSSE();
615
+
616
+ // --- Multi-repo (index.sqlite via /api/index) ---------------------------
617
+ function fmtBytesTop(b) {
618
+ b = b || 0;
619
+ if (b >= 1048576) return (b / 1048576).toFixed(1) + ' MiB';
620
+ if (b >= 1024) return (b / 1024).toFixed(1) + ' KiB';
621
+ return b + ' B';
622
+ }
623
+ function renderIndex(d) {
624
+ d = d || { updatedAt: null, summary: null, repos: [] };
625
+ var repos = d.repos || [];
626
+ var s = d.summary || { totalRepos: 0, totalCheckpoints: 0, totalTokensSaved: 0, totalCompressedOriginalBytes: 0 };
627
+ document.getElementById('sm-repos').textContent = (s.totalRepos || 0).toLocaleString();
628
+ document.getElementById('sm-checkpoints').textContent = (s.totalCheckpoints || 0).toLocaleString();
629
+ document.getElementById('sm-saved').textContent = (s.totalTokensSaved || 0).toLocaleString();
630
+ document.getElementById('sm-bytes').textContent = fmtBytesTop(s.totalCompressedOriginalBytes);
631
+
632
+ // Shared clickable-row renderer for both the in-current table and the
633
+ // All-repos tab — each row opens the per-repo detail modal.
634
+ function rowsHtml() {
635
+ if (!repos.length) return '<tr><td colspan="6" class="repo-none">No repositories registered yet.</td></tr>';
636
+ return repos.map(function(r) {
637
+ var model = r.modelName
638
+ ? '<span class="repo-model">' + sanitize(r.modelName) + '</span>'
639
+ : '<span class="repo-none">—</span>';
640
+ var when = r.lastCompactedAt ? new Date(r.lastCompactedAt).toLocaleString() : '—';
641
+ return '<tr class="repo-link" data-repo="' + sanitize(r.repoRoot) + '">' +
642
+ '<td title="' + sanitize(r.repoRoot) + '">' + sanitize(r.displayName || r.repoRoot) + '</td>' +
643
+ '<td>' + model + '</td>' +
644
+ '<td class="num">' + (r.checkpointCount || 0).toLocaleString() + '</td>' +
645
+ '<td class="num">' + (r.tokensSaved || 0).toLocaleString() + '</td>' +
646
+ '<td class="num">' + fmtBytesTop(r.compressedOriginalBytes) + '</td>' +
647
+ '<td class="num">' + sanitize(when) + '</td>' +
648
+ '</tr>';
649
+ }).join('');
650
+ }
651
+ document.getElementById('cur-rows').innerHTML = rowsHtml();
652
+ document.getElementById('all-rows').innerHTML = rowsHtml();
653
+ bindRepoRows();
654
+
655
+ var stamp = d.updatedAt ? 'Updated ' + new Date(d.updatedAt).toLocaleTimeString() : '';
656
+ document.getElementById('cur-updated').textContent = stamp;
657
+ document.getElementById('all-updated').textContent = stamp;
658
+ document.getElementById('sm-updated').textContent = stamp;
659
+ }
660
+
661
+ // Per-repo detail modal ---------------------------------------------------
662
+ var detailEl = document.getElementById('repo-detail');
663
+ var indexCache = { repos: [] };
664
+ function openRepoDetail(root) {
665
+ var r = null;
666
+ for (var i = 0; i < indexCache.repos.length; i++) {
667
+ if (indexCache.repos[i].repoRoot === root) { r = indexCache.repos[i]; break; }
668
+ }
669
+ if (!r) return;
670
+ document.getElementById('rd-name').textContent = r.displayName || r.repoRoot;
671
+ document.getElementById('rd-path').textContent = r.repoRoot;
672
+ document.getElementById('rd-model').textContent = r.modelName || '—';
673
+ document.getElementById('rd-provider').textContent = r.providerName || (r.provider || '—');
674
+ document.getElementById('rd-cp').textContent = (r.checkpointCount || 0).toLocaleString();
675
+ document.getElementById('rd-saved').textContent = (r.tokensSaved || 0).toLocaleString();
676
+ document.getElementById('rd-bytes').textContent = fmtBytesTop(r.compressedOriginalBytes);
677
+ document.getElementById('rd-when').textContent = r.lastCompactedAt ? new Date(r.lastCompactedAt).toLocaleString() : '—';
678
+ detailEl.classList.add('open');
679
+ }
680
+ document.getElementById('rd-close').addEventListener('click', function() { detailEl.classList.remove('open'); });
681
+ detailEl.addEventListener('click', function(e) { if (e.target === detailEl) detailEl.classList.remove('open'); });
682
+ function bindRepoRows() {
683
+ var rows = document.querySelectorAll('.repo-link');
684
+ for (var i = 0; i < rows.length; i++) {
685
+ rows[i].addEventListener('click', function() { openRepoDetail(this.getAttribute('data-repo')); });
686
+ }
687
+ }
688
+ function pollIndex() {
689
+ fetch('/api/index').then(function(r) { return r.json(); }).then(function(d) {
690
+ indexCache = d && d.repos ? d : indexCache;
691
+ renderIndex(d);
692
+ }).catch(function() {});
693
+ }
694
+ pollIndex();
695
+ setInterval(pollIndex, 5000);
696
+
697
+ // --- Tab switching ------------------------------------------------------
698
+ var tabs = document.querySelectorAll('.tab');
699
+ var panels = { current: 'panel-current', all: 'panel-all', summary: 'panel-summary' };
700
+ for (var i = 0; i < tabs.length; i++) {
701
+ tabs[i].addEventListener('click', function() {
702
+ var name = this.getAttribute('data-tab');
703
+ for (var j = 0; j < tabs.length; j++) tabs[j].classList.remove('active');
704
+ this.classList.add('active');
705
+ for (var k in panels) {
706
+ if (Object.prototype.hasOwnProperty.call(panels, k)) {
707
+ var el = document.getElementById(panels[k]);
708
+ if (el) el.classList.toggle('active', k === name);
709
+ }
710
+ }
711
+ if (name === 'all' || name === 'summary') pollIndex();
712
+ });
713
+ }
398
714
  })();
399
715
  </script>
400
716
  </body>
@@ -453,6 +769,15 @@ export function launchDashboardServer(stateDir: string): Promise<{ port: number;
453
769
  return;
454
770
  }
455
771
 
772
+ // Multi-repo aggregate (Phase 5b): the machine-wide repo registry read
773
+ // directly from SQLite (index.sqlite). Lets one dashboard show every repo's
774
+ // checkpoints, tokens saved, and active model. Read-only.
775
+ if (req.url === "/api/index") {
776
+ res.writeHead(200, { "Content-Type": "application/json" });
777
+ res.end(JSON.stringify(readIndex() ?? { updatedAt: null, summary: null, repos: [] }));
778
+ return;
779
+ }
780
+
456
781
  if (req.url === "/api/events") {
457
782
  res.writeHead(200, {
458
783
  "Content-Type": "text/event-stream",
@@ -82,6 +82,14 @@ export interface DashboardSnapshot {
82
82
  duplicatesCollapsed: number; // dedup duplicates (original kept on survivor)
83
83
  bytesPermanentlyDeleted: number; // ALWAYS 0 — the invariant
84
84
  };
85
+ /** Active model/provider (captured live) — shown on the current-repo card. */
86
+ model?: {
87
+ name: string; // Model.name or Model.id
88
+ provider: string; // ProviderId (Model.provider)
89
+ providerName: string; // human display name (e.g. "OpenAI")
90
+ inputRate: number; // USD per input token (Model.cost)
91
+ outputRate: number; // USD per output token (Model.cost)
92
+ };
85
93
  }
86
94
 
87
95
  export class Dashboard {
@@ -17,7 +17,7 @@ import { VectorStore } from "../src/vectorStore.js";
17
17
  import { toEngineMessages } from "../src/adapt.js";
18
18
  import { normalizeSessionId } from "../src/store.js";
19
19
  import { Logger } from "../src/log.js";
20
- import { recordModelSnapshot, type ModelSnapshot } from "../src/store/sqlite.js";
20
+ import { recordModelSnapshot, latestModelSnapshot, upsertRepoRegistry, recordRepoModel, type ModelSnapshot } from "../src/store/sqlite.js";
21
21
  import { repoStateDir, resolveRepoRoot, type MegaConfig } from "./mega-config.js";
22
22
  import { Dashboard, type DashboardSnapshot } from "./mega-dashboard.js";
23
23
 
@@ -142,6 +142,26 @@ export class MegaRuntime {
142
142
  this.store = new VectorStore({ dedupSim: this.config.dedupSim, stateDir: dir });
143
143
  this.logger = new Logger({ enabled: this.config.debug, path: join(dir, "mega-compact.log") });
144
144
  this.dashboard = new Dashboard(dir);
145
+ // Aggregate this repo into the machine-wide index so the multi-repo
146
+ // dashboard (Summary / All-repos tabs) can show it alongside every other
147
+ // repo. Best-effort + non-fatal: a read-only index dir or contention must
148
+ // never break the per-repo compaction path. Runs only on repo-switch
149
+ // (this branch), so it's infrequent — not per-context-event.
150
+ try {
151
+ const repo = this.store.repoStats();
152
+ const di = this.store.dataInvariant();
153
+ const root = key !== dir ? key : resolveRepoRoot(cwd ?? dir) ?? dir;
154
+ upsertRepoRegistry({
155
+ repoRoot: root,
156
+ displayName: root.split(/[\\/]/).filter(Boolean).pop() ?? root,
157
+ stateDir: dir,
158
+ checkpointCount: repo.checkpointCount,
159
+ tokensSaved: repo.tokensSaved,
160
+ compressedOriginalBytes: di.compressedOriginalBytes,
161
+ });
162
+ } catch {
163
+ /* non-fatal: index aggregation must not block compaction */
164
+ }
145
165
  return dir;
146
166
  }
147
167
 
@@ -153,6 +173,17 @@ export class MegaRuntime {
153
173
  const st = this.store.stats(this.rt.sessionId);
154
174
  const repo = this.store.repoStats();
155
175
  const di = this.store.dataInvariant();
176
+ // Active model/provider for the current-repo card + the multi-repo table.
177
+ const modelSnap = latestModelSnapshot(this.currentStateDir);
178
+ const model = modelSnap
179
+ ? {
180
+ name: modelSnap.modelName ?? modelSnap.modelId,
181
+ provider: modelSnap.provider,
182
+ providerName: modelSnap.providerName ?? "",
183
+ inputRate: modelSnap.inputRate,
184
+ outputRate: modelSnap.outputRate,
185
+ }
186
+ : undefined;
156
187
  const armed = this.lastCtxPercent != null && this.lastCtxPercent >= this.config.fastGatePct;
157
188
  const ready = armed && (this.lastCtxTokens ?? 0) >= this.config.thresholdTokens;
158
189
  this.dashboard.snapshot({
@@ -197,6 +228,7 @@ export class MegaRuntime {
197
228
  duplicatesCollapsed: di.duplicatesCollapsed,
198
229
  bytesPermanentlyDeleted: di.bytesPermanentlyDeleted,
199
230
  },
231
+ model,
200
232
  } as DashboardSnapshot);
201
233
 
202
234
  // Live stats widget above the editor
@@ -321,6 +353,18 @@ export class MegaRuntime {
321
353
  try {
322
354
  const repo = resolveRepoRoot(ctx.cwd) ?? this.currentStateDir;
323
355
  recordModelSnapshot(repo, snap, this.currentStateDir);
356
+ // Denormalize the active model into the machine-wide index so the
357
+ // All-repos dashboard table can show provider/model per repo without
358
+ // opening every repo's DB. Best-effort + non-fatal.
359
+ recordRepoModel(repo, {
360
+ provider: snap.provider,
361
+ providerName: snap.providerName,
362
+ modelName: snap.modelName,
363
+ inputRate: snap.inputRate,
364
+ outputRate: snap.outputRate,
365
+ stateDir: this.currentStateDir,
366
+ displayName: repo.split(/[\\/]/).filter(Boolean).pop() ?? repo,
367
+ });
324
368
  } catch { /* non-fatal: cost estimation degrades to model-in-memory only */ }
325
369
  }
326
370
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-mega-compact",
3
- "version": "0.4.17",
3
+ "version": "0.4.19",
4
4
  "description": "Layered, local, vector-backed context compressor for pi — supersede/collapse/cluster compaction with deduped inline recall.",
5
5
  "type": "module",
6
6
  "license": "BSD-2-Clause",