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 +2 -2
- package/bin/claude-watch.js +1 -1
- package/package.json +1 -1
- package/public/index.html +157 -25
- package/src/server/server.js +2 -1
- package/src/watcher/watcher.js +13 -11
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
|
|
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
|
|
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
|
package/bin/claude-watch.js
CHANGED
package/package.json
CHANGED
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 =
|
|
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;
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
632
|
+
scheduleTreeRender();
|
|
622
633
|
}
|
|
623
634
|
|
|
624
635
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
@@ -713,10 +724,30 @@ function isItemVisible(item) {
|
|
|
713
724
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
714
725
|
|
|
715
726
|
function rebuildNodes() {
|
|
716
|
-
|
|
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
|
-
|
|
719
|
-
if (
|
|
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
|
-
|
|
752
|
-
|
|
753
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
//
|
|
981
|
+
// Main agent: 10 minutes
|
|
872
982
|
const mainCtx = contextData[session.id + ':'];
|
|
873
|
-
if (mainCtx && (now - mainCtx.lastActivity) <
|
|
874
|
-
//
|
|
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) <
|
|
988
|
+
if (ctx && (now - ctx.lastActivity) < 180000) return true;
|
|
879
989
|
}
|
|
880
|
-
//
|
|
881
|
-
return (now - session.lastActivity) <
|
|
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 === '
|
|
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();
|
|
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(() => {
|
package/src/server/server.js
CHANGED
|
@@ -22,7 +22,7 @@ var MIME = {
|
|
|
22
22
|
'.ico': 'image/x-icon',
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
-
var MAX_ITEM_BUFFER =
|
|
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;
|
package/src/watcher/watcher.js
CHANGED
|
@@ -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
|
}
|