myrlin-workbook 0.9.4 → 0.9.5
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/package.json +1 -1
- package/src/web/public/app.js +29 -30
- package/src/web/server.js +51 -0
package/package.json
CHANGED
package/src/web/public/app.js
CHANGED
|
@@ -7740,13 +7740,8 @@ class CWMApp {
|
|
|
7740
7740
|
|
|
7741
7741
|
list.innerHTML = html;
|
|
7742
7742
|
|
|
7743
|
-
//
|
|
7744
|
-
|
|
7745
|
-
.filter(s => s.status === 'running' || s.status === 'idle')
|
|
7746
|
-
.map(s => s.id);
|
|
7747
|
-
if (visibleSessionIds.length > 0) {
|
|
7748
|
-
this._fetchSessionCostsAsync(visibleSessionIds);
|
|
7749
|
-
}
|
|
7743
|
+
// Fetch all session costs in a single batch request (non-blocking)
|
|
7744
|
+
this._fetchSessionCostsAsync();
|
|
7750
7745
|
|
|
7751
7746
|
|
|
7752
7747
|
this.els.workspaceCount.textContent = `${workspaces.length} project${workspaces.length !== 1 ? 's' : ''}`;
|
|
@@ -14745,30 +14740,34 @@ class CWMApp {
|
|
|
14745
14740
|
return null;
|
|
14746
14741
|
}
|
|
14747
14742
|
|
|
14748
|
-
|
|
14743
|
+
/**
|
|
14744
|
+
* Fetch costs for all sessions in a single batch request instead of N+1
|
|
14745
|
+
* individual requests. Results are cached for 5 minutes. Only re-renders
|
|
14746
|
+
* the sidebar once after all costs are received.
|
|
14747
|
+
*/
|
|
14748
|
+
_fetchSessionCostsAsync() {
|
|
14749
14749
|
if (!this._costCache) this._costCache = {};
|
|
14750
|
-
if
|
|
14751
|
-
|
|
14752
|
-
|
|
14753
|
-
|
|
14754
|
-
|
|
14755
|
-
|
|
14756
|
-
|
|
14757
|
-
|
|
14758
|
-
|
|
14759
|
-
|
|
14760
|
-
|
|
14761
|
-
|
|
14762
|
-
|
|
14763
|
-
this._costCache[sid] = { cost, ts: Date.now() };
|
|
14764
|
-
|
|
14765
|
-
|
|
14766
|
-
|
|
14767
|
-
}
|
|
14768
|
-
|
|
14769
|
-
|
|
14770
|
-
|
|
14771
|
-
});
|
|
14750
|
+
// Skip if a batch fetch is already in flight or cache is fresh
|
|
14751
|
+
if (this._costBatchInFlight) return;
|
|
14752
|
+
if (this._costBatchTs && (Date.now() - this._costBatchTs < 300000)) return;
|
|
14753
|
+
|
|
14754
|
+
this._costBatchInFlight = true;
|
|
14755
|
+
this.api('GET', '/api/cost/batch').then(data => {
|
|
14756
|
+
this._costBatchInFlight = false;
|
|
14757
|
+
this._costBatchTs = Date.now();
|
|
14758
|
+
if (data && data.costs) {
|
|
14759
|
+
let changed = false;
|
|
14760
|
+
for (const [sid, entry] of Object.entries(data.costs)) {
|
|
14761
|
+
const prev = this._costCache[sid];
|
|
14762
|
+
if (!prev || prev.cost !== entry.cost) changed = true;
|
|
14763
|
+
this._costCache[sid] = { cost: entry.cost, ts: Date.now() };
|
|
14764
|
+
}
|
|
14765
|
+
// Only re-render if any cost value actually changed
|
|
14766
|
+
if (changed) this.renderWorkspaces();
|
|
14767
|
+
}
|
|
14768
|
+
}).catch(() => {
|
|
14769
|
+
this._costBatchInFlight = false;
|
|
14770
|
+
this._costBatchTs = Date.now();
|
|
14772
14771
|
});
|
|
14773
14772
|
}
|
|
14774
14773
|
|
package/src/web/server.js
CHANGED
|
@@ -2444,6 +2444,57 @@ app.get('/api/sessions/:id/cost', requireAuth, (req, res) => {
|
|
|
2444
2444
|
}
|
|
2445
2445
|
});
|
|
2446
2446
|
|
|
2447
|
+
/**
|
|
2448
|
+
* GET /api/cost/batch
|
|
2449
|
+
* Returns cost totals for all sessions in a single response, avoiding N+1 requests.
|
|
2450
|
+
* Each entry includes sessionId, totalCost, and lastActive for sidebar badge rendering.
|
|
2451
|
+
* Uses the same cache as per-session cost endpoints.
|
|
2452
|
+
*/
|
|
2453
|
+
app.get('/api/cost/batch', requireAuth, (req, res) => {
|
|
2454
|
+
try {
|
|
2455
|
+
const store = getStore();
|
|
2456
|
+
const allWorkspaces = store.getAllWorkspacesList();
|
|
2457
|
+
const costs = {};
|
|
2458
|
+
|
|
2459
|
+
for (const workspace of allWorkspaces) {
|
|
2460
|
+
const sessions = store.getWorkspaceSessions(workspace.id);
|
|
2461
|
+
for (const session of sessions) {
|
|
2462
|
+
const resumeSessionId = session.resumeSessionId;
|
|
2463
|
+
if (!resumeSessionId) continue;
|
|
2464
|
+
const jsonlPath = findJsonlFile(resumeSessionId);
|
|
2465
|
+
if (!jsonlPath) continue;
|
|
2466
|
+
|
|
2467
|
+
try {
|
|
2468
|
+
const stat = fs.statSync(jsonlPath);
|
|
2469
|
+
if (stat.size >= 500 * 1024 * 1024) continue;
|
|
2470
|
+
const mtimeMs = stat.mtimeMs;
|
|
2471
|
+
const cached = _costCache.get(resumeSessionId);
|
|
2472
|
+
const now = Date.now();
|
|
2473
|
+
let costData;
|
|
2474
|
+
|
|
2475
|
+
if (cached && cached.mtimeMs === mtimeMs && (now - cached.timestamp) < COST_CACHE_TTL) {
|
|
2476
|
+
costData = cached.result;
|
|
2477
|
+
} else {
|
|
2478
|
+
costData = calculateSessionCost(jsonlPath);
|
|
2479
|
+
const result = { sessionId: session.id, resumeSessionId, ...costData };
|
|
2480
|
+
_costCache.set(resumeSessionId, { mtimeMs, timestamp: now, result });
|
|
2481
|
+
costData = result;
|
|
2482
|
+
}
|
|
2483
|
+
|
|
2484
|
+
costs[session.id] = {
|
|
2485
|
+
cost: costData.cost ? costData.cost.total : 0,
|
|
2486
|
+
lastActive: costData.lastMessage || session.lastActive || null,
|
|
2487
|
+
};
|
|
2488
|
+
} catch (_) {}
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
return res.json({ costs });
|
|
2493
|
+
} catch (err) {
|
|
2494
|
+
return res.status(500).json({ error: 'Batch cost failed: ' + err.message });
|
|
2495
|
+
}
|
|
2496
|
+
});
|
|
2497
|
+
|
|
2447
2498
|
/**
|
|
2448
2499
|
* GET /api/quota-overview
|
|
2449
2500
|
* Returns all sessions ranked by context window size (heaviness).
|