codedash-app 1.9.0 → 2.0.0

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codedash-app",
3
- "version": "1.9.0",
3
+ "version": "2.0.0",
4
4
  "description": "Termius-style browser dashboard for Claude Code sessions. View, search, resume, and delete sessions with a dark-themed UI.",
5
5
  "bin": {
6
6
  "codedash": "./bin/cli.js"
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,
@@ -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';
@@ -1385,6 +1385,36 @@ body {
1385
1385
  color: #fff;
1386
1386
  }
1387
1387
 
1388
+ /* ── Live session badges ────────────────────────────────────── */
1389
+
1390
+ .live-badge {
1391
+ font-size: 9px;
1392
+ font-weight: 700;
1393
+ letter-spacing: 0.5px;
1394
+ padding: 2px 7px;
1395
+ border-radius: 4px;
1396
+ text-transform: uppercase;
1397
+ animation: pulse 2s infinite;
1398
+ }
1399
+
1400
+ .live-active {
1401
+ background: rgba(74, 222, 128, 0.2);
1402
+ color: var(--accent-green);
1403
+ border: 1px solid rgba(74, 222, 128, 0.3);
1404
+ }
1405
+
1406
+ .live-waiting {
1407
+ background: rgba(251, 191, 36, 0.15);
1408
+ color: #fbbf24;
1409
+ border: 1px solid rgba(251, 191, 36, 0.25);
1410
+ animation: pulse 1.5s infinite;
1411
+ }
1412
+
1413
+ @keyframes pulse {
1414
+ 0%, 100% { opacity: 1; }
1415
+ 50% { opacity: 0.6; }
1416
+ }
1417
+
1388
1418
  /* ── Card expand preview ────────────────────────────────────── */
1389
1419
 
1390
1420
  .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();