myrlin-workbook 0.9.4 → 0.9.6
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 +38 -37
- package/src/web/server.js +51 -0
package/package.json
CHANGED
package/src/web/public/app.js
CHANGED
|
@@ -1820,7 +1820,7 @@ class CWMApp {
|
|
|
1820
1820
|
this.showApp();
|
|
1821
1821
|
this.initDragAndDrop();
|
|
1822
1822
|
this.initTerminalResize();
|
|
1823
|
-
this.initTerminalGroups();
|
|
1823
|
+
await this.initTerminalGroups();
|
|
1824
1824
|
this.initTerminalPaneSwipe();
|
|
1825
1825
|
this.initNotesEditor();
|
|
1826
1826
|
this.initAIInsights();
|
|
@@ -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' : ''}`;
|
|
@@ -11526,15 +11521,16 @@ class CWMApp {
|
|
|
11526
11521
|
PHASE 4: TERMINAL TAB GROUPS
|
|
11527
11522
|
═══════════════════════════════════════════════════════════ */
|
|
11528
11523
|
|
|
11529
|
-
initTerminalGroups() {
|
|
11524
|
+
async initTerminalGroups() {
|
|
11530
11525
|
// Load layout from server
|
|
11531
11526
|
this._tabGroups = [];
|
|
11532
11527
|
this._tabFolders = []; // Tab group folders: { id, name, color, collapsed }
|
|
11533
11528
|
this._activeGroupId = null;
|
|
11534
11529
|
this._layoutSaveTimer = null;
|
|
11530
|
+
this._layoutRestored = false;
|
|
11535
11531
|
|
|
11536
|
-
// Load saved layout
|
|
11537
|
-
this.loadTerminalLayout();
|
|
11532
|
+
// Load saved layout (must complete before SSE or other init touches panes)
|
|
11533
|
+
await this.loadTerminalLayout();
|
|
11538
11534
|
}
|
|
11539
11535
|
|
|
11540
11536
|
async loadTerminalLayout() {
|
|
@@ -11562,11 +11558,12 @@ class CWMApp {
|
|
|
11562
11558
|
const group = this._tabGroups.find(g => g.id === this._activeGroupId);
|
|
11563
11559
|
if (group && group.panes && group.panes.length > 0) {
|
|
11564
11560
|
group.panes.forEach(p => {
|
|
11565
|
-
if (p.sessionId) {
|
|
11561
|
+
if (p.sessionId && !this.terminalPanes[p.slot]) {
|
|
11566
11562
|
this.openTerminalInPane(p.slot, p.sessionId, p.sessionName || 'Terminal', p.spawnOpts || {});
|
|
11567
11563
|
}
|
|
11568
11564
|
});
|
|
11569
11565
|
}
|
|
11566
|
+
this._layoutRestored = true;
|
|
11570
11567
|
}
|
|
11571
11568
|
|
|
11572
11569
|
/**
|
|
@@ -11964,11 +11961,11 @@ class CWMApp {
|
|
|
11964
11961
|
}
|
|
11965
11962
|
});
|
|
11966
11963
|
} else {
|
|
11967
|
-
// No cache
|
|
11964
|
+
// No cache, create fresh connections (first time opening this group)
|
|
11968
11965
|
const group = this._tabGroups.find(g => g.id === groupId);
|
|
11969
11966
|
if (group && group.panes) {
|
|
11970
11967
|
group.panes.forEach(p => {
|
|
11971
|
-
if (p.sessionId) {
|
|
11968
|
+
if (p.sessionId && !this.terminalPanes[p.slot]) {
|
|
11972
11969
|
this.openTerminalInPane(p.slot, p.sessionId, p.sessionName || 'Terminal', p.spawnOpts || {});
|
|
11973
11970
|
}
|
|
11974
11971
|
});
|
|
@@ -14745,30 +14742,34 @@ class CWMApp {
|
|
|
14745
14742
|
return null;
|
|
14746
14743
|
}
|
|
14747
14744
|
|
|
14748
|
-
|
|
14745
|
+
/**
|
|
14746
|
+
* Fetch costs for all sessions in a single batch request instead of N+1
|
|
14747
|
+
* individual requests. Results are cached for 5 minutes. Only re-renders
|
|
14748
|
+
* the sidebar once after all costs are received.
|
|
14749
|
+
*/
|
|
14750
|
+
_fetchSessionCostsAsync() {
|
|
14749
14751
|
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
|
-
});
|
|
14752
|
+
// Skip if a batch fetch is already in flight or cache is fresh
|
|
14753
|
+
if (this._costBatchInFlight) return;
|
|
14754
|
+
if (this._costBatchTs && (Date.now() - this._costBatchTs < 300000)) return;
|
|
14755
|
+
|
|
14756
|
+
this._costBatchInFlight = true;
|
|
14757
|
+
this.api('GET', '/api/cost/batch').then(data => {
|
|
14758
|
+
this._costBatchInFlight = false;
|
|
14759
|
+
this._costBatchTs = Date.now();
|
|
14760
|
+
if (data && data.costs) {
|
|
14761
|
+
let changed = false;
|
|
14762
|
+
for (const [sid, entry] of Object.entries(data.costs)) {
|
|
14763
|
+
const prev = this._costCache[sid];
|
|
14764
|
+
if (!prev || prev.cost !== entry.cost) changed = true;
|
|
14765
|
+
this._costCache[sid] = { cost: entry.cost, ts: Date.now() };
|
|
14766
|
+
}
|
|
14767
|
+
// Only re-render if any cost value actually changed
|
|
14768
|
+
if (changed) this.renderWorkspaces();
|
|
14769
|
+
}
|
|
14770
|
+
}).catch(() => {
|
|
14771
|
+
this._costBatchInFlight = false;
|
|
14772
|
+
this._costBatchTs = Date.now();
|
|
14772
14773
|
});
|
|
14773
14774
|
}
|
|
14774
14775
|
|
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).
|