claude-code-watch 0.0.17 → 0.0.19

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.17",
3
+ "version": "0.0.19",
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
  }
@@ -356,6 +365,7 @@ let sessions = [];
356
365
  let sessionsMap = new Map(); // id -> session, for O(1) lookups
357
366
  let treeNodes = [];
358
367
  let treeCursor = 0;
368
+ let folderCollapsed = {}; // dateStr -> boolean, default collapsed
359
369
  let streamItems = [];
360
370
  let visibleItems = [];
361
371
  let visibleDirty = true;
@@ -402,7 +412,7 @@ let collapseAfter = 0;
402
412
  let collapseTimer = null;
403
413
  let activeRefreshTimer = null;
404
414
 
405
- const MAX_ITEMS = 3000;
415
+ const MAX_ITEMS = 9999;
406
416
  const MAX_LINES = 50;
407
417
  let renderedItemCount = 0;
408
418
  let needsFullRender = true;
@@ -506,7 +516,7 @@ function handleMessage(msg) {
506
516
  case 'newBackgroundTask': handleNewBgTask(msg.payload); break;
507
517
  case 'sessionRemoved': handleSessionRemoved(msg.payload); break;
508
518
  case 'autoDiscoveryChanged': autoDiscovery = msg.payload.enabled; scheduleRender(); break;
509
- case 'context': contextData = msg.payload; scheduleRender(); break;
519
+ case 'context': contextData = msg.payload; updateTreeDots(); break;
510
520
  case 'config':
511
521
  if (msg.payload.version) appVersion = msg.payload.version;
512
522
  if (msg.payload.collapseAfter > 0 && !collapseTimer) {
@@ -534,13 +544,13 @@ function handleSnapshot(payload) {
534
544
  id: s.id, projectPath: s.projectPath, title: '',
535
545
  folder: folderName(s.projectPath), model: '',
536
546
  agents: [], tasks: [], collapsed: false, pinned: false,
537
- lastActivity: Date.now(),
547
+ lastActivity: s.birthtimeMs || 0,
548
+ birthtimeMs: s.birthtimeMs || 0,
538
549
  };
539
550
  sessions.push(session);
540
551
  sessionsMap.set(session.id, session);
541
552
  session.agents.push({ id: '', name: 'Main', type: 'main' });
542
553
  }
543
- session.lastActivity = Date.now();
544
554
  for (const [aid, atype] of Object.entries(s.subagents || {})) {
545
555
  if (!session.agents.find(a => a.id === aid)) {
546
556
  session.agents.push({ id: aid, name: agentDisplayName(aid, atype), type: 'agent' });
@@ -560,7 +570,7 @@ function handleSnapshot(payload) {
560
570
  rebuildNodes();
561
571
  needsFullRender = true;
562
572
  visibleDirty = true;
563
- scheduleRender();
573
+ scheduleTreeRender();
564
574
  }
565
575
 
566
576
  function handleNewSession(payload) {
@@ -571,6 +581,7 @@ function handleNewSession(payload) {
571
581
  agents: [{ id: '', name: 'Main', type: 'main' }],
572
582
  tasks: [], collapsed: false, pinned: false,
573
583
  lastActivity: Date.now(),
584
+ birthtimeMs: payload.birthtimeMs || 0,
574
585
  };
575
586
  sessions.push(session);
576
587
  sessionsMap.set(session.id, session);
@@ -578,7 +589,7 @@ function handleNewSession(payload) {
578
589
  rebuildNodes();
579
590
  needsFullRender = true;
580
591
  visibleDirty = true;
581
- scheduleRender();
592
+ scheduleTreeRender();
582
593
  }
583
594
 
584
595
  function handleNewAgent(payload) {
@@ -593,7 +604,7 @@ function handleNewAgent(payload) {
593
604
  rebuildNodes();
594
605
  needsFullRender = true;
595
606
  visibleDirty = true;
596
- scheduleRender();
607
+ scheduleTreeRender();
597
608
  }
598
609
 
599
610
  function handleNewBgTask(payload) {
@@ -605,7 +616,7 @@ function handleNewBgTask(payload) {
605
616
  isComplete: payload.isComplete,
606
617
  });
607
618
  rebuildNodes();
608
- scheduleRender();
619
+ scheduleTreeRender();
609
620
  }
610
621
 
611
622
  function handleSessionRemoved(payload) {
@@ -618,7 +629,7 @@ function handleSessionRemoved(payload) {
618
629
  rebuildNodes();
619
630
  needsFullRender = true;
620
631
  visibleDirty = true;
621
- scheduleRender();
632
+ scheduleTreeRender();
622
633
  }
623
634
 
624
635
  // ══════════════════════════════════════════════════════════════════════════════
@@ -646,6 +657,8 @@ function handleItemBatch(items) {
646
657
  if (s) { s.title = item.content.slice(0, 30); }
647
658
  continue;
648
659
  }
660
+ const s = sessionsMap.get(item.sessionID);
661
+ if (s) s.lastActivity = Date.now();
649
662
  pushItem(item);
650
663
  }
651
664
  scheduleRender();
@@ -713,10 +726,30 @@ function isItemVisible(item) {
713
726
  // ══════════════════════════════════════════════════════════════════════════════
714
727
 
715
728
  function rebuildNodes() {
716
- treeNodes = [];
729
+ // Sort sessions by creation time, newest first
730
+ sessions.sort((a, b) => (b.birthtimeMs || 0) - (a.birthtimeMs || 0));
731
+
732
+ const today = new Date();
733
+ const todayStr = `${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
734
+
735
+ const todaySessions = [];
736
+ const olderByDate = new Map(); // dateStr -> [sessions]
737
+
717
738
  for (const s of sessions) {
718
- treeNodes.push({ type: 'session', level: 0, isLast: false, ...s });
719
- if (s.collapsed) continue;
739
+ const dateStr = s.birthtimeMs ? formatTime(s.birthtimeMs).split(' ')[0] : null;
740
+ if (!dateStr || dateStr === todayStr) {
741
+ todaySessions.push(s);
742
+ } else {
743
+ if (!olderByDate.has(dateStr)) olderByDate.set(dateStr, []);
744
+ olderByDate.get(dateStr).push(s);
745
+ }
746
+ }
747
+
748
+ treeNodes = [];
749
+
750
+ function addSessionWithChildren(s, inFolder) {
751
+ treeNodes.push({ type: 'session', level: 0, isLast: false, inFolder: !!inFolder, ...s });
752
+ if (s.collapsed) return;
720
753
  const agents = s.agents || [];
721
754
  const lastAgentIdx = agents.length - 1;
722
755
  for (let ai = 0; ai < agents.length; ai++) {
@@ -748,15 +781,58 @@ function rebuildNodes() {
748
781
  }
749
782
  }
750
783
  }
751
- // Mark last session
752
- const sessionNodes = treeNodes.filter(n => n.type === 'session');
753
- if (sessionNodes.length > 0) sessionNodes[sessionNodes.length - 1].isLast = true;
784
+
785
+ // Today's sessions (expanded)
786
+ for (const s of todaySessions) {
787
+ addSessionWithChildren(s, false);
788
+ }
789
+
790
+ // Date folders (older dates, collapsed by default)
791
+ const sortedDates = [...olderByDate.keys()].sort((a, b) => b.localeCompare(a));
792
+ for (let di = 0; di < sortedDates.length; di++) {
793
+ const dateStr = sortedDates[di];
794
+ const folderSessions = olderByDate.get(dateStr);
795
+ const collapsed = folderCollapsed[dateStr] !== false; // default collapsed
796
+ const isLastFolder = di === sortedDates.length - 1;
797
+
798
+ treeNodes.push({
799
+ type: 'date-folder', date: dateStr, level: 0, isLast: false,
800
+ collapsed, sessionCount: folderSessions.length,
801
+ });
802
+
803
+ if (!collapsed) {
804
+ for (const s of folderSessions) {
805
+ addSessionWithChildren(s, true);
806
+ }
807
+ }
808
+ }
809
+
810
+ // Mark last session among today's sessions
811
+ const todaySessionNodes = treeNodes.filter(n => n.type === 'session' && !n.inFolder);
812
+ if (todaySessionNodes.length > 0) todaySessionNodes[todaySessionNodes.length - 1].isLast = true;
813
+
814
+ // Mark last session inside each folder
815
+ for (const dateStr of sortedDates) {
816
+ if (folderCollapsed[dateStr] !== false) continue;
817
+ const folderSessionNodes = treeNodes.filter(n => n.type === 'session' && n.inFolder);
818
+ // Find sessions belonging to this folder
819
+ const thisFolder = [];
820
+ let inThisFolder = false;
821
+ for (const n of treeNodes) {
822
+ if (n.type === 'date-folder' && n.date === dateStr) { inThisFolder = true; continue; }
823
+ if (n.type === 'date-folder' && n.date !== dateStr) { inThisFolder = false; continue; }
824
+ if (inThisFolder && n.type === 'session') thisFolder.push(n);
825
+ }
826
+ if (thisFolder.length > 0) thisFolder[thisFolder.length - 1].isLast = true;
827
+ }
754
828
 
755
829
  if (treeCursor >= treeNodes.length) treeCursor = Math.max(0, treeNodes.length - 1);
756
830
  }
757
831
 
758
832
  function treePrefix(node) {
759
- if (node.level === 0) return '';
833
+ if (node.level === 0) {
834
+ return node.inFolder ? ' ' : '';
835
+ }
760
836
  const branch = node.isLast ? '└── ' : '├── ';
761
837
  if (node.level === 1) return branch;
762
838
  // Level 2: need to check if parent agent is last
@@ -770,6 +846,17 @@ function getNodeHTML(node, idx) {
770
846
  const isSelected = idx === treeCursor;
771
847
  const selClass = isSelected ? ' selected' : '';
772
848
 
849
+ if (node.type === 'date-folder') {
850
+ const icon = node.collapsed ? '▸' : '▾';
851
+ return `<div class="tree-row tree-row-folder${selClass ? ' selected' : ''}">
852
+ <div class="tree-content" onclick="treeClick(${idx})" data-idx="${idx}">
853
+ <div class="tree-node folder-node">
854
+ ${icon} 📁 ${node.date} <span style="font-size:10px;color:var(--dim);margin-left:4px">(${node.sessionCount})</span>
855
+ </div>
856
+ </div>
857
+ </div>`;
858
+ }
859
+
773
860
  if (node.type === 'session') {
774
861
  const displayName = node.title || folderName(node.projectPath) || node.id.slice(0, 14);
775
862
  const parts = [];
@@ -777,12 +864,15 @@ function getNodeHTML(node, idx) {
777
864
  const activeDot = isSessionActive(node) ? '<span class="active-dot on">🟢</span>' : '<span class="active-dot off">⚪</span>';
778
865
  const subInfo = parts.length > 0 ? ` <span style="color:#6b7280;font-size:10px">${parts.join(' · ')}</span>` : '';
779
866
  const agentCount = node.agents ? node.agents.filter(a => a.type === 'agent').length : 0;
780
- return `<div class="tree-row${selClass ? ' selected' : ''}">
867
+ const timeStr = formatTime(node.birthtimeMs);
868
+ const timeHtml = timeStr ? `<span style="margin-left:auto;font-size:10px;color:var(--dim);flex-shrink:0">${timeStr}</span>` : '';
869
+ return `<div class="tree-row tree-row-session${selClass ? ' selected' : ''}">
781
870
  <div class="tree-content" onclick="treeClick(${idx})" data-idx="${idx}">
782
871
  <div class="tree-node">
783
872
  <span class="tree-prefix">${treePrefix(node)}</span>${activeDot} ${node.collapsed ? '▸' : '▾'} ${esc(displayName)}
784
873
  ${node.collapsed && agentCount > 0 ? `(${esc(String(agentCount))})` : ''}
785
874
  ${subInfo}
875
+ ${timeHtml}
786
876
  </div>
787
877
  </div>
788
878
  <span class="tree-actions">
@@ -865,20 +955,42 @@ function renderTree() {
865
955
  treeCursorInfo.textContent = `${treeCursor + 1}/${treeNodes.length}`;
866
956
  }
867
957
 
958
+ function updateTreeDots() {
959
+ const dots = treeEl.querySelectorAll('.active-dot');
960
+ const now = Date.now();
961
+ for (const dot of dots) {
962
+ const content = dot.closest('.tree-content');
963
+ if (!content) continue;
964
+ const idx = parseInt(content.getAttribute('data-idx'));
965
+ if (isNaN(idx)) continue;
966
+ const node = treeNodes[idx];
967
+ if (!node || node.type !== 'session') continue;
968
+ const active = isSessionActive(node);
969
+ const newCls = active ? 'active-dot on' : 'active-dot off';
970
+ const newHTML = active ? '🟢' : '⚪';
971
+ if (dot.className !== newCls) {
972
+ dot.className = newCls;
973
+ dot.innerHTML = newHTML;
974
+ }
975
+ }
976
+ }
977
+
978
+ const ACTIVE_THRESHOLD = 600000; // 10 minutes
979
+
868
980
  function isSessionActive(session) {
869
981
  if (!session) return false;
870
982
  const now = Date.now();
871
- // Check main agent
983
+ // Main agent: 10 minutes
872
984
  const mainCtx = contextData[session.id + ':'];
873
- if (mainCtx && (now - mainCtx.lastActivity) < 120000) return true;
874
- // Check subagents
985
+ if (mainCtx && (now - mainCtx.lastActivity) < 600000) return true;
986
+ // Subagents: 3 minutes
875
987
  for (const a of session.agents) {
876
988
  if (a.id === '') continue;
877
989
  const ctx = contextData[session.id + ':' + a.id];
878
- if (ctx && (now - ctx.lastActivity) < 120000) return true;
990
+ if (ctx && (now - ctx.lastActivity) < 180000) return true;
879
991
  }
880
- // Fallback: check own lastActivity
881
- return (now - session.lastActivity) < 120000;
992
+ // Session fallback: 10 minutes
993
+ return (now - session.lastActivity) < 600000;
882
994
  }
883
995
 
884
996
  // ══════════════════════════════════════════════════════════════════════════════
@@ -1075,7 +1187,11 @@ function treeClick(idx) {
1075
1187
  selectIndex(idx);
1076
1188
  const node = treeNodes[idx];
1077
1189
  if (!node) return;
1078
- if (node.type === 'session') {
1190
+ if (node.type === 'date-folder') {
1191
+ node.collapsed = !node.collapsed;
1192
+ folderCollapsed[node.date] = node.collapsed;
1193
+ rebuildNodes();
1194
+ } else if (node.type === 'session') {
1079
1195
  const session = sessions.find(s => s.id === node.id);
1080
1196
  if (session) {
1081
1197
  session.collapsed = !session.collapsed;
@@ -1216,7 +1332,7 @@ function toggleText() { showText = !showText; needsFullRender = true;
1216
1332
  visibleDirty = true; renderStream(); refreshButtons(); }
1217
1333
  function toggleHook() { showHook = !showHook; needsFullRender = true;
1218
1334
  visibleDirty = true; renderStream(); refreshButtons(); }
1219
- function toggleActivity() { showActivity = !showActivity; rebuildNodes(); scheduleRender(); refreshButtons(); }
1335
+ function toggleActivity() { showActivity = !showActivity; rebuildNodes(); scheduleTreeRender(); refreshButtons(); }
1220
1336
  function toggleAutoScroll() { autoScroll = !autoScroll; if (autoScroll) streamEl.scrollTop = streamEl.scrollHeight; renderAll(); }
1221
1337
  function toggleTree() { showTree = !showTree; document.getElementById('tree-panel').classList.toggle('hidden', !showTree); }
1222
1338
  function toggleAutoDiscovery() { sendCmd('toggleAutoDiscovery'); }
@@ -1348,6 +1464,13 @@ function fmtDur(ms) {
1348
1464
  return `(${(ms / 60000).toFixed(1)}m)`;
1349
1465
  }
1350
1466
 
1467
+ function formatTime(ms) {
1468
+ if (!ms) return '';
1469
+ const d = new Date(ms);
1470
+ const pad = (n) => String(n).padStart(2, '0');
1471
+ return `${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
1472
+ }
1473
+
1351
1474
  function fmtTimestamp(ts) {
1352
1475
  if (!ts) return '';
1353
1476
  const d = ts instanceof Date ? ts : new Date(ts);
@@ -1373,6 +1496,17 @@ function renderAll() {
1373
1496
  }
1374
1497
 
1375
1498
  function scheduleRender() {
1499
+ if (!renderPending) {
1500
+ renderPending = true;
1501
+ requestAnimationFrame(() => {
1502
+ renderPending = false;
1503
+ renderStream();
1504
+ refreshButtons();
1505
+ });
1506
+ }
1507
+ }
1508
+
1509
+ function scheduleTreeRender() {
1376
1510
  if (!renderPending) {
1377
1511
  renderPending = true;
1378
1512
  requestAnimationFrame(() => {
@@ -22,7 +22,7 @@ var MIME = {
22
22
  '.ico': 'image/x-icon',
23
23
  };
24
24
 
25
- var MAX_ITEM_BUFFER = 2000;
25
+ var MAX_ITEM_BUFFER = 9999;
26
26
  var CONTEXT_STALE_MS = 60 * 60 * 1000; // 60 minutes
27
27
 
28
28
  class DashboardServer {
@@ -50,11 +50,19 @@ class DashboardServer {
50
50
  return sessionID + ':' + (agentID || '');
51
51
  }
52
52
 
53
+ itemTime(item) {
54
+ if (item.timestamp) {
55
+ const ts = item.timestamp instanceof Date ? item.timestamp : new Date(item.timestamp);
56
+ if (!isNaN(ts.getTime())) return ts.getTime();
57
+ }
58
+ return Date.now();
59
+ }
60
+
53
61
  updateContext(item) {
54
62
  const key = this.getCtxKey(item.sessionID, item.agentID);
55
63
  let ctx = this.contextMap.get(key);
56
64
  if (!ctx) {
57
- ctx = { inputTokens: 0, outputTokens: 0, cacheCreation: 0, cacheRead: 0, model: '', contextWindow: 200000, lastActivity: Date.now() };
65
+ ctx = { inputTokens: 0, outputTokens: 0, cacheCreation: 0, cacheRead: 0, model: '', contextWindow: 200000, lastActivity: this.itemTime(item) };
58
66
  this.contextMap.set(key, ctx);
59
67
  }
60
68
  // inputTokens: Claude API returns cumulative total per call, not incremental — use Math.max
@@ -67,7 +75,7 @@ class DashboardServer {
67
75
  ctx.model = item.model;
68
76
  ctx.contextWindow = contextWindowFor(item.model);
69
77
  }
70
- ctx.lastActivity = Date.now();
78
+ ctx.lastActivity = Math.max(ctx.lastActivity || 0, this.itemTime(item));
71
79
  }
72
80
 
73
81
  cleanupContextMap() {
@@ -281,6 +289,7 @@ class DashboardServer {
281
289
  const sessions = this.watcher.getSessionsSnapshot().map(s => ({
282
290
  id: s.id,
283
291
  projectPath: s.projectPath,
292
+ birthtimeMs: s.birthtimeMs || 0,
284
293
  subagents: Object.entries(s.subagentTypes || s.subagents || {}).reduce((acc, [id, type]) => {
285
294
  acc[id] = typeof type === 'string' ? type : '';
286
295
  return acc;
@@ -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
  }