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.
- package/dist/extensions/dashboard-server.js +303 -1
- package/dist/extensions/mega-runtime.js +46 -1
- package/dist/src/store/sqlite.js +162 -0
- package/extensions/DASHBOARD.md +18 -5
- package/extensions/dashboard-server.ts +326 -1
- package/extensions/mega-dashboard.ts +8 -0
- package/extensions/mega-runtime.ts +45 -1
- package/package.json +1 -1
- package/src/store/sqlite.ts +214 -0
|
@@ -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 & 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.
|
|
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",
|