codedash-app 1.9.0 → 2.0.1
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/data.js +88 -0
- package/src/frontend/app.js +41 -0
- package/src/frontend/styles.css +63 -0
- package/src/server.js +7 -1
package/package.json
CHANGED
package/src/data.js
CHANGED
|
@@ -539,6 +539,93 @@ function searchFullText(query, sessions) {
|
|
|
539
539
|
|
|
540
540
|
// ── Exports ────────────────────────────────────────────────
|
|
541
541
|
|
|
542
|
+
// ── Active sessions detection ─────────────────────────────
|
|
543
|
+
|
|
544
|
+
function getActiveSessions() {
|
|
545
|
+
const active = [];
|
|
546
|
+
const sessionsDir = path.join(CLAUDE_DIR, 'sessions');
|
|
547
|
+
|
|
548
|
+
// Read ~/.claude/sessions/<PID>.json files
|
|
549
|
+
if (fs.existsSync(sessionsDir)) {
|
|
550
|
+
for (const file of fs.readdirSync(sessionsDir)) {
|
|
551
|
+
if (!file.endsWith('.json')) continue;
|
|
552
|
+
try {
|
|
553
|
+
const data = JSON.parse(fs.readFileSync(path.join(sessionsDir, file), 'utf8'));
|
|
554
|
+
const pid = data.pid;
|
|
555
|
+
if (!pid) continue;
|
|
556
|
+
|
|
557
|
+
// Check if process is alive + get CPU
|
|
558
|
+
try {
|
|
559
|
+
const psOut = execSync(`ps -p ${pid} -o pid=,%cpu=,rss=,stat= 2>/dev/null`, { encoding: 'utf8', timeout: 2000 }).trim();
|
|
560
|
+
if (!psOut) continue;
|
|
561
|
+
|
|
562
|
+
const parts = psOut.trim().split(/\s+/);
|
|
563
|
+
const cpu = parseFloat(parts[1]) || 0;
|
|
564
|
+
const rss = parseInt(parts[2]) || 0; // KB
|
|
565
|
+
const stat = parts[3] || '';
|
|
566
|
+
|
|
567
|
+
// Determine status
|
|
568
|
+
let status = 'active';
|
|
569
|
+
if (cpu < 1 && (stat.includes('S') || stat.includes('T'))) {
|
|
570
|
+
status = 'waiting'; // idle/sleeping — likely waiting for user input
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
active.push({
|
|
574
|
+
pid: pid,
|
|
575
|
+
sessionId: data.sessionId,
|
|
576
|
+
cwd: data.cwd || '',
|
|
577
|
+
startedAt: data.startedAt || 0,
|
|
578
|
+
kind: data.kind || 'interactive',
|
|
579
|
+
entrypoint: data.entrypoint || '',
|
|
580
|
+
status: status,
|
|
581
|
+
cpu: cpu,
|
|
582
|
+
memoryMB: Math.round(rss / 1024),
|
|
583
|
+
});
|
|
584
|
+
} catch {
|
|
585
|
+
// Process not found — stale file, skip
|
|
586
|
+
}
|
|
587
|
+
} catch {}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Also check Codex processes
|
|
592
|
+
try {
|
|
593
|
+
const codexPs = execSync('ps aux 2>/dev/null | grep "[c]odex" || true', { encoding: 'utf8', timeout: 2000 });
|
|
594
|
+
for (const line of codexPs.split('\n').filter(Boolean)) {
|
|
595
|
+
const parts = line.trim().split(/\s+/);
|
|
596
|
+
if (parts.length < 11) continue;
|
|
597
|
+
const pid = parseInt(parts[1]);
|
|
598
|
+
const cpu = parseFloat(parts[2]) || 0;
|
|
599
|
+
const rss = parseInt(parts[5]) || 0;
|
|
600
|
+
|
|
601
|
+
// Skip if already found via claude sessions
|
|
602
|
+
if (active.find(a => a.pid === pid)) continue;
|
|
603
|
+
|
|
604
|
+
// Try to get cwd
|
|
605
|
+
let cwd = '';
|
|
606
|
+
try {
|
|
607
|
+
const lsofOut = execSync(`lsof -d cwd -p ${pid} -Fn 2>/dev/null`, { encoding: 'utf8', timeout: 2000 });
|
|
608
|
+
const match = lsofOut.match(/\nn(\/[^\n]+)/);
|
|
609
|
+
if (match) cwd = match[1];
|
|
610
|
+
} catch {}
|
|
611
|
+
|
|
612
|
+
active.push({
|
|
613
|
+
pid: pid,
|
|
614
|
+
sessionId: '',
|
|
615
|
+
cwd: cwd,
|
|
616
|
+
startedAt: 0,
|
|
617
|
+
kind: 'codex',
|
|
618
|
+
entrypoint: 'codex',
|
|
619
|
+
status: cpu < 1 ? 'waiting' : 'active',
|
|
620
|
+
cpu: cpu,
|
|
621
|
+
memoryMB: Math.round(rss / 1024),
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
} catch {}
|
|
625
|
+
|
|
626
|
+
return active;
|
|
627
|
+
}
|
|
628
|
+
|
|
542
629
|
module.exports = {
|
|
543
630
|
loadSessions,
|
|
544
631
|
loadSessionDetail,
|
|
@@ -547,6 +634,7 @@ module.exports = {
|
|
|
547
634
|
exportSessionMarkdown,
|
|
548
635
|
getSessionPreview,
|
|
549
636
|
searchFullText,
|
|
637
|
+
getActiveSessions,
|
|
550
638
|
CLAUDE_DIR,
|
|
551
639
|
CODEX_DIR,
|
|
552
640
|
HISTORY_FILE,
|
package/src/frontend/app.js
CHANGED
|
@@ -18,6 +18,7 @@ let selectedIds = new Set();
|
|
|
18
18
|
let focusedIndex = -1;
|
|
19
19
|
let availableTerminals = [];
|
|
20
20
|
let pendingDelete = null;
|
|
21
|
+
let activeSessions = {}; // sessionId -> {status, cpu, memoryMB, pid}
|
|
21
22
|
|
|
22
23
|
// Persisted in localStorage
|
|
23
24
|
let stars = JSON.parse(localStorage.getItem('codedash-stars') || '[]');
|
|
@@ -196,6 +197,45 @@ function saveTerminalPref(val) {
|
|
|
196
197
|
localStorage.setItem('codedash-terminal', val);
|
|
197
198
|
}
|
|
198
199
|
|
|
200
|
+
// ── Active sessions polling ───────────────────────────────────
|
|
201
|
+
|
|
202
|
+
async function pollActiveSessions() {
|
|
203
|
+
try {
|
|
204
|
+
var resp = await fetch('/api/active');
|
|
205
|
+
var data = await resp.json();
|
|
206
|
+
activeSessions = {};
|
|
207
|
+
data.forEach(function(a) {
|
|
208
|
+
if (a.sessionId) {
|
|
209
|
+
activeSessions[a.sessionId] = a;
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
// Update badges without full re-render
|
|
213
|
+
document.querySelectorAll('.card').forEach(function(card) {
|
|
214
|
+
var id = card.getAttribute('data-id');
|
|
215
|
+
var existing = card.querySelector('.live-badge');
|
|
216
|
+
if (existing) existing.remove();
|
|
217
|
+
if (activeSessions[id]) {
|
|
218
|
+
var a = activeSessions[id];
|
|
219
|
+
var badge = document.createElement('span');
|
|
220
|
+
badge.className = 'live-badge live-' + a.status;
|
|
221
|
+
badge.textContent = a.status === 'waiting' ? 'WAITING' : 'LIVE';
|
|
222
|
+
badge.title = 'PID ' + a.pid + ' | CPU ' + a.cpu.toFixed(1) + '% | ' + a.memoryMB + 'MB';
|
|
223
|
+
var top = card.querySelector('.card-top');
|
|
224
|
+
if (top) top.insertBefore(badge, top.firstChild);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
} catch {}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
var activeInterval = null;
|
|
231
|
+
function startActivePolling() {
|
|
232
|
+
pollActiveSessions();
|
|
233
|
+
activeInterval = setInterval(pollActiveSessions, 5000);
|
|
234
|
+
}
|
|
235
|
+
function stopActivePolling() {
|
|
236
|
+
if (activeInterval) clearInterval(activeInterval);
|
|
237
|
+
}
|
|
238
|
+
|
|
199
239
|
// ── Trigram search ─────────────────────────────────────────────
|
|
200
240
|
|
|
201
241
|
function trigrams(str) {
|
|
@@ -1426,6 +1466,7 @@ function dismissUpdate() {
|
|
|
1426
1466
|
loadTerminals();
|
|
1427
1467
|
checkForUpdates();
|
|
1428
1468
|
initHoverPreview();
|
|
1469
|
+
startActivePolling();
|
|
1429
1470
|
|
|
1430
1471
|
// Apply saved theme
|
|
1431
1472
|
var savedTheme = localStorage.getItem('codedash-theme') || 'dark';
|
package/src/frontend/styles.css
CHANGED
|
@@ -1385,6 +1385,69 @@ body {
|
|
|
1385
1385
|
color: #fff;
|
|
1386
1386
|
}
|
|
1387
1387
|
|
|
1388
|
+
/* ── Live session badges ────────────────────────────────────── */
|
|
1389
|
+
|
|
1390
|
+
.live-badge {
|
|
1391
|
+
font-size: 10px;
|
|
1392
|
+
font-weight: 800;
|
|
1393
|
+
letter-spacing: 0.8px;
|
|
1394
|
+
padding: 3px 10px;
|
|
1395
|
+
border-radius: 6px;
|
|
1396
|
+
text-transform: uppercase;
|
|
1397
|
+
display: inline-flex;
|
|
1398
|
+
align-items: center;
|
|
1399
|
+
gap: 5px;
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
.live-badge::before {
|
|
1403
|
+
content: '';
|
|
1404
|
+
width: 8px;
|
|
1405
|
+
height: 8px;
|
|
1406
|
+
border-radius: 50%;
|
|
1407
|
+
display: inline-block;
|
|
1408
|
+
animation: dot-pulse 1.5s infinite;
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
.live-active {
|
|
1412
|
+
background: rgba(74, 222, 128, 0.25);
|
|
1413
|
+
color: #22c55e;
|
|
1414
|
+
border: 1px solid rgba(74, 222, 128, 0.5);
|
|
1415
|
+
box-shadow: 0 0 12px rgba(74, 222, 128, 0.3);
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
.live-active::before {
|
|
1419
|
+
background: #22c55e;
|
|
1420
|
+
box-shadow: 0 0 6px #22c55e;
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
.live-waiting {
|
|
1424
|
+
background: rgba(251, 191, 36, 0.2);
|
|
1425
|
+
color: #f59e0b;
|
|
1426
|
+
border: 1px solid rgba(251, 191, 36, 0.4);
|
|
1427
|
+
box-shadow: 0 0 12px rgba(251, 191, 36, 0.25);
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
.live-waiting::before {
|
|
1431
|
+
background: #f59e0b;
|
|
1432
|
+
box-shadow: 0 0 6px #f59e0b;
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
@keyframes dot-pulse {
|
|
1436
|
+
0%, 100% { opacity: 1; transform: scale(1); }
|
|
1437
|
+
50% { opacity: 0.3; transform: scale(0.6); }
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
/* Glow the entire card when live */
|
|
1441
|
+
.card:has(.live-active) {
|
|
1442
|
+
border-color: rgba(74, 222, 128, 0.3);
|
|
1443
|
+
box-shadow: 0 0 20px rgba(74, 222, 128, 0.08);
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
.card:has(.live-waiting) {
|
|
1447
|
+
border-color: rgba(251, 191, 36, 0.25);
|
|
1448
|
+
box-shadow: 0 0 20px rgba(251, 191, 36, 0.06);
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1388
1451
|
/* ── Card expand preview ────────────────────────────────────── */
|
|
1389
1452
|
|
|
1390
1453
|
.card-expand-btn {
|
package/src/server.js
CHANGED
|
@@ -3,7 +3,7 @@ const http = require('http');
|
|
|
3
3
|
const https = require('https');
|
|
4
4
|
const { URL } = require('url');
|
|
5
5
|
const { exec } = require('child_process');
|
|
6
|
-
const { loadSessions, loadSessionDetail, deleteSession, getGitCommits, exportSessionMarkdown, getSessionPreview, searchFullText } = require('./data');
|
|
6
|
+
const { loadSessions, loadSessionDetail, deleteSession, getGitCommits, exportSessionMarkdown, getSessionPreview, searchFullText, getActiveSessions } = require('./data');
|
|
7
7
|
const { detectTerminals, openInTerminal } = require('./terminals');
|
|
8
8
|
const { getHTML } = require('./html');
|
|
9
9
|
|
|
@@ -104,6 +104,12 @@ function startServer(port, openBrowser = true) {
|
|
|
104
104
|
json(res, commits);
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
// ── Active sessions ─────────────────────
|
|
108
|
+
else if (req.method === 'GET' && pathname === '/api/active') {
|
|
109
|
+
const active = getActiveSessions();
|
|
110
|
+
json(res, active);
|
|
111
|
+
}
|
|
112
|
+
|
|
107
113
|
// ── Session preview ─────────────────────
|
|
108
114
|
else if (req.method === 'GET' && pathname.startsWith('/api/preview/')) {
|
|
109
115
|
const sessionId = pathname.split('/').pop();
|