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 +2 -2
- package/bin/claude-watch.js +1 -1
- package/package.json +1 -1
- package/public/index.html +159 -25
- package/src/server/server.js +12 -3
- 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
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
@@ -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
|
-
|
|
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
|
-
|
|
719
|
-
if (
|
|
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
|
-
|
|
752
|
-
|
|
753
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
//
|
|
983
|
+
// Main agent: 10 minutes
|
|
872
984
|
const mainCtx = contextData[session.id + ':'];
|
|
873
|
-
if (mainCtx && (now - mainCtx.lastActivity) <
|
|
874
|
-
//
|
|
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) <
|
|
990
|
+
if (ctx && (now - ctx.lastActivity) < 180000) return true;
|
|
879
991
|
}
|
|
880
|
-
//
|
|
881
|
-
return (now - session.lastActivity) <
|
|
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 === '
|
|
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();
|
|
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(() => {
|
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 {
|
|
@@ -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:
|
|
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 =
|
|
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;
|
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
|
}
|