claude-code-watch 0.0.17 → 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.17",
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
  }
@@ -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
  // ══════════════════════════════════════════════════════════════════════════════
@@ -713,10 +724,30 @@ function isItemVisible(item) {
713
724
  // ══════════════════════════════════════════════════════════════════════════════
714
725
 
715
726
  function rebuildNodes() {
716
- 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
+
717
736
  for (const s of sessions) {
718
- treeNodes.push({ type: 'session', level: 0, isLast: false, ...s });
719
- 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;
720
751
  const agents = s.agents || [];
721
752
  const lastAgentIdx = agents.length - 1;
722
753
  for (let ai = 0; ai < agents.length; ai++) {
@@ -748,15 +779,58 @@ function rebuildNodes() {
748
779
  }
749
780
  }
750
781
  }
751
- // Mark last session
752
- const sessionNodes = treeNodes.filter(n => n.type === 'session');
753
- 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
+ }
754
826
 
755
827
  if (treeCursor >= treeNodes.length) treeCursor = Math.max(0, treeNodes.length - 1);
756
828
  }
757
829
 
758
830
  function treePrefix(node) {
759
- if (node.level === 0) return '';
831
+ if (node.level === 0) {
832
+ return node.inFolder ? ' ' : '';
833
+ }
760
834
  const branch = node.isLast ? '└── ' : '├── ';
761
835
  if (node.level === 1) return branch;
762
836
  // Level 2: need to check if parent agent is last
@@ -770,6 +844,17 @@ function getNodeHTML(node, idx) {
770
844
  const isSelected = idx === treeCursor;
771
845
  const selClass = isSelected ? ' selected' : '';
772
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
+
773
858
  if (node.type === 'session') {
774
859
  const displayName = node.title || folderName(node.projectPath) || node.id.slice(0, 14);
775
860
  const parts = [];
@@ -777,12 +862,15 @@ function getNodeHTML(node, idx) {
777
862
  const activeDot = isSessionActive(node) ? '<span class="active-dot on">🟢</span>' : '<span class="active-dot off">⚪</span>';
778
863
  const subInfo = parts.length > 0 ? ` <span style="color:#6b7280;font-size:10px">${parts.join(' · ')}</span>` : '';
779
864
  const agentCount = node.agents ? node.agents.filter(a => a.type === 'agent').length : 0;
780
- 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' : ''}">
781
868
  <div class="tree-content" onclick="treeClick(${idx})" data-idx="${idx}">
782
869
  <div class="tree-node">
783
870
  <span class="tree-prefix">${treePrefix(node)}</span>${activeDot} ${node.collapsed ? '▸' : '▾'} ${esc(displayName)}
784
871
  ${node.collapsed && agentCount > 0 ? `(${esc(String(agentCount))})` : ''}
785
872
  ${subInfo}
873
+ ${timeHtml}
786
874
  </div>
787
875
  </div>
788
876
  <span class="tree-actions">
@@ -865,20 +953,42 @@ function renderTree() {
865
953
  treeCursorInfo.textContent = `${treeCursor + 1}/${treeNodes.length}`;
866
954
  }
867
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
+
868
978
  function isSessionActive(session) {
869
979
  if (!session) return false;
870
980
  const now = Date.now();
871
- // Check main agent
981
+ // Main agent: 10 minutes
872
982
  const mainCtx = contextData[session.id + ':'];
873
- if (mainCtx && (now - mainCtx.lastActivity) < 120000) return true;
874
- // Check subagents
983
+ if (mainCtx && (now - mainCtx.lastActivity) < 600000) return true;
984
+ // Subagents: 3 minutes
875
985
  for (const a of session.agents) {
876
986
  if (a.id === '') continue;
877
987
  const ctx = contextData[session.id + ':' + a.id];
878
- if (ctx && (now - ctx.lastActivity) < 120000) return true;
988
+ if (ctx && (now - ctx.lastActivity) < 180000) return true;
879
989
  }
880
- // Fallback: check own lastActivity
881
- return (now - session.lastActivity) < 120000;
990
+ // Session fallback: 10 minutes
991
+ return (now - session.lastActivity) < 600000;
882
992
  }
883
993
 
884
994
  // ══════════════════════════════════════════════════════════════════════════════
@@ -1075,7 +1185,11 @@ function treeClick(idx) {
1075
1185
  selectIndex(idx);
1076
1186
  const node = treeNodes[idx];
1077
1187
  if (!node) return;
1078
- 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') {
1079
1193
  const session = sessions.find(s => s.id === node.id);
1080
1194
  if (session) {
1081
1195
  session.collapsed = !session.collapsed;
@@ -1216,7 +1330,7 @@ function toggleText() { showText = !showText; needsFullRender = true;
1216
1330
  visibleDirty = true; renderStream(); refreshButtons(); }
1217
1331
  function toggleHook() { showHook = !showHook; needsFullRender = true;
1218
1332
  visibleDirty = true; renderStream(); refreshButtons(); }
1219
- function toggleActivity() { showActivity = !showActivity; rebuildNodes(); scheduleRender(); refreshButtons(); }
1333
+ function toggleActivity() { showActivity = !showActivity; rebuildNodes(); scheduleTreeRender(); refreshButtons(); }
1220
1334
  function toggleAutoScroll() { autoScroll = !autoScroll; if (autoScroll) streamEl.scrollTop = streamEl.scrollHeight; renderAll(); }
1221
1335
  function toggleTree() { showTree = !showTree; document.getElementById('tree-panel').classList.toggle('hidden', !showTree); }
1222
1336
  function toggleAutoDiscovery() { sendCmd('toggleAutoDiscovery'); }
@@ -1348,6 +1462,13 @@ function fmtDur(ms) {
1348
1462
  return `(${(ms / 60000).toFixed(1)}m)`;
1349
1463
  }
1350
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
+
1351
1472
  function fmtTimestamp(ts) {
1352
1473
  if (!ts) return '';
1353
1474
  const d = ts instanceof Date ? ts : new Date(ts);
@@ -1373,6 +1494,17 @@ function renderAll() {
1373
1494
  }
1374
1495
 
1375
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() {
1376
1508
  if (!renderPending) {
1377
1509
  renderPending = true;
1378
1510
  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 {
@@ -281,6 +281,7 @@ class DashboardServer {
281
281
  const sessions = this.watcher.getSessionsSnapshot().map(s => ({
282
282
  id: s.id,
283
283
  projectPath: s.projectPath,
284
+ birthtimeMs: s.birthtimeMs || 0,
284
285
  subagents: Object.entries(s.subagentTypes || s.subagents || {}).reduce((acc, [id, type]) => {
285
286
  acc[id] = typeof type === 'string' ? type : '';
286
287
  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
  }