claude-code-watch 0.0.16 → 0.0.18

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/README.md CHANGED
@@ -9,7 +9,7 @@ Claude Code writes detailed JSONL logs under `~/.claude/projects/` as it works
9
9
  ## Features
10
10
 
11
11
  - **Real-time streaming** — thinking, tool calls, tool results, and text responses appear as they happen
12
- - **Multi-session** — watch all active Claude Code sessions simultaneously in a tree view
12
+ - **Multi-session** — watch all Claude Code sessions from the past 24 hours in a tree view, grouped by date
13
13
  - **Subagent tracking** — see subagent activity nested under their parent session
14
14
  - **Token & cost visibility** — tracks input/output/cache tokens per agent, with context window utilization
15
15
  - **Filter controls** — toggle thinking, tool input, tool output, hook output, and text visibility independently
@@ -51,7 +51,7 @@ OPTIONS:
51
51
  -n Start from newest (skip history, live only)
52
52
  -l [N] List recent sessions (default 10) and exit
53
53
  -a [N] List active sessions (default all) and exit
54
- -w <dur> Active window duration (default 30m, e.g. 30s, 2m, 10m)
54
+ -w <dur> Active window duration (default 24h, e.g. 30s, 2m, 10m)
55
55
  -m <N> Max sessions to show in tree (default 0=unlimited)
56
56
  -c <dur> Auto-collapse sessions inactive for this duration (e.g. 2m)
57
57
  -D Debug: show raw type:subtype for every JSONL line we'd drop
@@ -131,7 +131,7 @@ async function main() {
131
131
  sessionID: '',
132
132
  skipHistory: false,
133
133
  pollMs: 500,
134
- activeWindow: 30 * 60 * 1000,
134
+ activeWindow: 24 * 60 * 60 * 1000,
135
135
  maxSessions: 0,
136
136
  collapseAfter: 0,
137
137
  debugAll: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-watch",
3
- "version": "0.0.16",
3
+ "version": "0.0.18",
4
4
  "description": "Web-based real-time monitor for Claude Code.",
5
5
  "main": "./src/server/server.js",
6
6
  "bin": {
package/public/index.html CHANGED
@@ -149,6 +149,15 @@ body {
149
149
  .tree-row {
150
150
  display: flex; align-items: flex-start;
151
151
  }
152
+ .tree-row-session {
153
+ margin-top: 6px;
154
+ }
155
+ .tree-row-folder {
156
+ margin-top: 6px;
157
+ }
158
+ .folder-node {
159
+ font-size: 13px; font-weight: 500; color: var(--yellow);
160
+ }
152
161
  .tree-content {
153
162
  flex: 1; min-width: 0;
154
163
  }
@@ -320,6 +329,7 @@ body {
320
329
  <span class="sep">│</span>
321
330
  <span id="item-count">0 items</span>
322
331
  <span class="sep">│</span>
332
+ <span id="footer-version" style="margin-left:auto;font-size:10px;color:var(--dim)"></span>
323
333
  </div>
324
334
 
325
335
  <script src="vendor/highlight.min.js"></script>
@@ -355,6 +365,7 @@ let sessions = [];
355
365
  let sessionsMap = new Map(); // id -> session, for O(1) lookups
356
366
  let treeNodes = [];
357
367
  let treeCursor = 0;
368
+ let folderCollapsed = {}; // dateStr -> boolean, default collapsed
358
369
  let streamItems = [];
359
370
  let visibleItems = [];
360
371
  let visibleDirty = true;
@@ -380,6 +391,7 @@ let showText = true;
380
391
  let showHook = true;
381
392
  let showActivity = true;
382
393
  let autoDiscovery = true;
394
+ let appVersion = '';
383
395
 
384
396
  let renderPending = false;
385
397
 
@@ -400,7 +412,7 @@ let collapseAfter = 0;
400
412
  let collapseTimer = null;
401
413
  let activeRefreshTimer = null;
402
414
 
403
- const MAX_ITEMS = 3000;
415
+ const MAX_ITEMS = 9999;
404
416
  const MAX_LINES = 50;
405
417
  let renderedItemCount = 0;
406
418
  let needsFullRender = true;
@@ -504,8 +516,9 @@ function handleMessage(msg) {
504
516
  case 'newBackgroundTask': handleNewBgTask(msg.payload); break;
505
517
  case 'sessionRemoved': handleSessionRemoved(msg.payload); break;
506
518
  case 'autoDiscoveryChanged': autoDiscovery = msg.payload.enabled; scheduleRender(); break;
507
- case 'context': contextData = msg.payload; scheduleRender(); break;
519
+ case 'context': contextData = msg.payload; updateTreeDots(); break;
508
520
  case 'config':
521
+ if (msg.payload.version) appVersion = msg.payload.version;
509
522
  if (msg.payload.collapseAfter > 0 && !collapseTimer) {
510
523
  applyCollapsePolicy(msg.payload.collapseAfter);
511
524
  }
@@ -531,13 +544,13 @@ function handleSnapshot(payload) {
531
544
  id: s.id, projectPath: s.projectPath, title: '',
532
545
  folder: folderName(s.projectPath), model: '',
533
546
  agents: [], tasks: [], collapsed: false, pinned: false,
534
- lastActivity: Date.now(),
547
+ lastActivity: s.birthtimeMs || 0,
548
+ birthtimeMs: s.birthtimeMs || 0,
535
549
  };
536
550
  sessions.push(session);
537
551
  sessionsMap.set(session.id, session);
538
552
  session.agents.push({ id: '', name: 'Main', type: 'main' });
539
553
  }
540
- session.lastActivity = Date.now();
541
554
  for (const [aid, atype] of Object.entries(s.subagents || {})) {
542
555
  if (!session.agents.find(a => a.id === aid)) {
543
556
  session.agents.push({ id: aid, name: agentDisplayName(aid, atype), type: 'agent' });
@@ -557,7 +570,7 @@ function handleSnapshot(payload) {
557
570
  rebuildNodes();
558
571
  needsFullRender = true;
559
572
  visibleDirty = true;
560
- scheduleRender();
573
+ scheduleTreeRender();
561
574
  }
562
575
 
563
576
  function handleNewSession(payload) {
@@ -568,6 +581,7 @@ function handleNewSession(payload) {
568
581
  agents: [{ id: '', name: 'Main', type: 'main' }],
569
582
  tasks: [], collapsed: false, pinned: false,
570
583
  lastActivity: Date.now(),
584
+ birthtimeMs: payload.birthtimeMs || 0,
571
585
  };
572
586
  sessions.push(session);
573
587
  sessionsMap.set(session.id, session);
@@ -575,7 +589,7 @@ function handleNewSession(payload) {
575
589
  rebuildNodes();
576
590
  needsFullRender = true;
577
591
  visibleDirty = true;
578
- scheduleRender();
592
+ scheduleTreeRender();
579
593
  }
580
594
 
581
595
  function handleNewAgent(payload) {
@@ -590,7 +604,7 @@ function handleNewAgent(payload) {
590
604
  rebuildNodes();
591
605
  needsFullRender = true;
592
606
  visibleDirty = true;
593
- scheduleRender();
607
+ scheduleTreeRender();
594
608
  }
595
609
 
596
610
  function handleNewBgTask(payload) {
@@ -602,7 +616,7 @@ function handleNewBgTask(payload) {
602
616
  isComplete: payload.isComplete,
603
617
  });
604
618
  rebuildNodes();
605
- scheduleRender();
619
+ scheduleTreeRender();
606
620
  }
607
621
 
608
622
  function handleSessionRemoved(payload) {
@@ -615,7 +629,7 @@ function handleSessionRemoved(payload) {
615
629
  rebuildNodes();
616
630
  needsFullRender = true;
617
631
  visibleDirty = true;
618
- scheduleRender();
632
+ scheduleTreeRender();
619
633
  }
620
634
 
621
635
  // ══════════════════════════════════════════════════════════════════════════════
@@ -710,10 +724,30 @@ function isItemVisible(item) {
710
724
  // ══════════════════════════════════════════════════════════════════════════════
711
725
 
712
726
  function rebuildNodes() {
713
- treeNodes = [];
727
+ // Sort sessions by creation time, newest first
728
+ sessions.sort((a, b) => (b.birthtimeMs || 0) - (a.birthtimeMs || 0));
729
+
730
+ const today = new Date();
731
+ const todayStr = `${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
732
+
733
+ const todaySessions = [];
734
+ const olderByDate = new Map(); // dateStr -> [sessions]
735
+
714
736
  for (const s of sessions) {
715
- treeNodes.push({ type: 'session', level: 0, isLast: false, ...s });
716
- if (s.collapsed) continue;
737
+ const dateStr = s.birthtimeMs ? formatTime(s.birthtimeMs).split(' ')[0] : null;
738
+ if (!dateStr || dateStr === todayStr) {
739
+ todaySessions.push(s);
740
+ } else {
741
+ if (!olderByDate.has(dateStr)) olderByDate.set(dateStr, []);
742
+ olderByDate.get(dateStr).push(s);
743
+ }
744
+ }
745
+
746
+ treeNodes = [];
747
+
748
+ function addSessionWithChildren(s, inFolder) {
749
+ treeNodes.push({ type: 'session', level: 0, isLast: false, inFolder: !!inFolder, ...s });
750
+ if (s.collapsed) return;
717
751
  const agents = s.agents || [];
718
752
  const lastAgentIdx = agents.length - 1;
719
753
  for (let ai = 0; ai < agents.length; ai++) {
@@ -745,15 +779,58 @@ function rebuildNodes() {
745
779
  }
746
780
  }
747
781
  }
748
- // Mark last session
749
- const sessionNodes = treeNodes.filter(n => n.type === 'session');
750
- if (sessionNodes.length > 0) sessionNodes[sessionNodes.length - 1].isLast = true;
782
+
783
+ // Today's sessions (expanded)
784
+ for (const s of todaySessions) {
785
+ addSessionWithChildren(s, false);
786
+ }
787
+
788
+ // Date folders (older dates, collapsed by default)
789
+ const sortedDates = [...olderByDate.keys()].sort((a, b) => b.localeCompare(a));
790
+ for (let di = 0; di < sortedDates.length; di++) {
791
+ const dateStr = sortedDates[di];
792
+ const folderSessions = olderByDate.get(dateStr);
793
+ const collapsed = folderCollapsed[dateStr] !== false; // default collapsed
794
+ const isLastFolder = di === sortedDates.length - 1;
795
+
796
+ treeNodes.push({
797
+ type: 'date-folder', date: dateStr, level: 0, isLast: false,
798
+ collapsed, sessionCount: folderSessions.length,
799
+ });
800
+
801
+ if (!collapsed) {
802
+ for (const s of folderSessions) {
803
+ addSessionWithChildren(s, true);
804
+ }
805
+ }
806
+ }
807
+
808
+ // Mark last session among today's sessions
809
+ const todaySessionNodes = treeNodes.filter(n => n.type === 'session' && !n.inFolder);
810
+ if (todaySessionNodes.length > 0) todaySessionNodes[todaySessionNodes.length - 1].isLast = true;
811
+
812
+ // Mark last session inside each folder
813
+ for (const dateStr of sortedDates) {
814
+ if (folderCollapsed[dateStr] !== false) continue;
815
+ const folderSessionNodes = treeNodes.filter(n => n.type === 'session' && n.inFolder);
816
+ // Find sessions belonging to this folder
817
+ const thisFolder = [];
818
+ let inThisFolder = false;
819
+ for (const n of treeNodes) {
820
+ if (n.type === 'date-folder' && n.date === dateStr) { inThisFolder = true; continue; }
821
+ if (n.type === 'date-folder' && n.date !== dateStr) { inThisFolder = false; continue; }
822
+ if (inThisFolder && n.type === 'session') thisFolder.push(n);
823
+ }
824
+ if (thisFolder.length > 0) thisFolder[thisFolder.length - 1].isLast = true;
825
+ }
751
826
 
752
827
  if (treeCursor >= treeNodes.length) treeCursor = Math.max(0, treeNodes.length - 1);
753
828
  }
754
829
 
755
830
  function treePrefix(node) {
756
- if (node.level === 0) return '';
831
+ if (node.level === 0) {
832
+ return node.inFolder ? ' ' : '';
833
+ }
757
834
  const branch = node.isLast ? '└── ' : '├── ';
758
835
  if (node.level === 1) return branch;
759
836
  // Level 2: need to check if parent agent is last
@@ -767,6 +844,17 @@ function getNodeHTML(node, idx) {
767
844
  const isSelected = idx === treeCursor;
768
845
  const selClass = isSelected ? ' selected' : '';
769
846
 
847
+ if (node.type === 'date-folder') {
848
+ const icon = node.collapsed ? '▸' : '▾';
849
+ return `<div class="tree-row tree-row-folder${selClass ? ' selected' : ''}">
850
+ <div class="tree-content" onclick="treeClick(${idx})" data-idx="${idx}">
851
+ <div class="tree-node folder-node">
852
+ ${icon} 📁 ${node.date} <span style="font-size:10px;color:var(--dim);margin-left:4px">(${node.sessionCount})</span>
853
+ </div>
854
+ </div>
855
+ </div>`;
856
+ }
857
+
770
858
  if (node.type === 'session') {
771
859
  const displayName = node.title || folderName(node.projectPath) || node.id.slice(0, 14);
772
860
  const parts = [];
@@ -774,12 +862,15 @@ function getNodeHTML(node, idx) {
774
862
  const activeDot = isSessionActive(node) ? '<span class="active-dot on">🟢</span>' : '<span class="active-dot off">⚪</span>';
775
863
  const subInfo = parts.length > 0 ? ` <span style="color:#6b7280;font-size:10px">${parts.join(' · ')}</span>` : '';
776
864
  const agentCount = node.agents ? node.agents.filter(a => a.type === 'agent').length : 0;
777
- return `<div class="tree-row${selClass ? ' selected' : ''}">
865
+ const timeStr = formatTime(node.birthtimeMs);
866
+ const timeHtml = timeStr ? `<span style="margin-left:auto;font-size:10px;color:var(--dim);flex-shrink:0">${timeStr}</span>` : '';
867
+ return `<div class="tree-row tree-row-session${selClass ? ' selected' : ''}">
778
868
  <div class="tree-content" onclick="treeClick(${idx})" data-idx="${idx}">
779
869
  <div class="tree-node">
780
870
  <span class="tree-prefix">${treePrefix(node)}</span>${activeDot} ${node.collapsed ? '▸' : '▾'} ${esc(displayName)}
781
871
  ${node.collapsed && agentCount > 0 ? `(${esc(String(agentCount))})` : ''}
782
872
  ${subInfo}
873
+ ${timeHtml}
783
874
  </div>
784
875
  </div>
785
876
  <span class="tree-actions">
@@ -862,20 +953,42 @@ function renderTree() {
862
953
  treeCursorInfo.textContent = `${treeCursor + 1}/${treeNodes.length}`;
863
954
  }
864
955
 
956
+ function updateTreeDots() {
957
+ const dots = treeEl.querySelectorAll('.active-dot');
958
+ const now = Date.now();
959
+ for (const dot of dots) {
960
+ const content = dot.closest('.tree-content');
961
+ if (!content) continue;
962
+ const idx = parseInt(content.getAttribute('data-idx'));
963
+ if (isNaN(idx)) continue;
964
+ const node = treeNodes[idx];
965
+ if (!node || node.type !== 'session') continue;
966
+ const active = isSessionActive(node);
967
+ const newCls = active ? 'active-dot on' : 'active-dot off';
968
+ const newHTML = active ? '🟢' : '⚪';
969
+ if (dot.className !== newCls) {
970
+ dot.className = newCls;
971
+ dot.innerHTML = newHTML;
972
+ }
973
+ }
974
+ }
975
+
976
+ const ACTIVE_THRESHOLD = 600000; // 10 minutes
977
+
865
978
  function isSessionActive(session) {
866
979
  if (!session) return false;
867
980
  const now = Date.now();
868
- // Check main agent
981
+ // Main agent: 10 minutes
869
982
  const mainCtx = contextData[session.id + ':'];
870
- if (mainCtx && (now - mainCtx.lastActivity) < 120000) return true;
871
- // Check subagents
983
+ if (mainCtx && (now - mainCtx.lastActivity) < 600000) return true;
984
+ // Subagents: 3 minutes
872
985
  for (const a of session.agents) {
873
986
  if (a.id === '') continue;
874
987
  const ctx = contextData[session.id + ':' + a.id];
875
- if (ctx && (now - ctx.lastActivity) < 120000) return true;
988
+ if (ctx && (now - ctx.lastActivity) < 180000) return true;
876
989
  }
877
- // Fallback: check own lastActivity
878
- return (now - session.lastActivity) < 120000;
990
+ // Session fallback: 10 minutes
991
+ return (now - session.lastActivity) < 600000;
879
992
  }
880
993
 
881
994
  // ══════════════════════════════════════════════════════════════════════════════
@@ -1051,6 +1164,13 @@ function refreshButtons() {
1051
1164
  }
1052
1165
  }
1053
1166
  tokenInfo.textContent = tokStr;
1167
+
1168
+ // Footer version
1169
+ const vEl = document.getElementById('footer-version');
1170
+ if (vEl) {
1171
+ const v = appVersion ? `v${appVersion}` : '';
1172
+ vEl.innerHTML = `${v ? v + ' · ' : ''}<a href="https://github.com/shuxuecode/claude-watch" target="_blank" rel="noopener" style="color:var(--dim);display:inline-flex;align-items:center;gap:3px"><svg viewBox="0 0 16 16" width="12" height="12" fill="currentColor" style="vertical-align:middle"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg>claude-watch</a>`;
1173
+ }
1054
1174
  }
1055
1175
 
1056
1176
  // ══════════════════════════════════════════════════════════════════════════════
@@ -1065,7 +1185,11 @@ function treeClick(idx) {
1065
1185
  selectIndex(idx);
1066
1186
  const node = treeNodes[idx];
1067
1187
  if (!node) return;
1068
- if (node.type === 'session') {
1188
+ if (node.type === 'date-folder') {
1189
+ node.collapsed = !node.collapsed;
1190
+ folderCollapsed[node.date] = node.collapsed;
1191
+ rebuildNodes();
1192
+ } else if (node.type === 'session') {
1069
1193
  const session = sessions.find(s => s.id === node.id);
1070
1194
  if (session) {
1071
1195
  session.collapsed = !session.collapsed;
@@ -1206,7 +1330,7 @@ function toggleText() { showText = !showText; needsFullRender = true;
1206
1330
  visibleDirty = true; renderStream(); refreshButtons(); }
1207
1331
  function toggleHook() { showHook = !showHook; needsFullRender = true;
1208
1332
  visibleDirty = true; renderStream(); refreshButtons(); }
1209
- function toggleActivity() { showActivity = !showActivity; rebuildNodes(); scheduleRender(); refreshButtons(); }
1333
+ function toggleActivity() { showActivity = !showActivity; rebuildNodes(); scheduleTreeRender(); refreshButtons(); }
1210
1334
  function toggleAutoScroll() { autoScroll = !autoScroll; if (autoScroll) streamEl.scrollTop = streamEl.scrollHeight; renderAll(); }
1211
1335
  function toggleTree() { showTree = !showTree; document.getElementById('tree-panel').classList.toggle('hidden', !showTree); }
1212
1336
  function toggleAutoDiscovery() { sendCmd('toggleAutoDiscovery'); }
@@ -1338,6 +1462,13 @@ function fmtDur(ms) {
1338
1462
  return `(${(ms / 60000).toFixed(1)}m)`;
1339
1463
  }
1340
1464
 
1465
+ function formatTime(ms) {
1466
+ if (!ms) return '';
1467
+ const d = new Date(ms);
1468
+ const pad = (n) => String(n).padStart(2, '0');
1469
+ return `${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
1470
+ }
1471
+
1341
1472
  function fmtTimestamp(ts) {
1342
1473
  if (!ts) return '';
1343
1474
  const d = ts instanceof Date ? ts : new Date(ts);
@@ -1363,6 +1494,17 @@ function renderAll() {
1363
1494
  }
1364
1495
 
1365
1496
  function scheduleRender() {
1497
+ if (!renderPending) {
1498
+ renderPending = true;
1499
+ requestAnimationFrame(() => {
1500
+ renderPending = false;
1501
+ renderStream();
1502
+ refreshButtons();
1503
+ });
1504
+ }
1505
+ }
1506
+
1507
+ function scheduleTreeRender() {
1366
1508
  if (!renderPending) {
1367
1509
  renderPending = true;
1368
1510
  requestAnimationFrame(() => {
@@ -10,6 +10,8 @@ var { WebSocketServer } = require('ws');
10
10
  var { Watcher, listSessions, listActiveSessions } = require('../watcher/watcher');
11
11
  var { setDebugAll, contextWindowFor } = require('../parser/parser');
12
12
 
13
+ var PACKAGE_VERSION = require('../../package.json').version;
14
+
13
15
  var MIME = {
14
16
  '.html': 'text/html; charset=utf-8',
15
17
  '.css': 'text/css; charset=utf-8',
@@ -20,7 +22,7 @@ var MIME = {
20
22
  '.ico': 'image/x-icon',
21
23
  };
22
24
 
23
- var MAX_ITEM_BUFFER = 2000;
25
+ var MAX_ITEM_BUFFER = 9999;
24
26
  var CONTEXT_STALE_MS = 60 * 60 * 1000; // 60 minutes
25
27
 
26
28
  class DashboardServer {
@@ -279,6 +281,7 @@ class DashboardServer {
279
281
  const sessions = this.watcher.getSessionsSnapshot().map(s => ({
280
282
  id: s.id,
281
283
  projectPath: s.projectPath,
284
+ birthtimeMs: s.birthtimeMs || 0,
282
285
  subagents: Object.entries(s.subagentTypes || s.subagents || {}).reduce((acc, [id, type]) => {
283
286
  acc[id] = typeof type === 'string' ? type : '';
284
287
  return acc;
@@ -306,7 +309,7 @@ class DashboardServer {
306
309
  }
307
310
 
308
311
  sendConfig(ws) {
309
- this.send(ws, 'config', { collapseAfter: this.collapseAfterMs });
312
+ this.send(ws, 'config', { collapseAfter: this.collapseAfterMs, version: PACKAGE_VERSION });
310
313
  }
311
314
 
312
315
  setupWatcher(watcherOpts) {
@@ -81,10 +81,11 @@ async function readAgentType(jsonlPath) {
81
81
  // ============================================================================
82
82
 
83
83
  class Session {
84
- constructor(id, projectPath, mainFile) {
84
+ constructor(id, projectPath, mainFile, birthtimeMs) {
85
85
  this.id = id;
86
86
  this.projectPath = projectPath;
87
87
  this.mainFile = mainFile;
88
+ this.birthtimeMs = birthtimeMs || 0;
88
89
  this.subagents = {}; // agentID -> file path
89
90
  this.subagentTypes = {}; // agentID -> agentType
90
91
  this.backgroundTasks = {}; // toolID -> BackgroundTask
@@ -207,13 +208,13 @@ class Watcher extends EventEmitter {
207
208
  return this.buildSession(mainFile);
208
209
  }
209
210
 
210
- async buildSession(mainFile) {
211
+ async buildSession(mainFile, birthtimeMs) {
211
212
  const base = path.basename(mainFile);
212
213
  const id = base.replace(/\.jsonl$/, '');
213
214
  const projectDir = path.basename(path.dirname(mainFile));
214
215
  const projectPath = await resolveProjectPath(projectDir);
215
216
 
216
- const session = new Session(id, projectPath, mainFile);
217
+ const session = new Session(id, projectPath, mainFile, birthtimeMs);
217
218
 
218
219
  // Find subagent files
219
220
  const subagentDir = path.join(path.dirname(mainFile), id, 'subagents');
@@ -245,7 +246,7 @@ class Watcher extends EventEmitter {
245
246
  await this._walkDir(this.claudeDir, (filePath, stats) => {
246
247
  if (!isMainSessionFile(filePath, stats)) return;
247
248
  if (now - stats.mtimeMs > this.activeWindow) return;
248
- discovered.push({ filePath, modTime: stats.mtimeMs });
249
+ discovered.push({ filePath, modTime: stats.mtimeMs, birthtimeMs: stats.birthtimeMs });
249
250
  });
250
251
  } catch (err) {
251
252
  if (this.debug) console.error('[watcher] discoverActiveSessions error:', err.message);
@@ -258,12 +259,12 @@ class Watcher extends EventEmitter {
258
259
  }
259
260
 
260
261
  for (const d of discovered) {
261
- const session = await this.buildSession(d.filePath);
262
+ const session = await this.buildSession(d.filePath, d.birthtimeMs);
262
263
  if (!this.sessions.has(session.id)) {
263
264
  this.sessions.set(session.id, session);
264
265
 
265
266
  // Broadcast so connected clients learn about the new session
266
- this.emit('broadcast', 'newSession', { sessionID: session.id, projectPath: session.projectPath });
267
+ this.emit('broadcast', 'newSession', { sessionID: session.id, projectPath: session.projectPath, birthtimeMs: session.birthtimeMs });
267
268
  for (const [agentID, agentType] of Object.entries(session.subagentTypes)) {
268
269
  this.emit('broadcast', 'newAgent', { sessionID: session.id, agentID, agentType });
269
270
  }
@@ -528,12 +529,12 @@ class Watcher extends EventEmitter {
528
529
  // Only accept sessions within the active window
529
530
  if (Date.now() - stats.mtimeMs > this.activeWindow) return;
530
531
 
531
- const session = await this.buildSession(p);
532
+ const session = await this.buildSession(p, stats.birthtimeMs);
532
533
  if (this.sessions.has(session.id)) return;
533
534
 
534
535
  this.sessions.set(session.id, session);
535
536
  this._registerSessionWatches(session);
536
- this.emit('broadcast', 'newSession', { sessionID: session.id, projectPath: session.projectPath });
537
+ this.emit('broadcast', 'newSession', { sessionID: session.id, projectPath: session.projectPath, birthtimeMs: session.birthtimeMs });
537
538
 
538
539
  // Broadcast pre-existing subagents to frontend
539
540
  for (const [agentID, agentType] of Object.entries(session.subagentTypes)) {
@@ -645,7 +646,7 @@ class Watcher extends EventEmitter {
645
646
  const id = path.basename(filePath).replace(/\.jsonl$/, '');
646
647
  if (this.sessions.has(id)) return;
647
648
 
648
- fileCandidates.push({ filePath, modTime: stats.mtimeMs });
649
+ fileCandidates.push({ filePath, modTime: stats.mtimeMs, birthtimeMs: stats.birthtimeMs });
649
650
  });
650
651
  } catch (err) {
651
652
  if (this.debug) console.error('[watcher] _checkForNewSessions error:', err.message);
@@ -653,7 +654,7 @@ class Watcher extends EventEmitter {
653
654
 
654
655
  const candidates = [];
655
656
  for (const fc of fileCandidates) {
656
- const session = await this.buildSession(fc.filePath);
657
+ const session = await this.buildSession(fc.filePath, fc.birthtimeMs);
657
658
  candidates.push({ session, modTime: fc.modTime });
658
659
  }
659
660
 
@@ -671,7 +672,7 @@ class Watcher extends EventEmitter {
671
672
  await this._readSessionFiles(c.session);
672
673
  }
673
674
 
674
- this.emit('broadcast', 'newSession', { sessionID: c.session.id, projectPath: c.session.projectPath });
675
+ this.emit('broadcast', 'newSession', { sessionID: c.session.id, projectPath: c.session.projectPath, birthtimeMs: c.session.birthtimeMs });
675
676
 
676
677
  for (const [agentID, agentType] of Object.entries(c.session.subagentTypes)) {
677
678
  this.emit('broadcast', 'newAgent', { sessionID: c.session.id, agentID, agentType });
@@ -1258,6 +1259,7 @@ async function _listSessionsFiltered(limit, activeWithin) {
1258
1259
  path: c.filePath,
1259
1260
  projectPath,
1260
1261
  modified: c.stats.mtime,
1262
+ birthtimeMs: c.stats.birthtimeMs,
1261
1263
  isActive: (now - c.stats.mtimeMs) < RecentActivityThreshold,
1262
1264
  });
1263
1265
  }