claude-code-watch 0.0.16 → 0.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/bin/claude-watch.js +1 -1
- package/package.json +1 -1
- package/public/index.html +167 -25
- package/src/server/server.js +5 -2
- 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
|
}
|
|
@@ -320,6 +329,7 @@ body {
|
|
|
320
329
|
<span class="sep">│</span>
|
|
321
330
|
<span id="item-count">0 items</span>
|
|
322
331
|
<span class="sep">│</span>
|
|
332
|
+
<span id="footer-version" style="margin-left:auto;font-size:10px;color:var(--dim)"></span>
|
|
323
333
|
</div>
|
|
324
334
|
|
|
325
335
|
<script src="vendor/highlight.min.js"></script>
|
|
@@ -355,6 +365,7 @@ let sessions = [];
|
|
|
355
365
|
let sessionsMap = new Map(); // id -> session, for O(1) lookups
|
|
356
366
|
let treeNodes = [];
|
|
357
367
|
let treeCursor = 0;
|
|
368
|
+
let folderCollapsed = {}; // dateStr -> boolean, default collapsed
|
|
358
369
|
let streamItems = [];
|
|
359
370
|
let visibleItems = [];
|
|
360
371
|
let visibleDirty = true;
|
|
@@ -380,6 +391,7 @@ let showText = true;
|
|
|
380
391
|
let showHook = true;
|
|
381
392
|
let showActivity = true;
|
|
382
393
|
let autoDiscovery = true;
|
|
394
|
+
let appVersion = '';
|
|
383
395
|
|
|
384
396
|
let renderPending = false;
|
|
385
397
|
|
|
@@ -400,7 +412,7 @@ let collapseAfter = 0;
|
|
|
400
412
|
let collapseTimer = null;
|
|
401
413
|
let activeRefreshTimer = null;
|
|
402
414
|
|
|
403
|
-
const MAX_ITEMS =
|
|
415
|
+
const MAX_ITEMS = 9999;
|
|
404
416
|
const MAX_LINES = 50;
|
|
405
417
|
let renderedItemCount = 0;
|
|
406
418
|
let needsFullRender = true;
|
|
@@ -504,8 +516,9 @@ function handleMessage(msg) {
|
|
|
504
516
|
case 'newBackgroundTask': handleNewBgTask(msg.payload); break;
|
|
505
517
|
case 'sessionRemoved': handleSessionRemoved(msg.payload); break;
|
|
506
518
|
case 'autoDiscoveryChanged': autoDiscovery = msg.payload.enabled; scheduleRender(); break;
|
|
507
|
-
case 'context': contextData = msg.payload;
|
|
519
|
+
case 'context': contextData = msg.payload; updateTreeDots(); break;
|
|
508
520
|
case 'config':
|
|
521
|
+
if (msg.payload.version) appVersion = msg.payload.version;
|
|
509
522
|
if (msg.payload.collapseAfter > 0 && !collapseTimer) {
|
|
510
523
|
applyCollapsePolicy(msg.payload.collapseAfter);
|
|
511
524
|
}
|
|
@@ -531,13 +544,13 @@ function handleSnapshot(payload) {
|
|
|
531
544
|
id: s.id, projectPath: s.projectPath, title: '',
|
|
532
545
|
folder: folderName(s.projectPath), model: '',
|
|
533
546
|
agents: [], tasks: [], collapsed: false, pinned: false,
|
|
534
|
-
lastActivity:
|
|
547
|
+
lastActivity: s.birthtimeMs || 0,
|
|
548
|
+
birthtimeMs: s.birthtimeMs || 0,
|
|
535
549
|
};
|
|
536
550
|
sessions.push(session);
|
|
537
551
|
sessionsMap.set(session.id, session);
|
|
538
552
|
session.agents.push({ id: '', name: 'Main', type: 'main' });
|
|
539
553
|
}
|
|
540
|
-
session.lastActivity = Date.now();
|
|
541
554
|
for (const [aid, atype] of Object.entries(s.subagents || {})) {
|
|
542
555
|
if (!session.agents.find(a => a.id === aid)) {
|
|
543
556
|
session.agents.push({ id: aid, name: agentDisplayName(aid, atype), type: 'agent' });
|
|
@@ -557,7 +570,7 @@ function handleSnapshot(payload) {
|
|
|
557
570
|
rebuildNodes();
|
|
558
571
|
needsFullRender = true;
|
|
559
572
|
visibleDirty = true;
|
|
560
|
-
|
|
573
|
+
scheduleTreeRender();
|
|
561
574
|
}
|
|
562
575
|
|
|
563
576
|
function handleNewSession(payload) {
|
|
@@ -568,6 +581,7 @@ function handleNewSession(payload) {
|
|
|
568
581
|
agents: [{ id: '', name: 'Main', type: 'main' }],
|
|
569
582
|
tasks: [], collapsed: false, pinned: false,
|
|
570
583
|
lastActivity: Date.now(),
|
|
584
|
+
birthtimeMs: payload.birthtimeMs || 0,
|
|
571
585
|
};
|
|
572
586
|
sessions.push(session);
|
|
573
587
|
sessionsMap.set(session.id, session);
|
|
@@ -575,7 +589,7 @@ function handleNewSession(payload) {
|
|
|
575
589
|
rebuildNodes();
|
|
576
590
|
needsFullRender = true;
|
|
577
591
|
visibleDirty = true;
|
|
578
|
-
|
|
592
|
+
scheduleTreeRender();
|
|
579
593
|
}
|
|
580
594
|
|
|
581
595
|
function handleNewAgent(payload) {
|
|
@@ -590,7 +604,7 @@ function handleNewAgent(payload) {
|
|
|
590
604
|
rebuildNodes();
|
|
591
605
|
needsFullRender = true;
|
|
592
606
|
visibleDirty = true;
|
|
593
|
-
|
|
607
|
+
scheduleTreeRender();
|
|
594
608
|
}
|
|
595
609
|
|
|
596
610
|
function handleNewBgTask(payload) {
|
|
@@ -602,7 +616,7 @@ function handleNewBgTask(payload) {
|
|
|
602
616
|
isComplete: payload.isComplete,
|
|
603
617
|
});
|
|
604
618
|
rebuildNodes();
|
|
605
|
-
|
|
619
|
+
scheduleTreeRender();
|
|
606
620
|
}
|
|
607
621
|
|
|
608
622
|
function handleSessionRemoved(payload) {
|
|
@@ -615,7 +629,7 @@ function handleSessionRemoved(payload) {
|
|
|
615
629
|
rebuildNodes();
|
|
616
630
|
needsFullRender = true;
|
|
617
631
|
visibleDirty = true;
|
|
618
|
-
|
|
632
|
+
scheduleTreeRender();
|
|
619
633
|
}
|
|
620
634
|
|
|
621
635
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
@@ -710,10 +724,30 @@ function isItemVisible(item) {
|
|
|
710
724
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
711
725
|
|
|
712
726
|
function rebuildNodes() {
|
|
713
|
-
|
|
727
|
+
// Sort sessions by creation time, newest first
|
|
728
|
+
sessions.sort((a, b) => (b.birthtimeMs || 0) - (a.birthtimeMs || 0));
|
|
729
|
+
|
|
730
|
+
const today = new Date();
|
|
731
|
+
const todayStr = `${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
|
|
732
|
+
|
|
733
|
+
const todaySessions = [];
|
|
734
|
+
const olderByDate = new Map(); // dateStr -> [sessions]
|
|
735
|
+
|
|
714
736
|
for (const s of sessions) {
|
|
715
|
-
|
|
716
|
-
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;
|
|
717
751
|
const agents = s.agents || [];
|
|
718
752
|
const lastAgentIdx = agents.length - 1;
|
|
719
753
|
for (let ai = 0; ai < agents.length; ai++) {
|
|
@@ -745,15 +779,58 @@ function rebuildNodes() {
|
|
|
745
779
|
}
|
|
746
780
|
}
|
|
747
781
|
}
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
782
|
+
|
|
783
|
+
// Today's sessions (expanded)
|
|
784
|
+
for (const s of todaySessions) {
|
|
785
|
+
addSessionWithChildren(s, false);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// Date folders (older dates, collapsed by default)
|
|
789
|
+
const sortedDates = [...olderByDate.keys()].sort((a, b) => b.localeCompare(a));
|
|
790
|
+
for (let di = 0; di < sortedDates.length; di++) {
|
|
791
|
+
const dateStr = sortedDates[di];
|
|
792
|
+
const folderSessions = olderByDate.get(dateStr);
|
|
793
|
+
const collapsed = folderCollapsed[dateStr] !== false; // default collapsed
|
|
794
|
+
const isLastFolder = di === sortedDates.length - 1;
|
|
795
|
+
|
|
796
|
+
treeNodes.push({
|
|
797
|
+
type: 'date-folder', date: dateStr, level: 0, isLast: false,
|
|
798
|
+
collapsed, sessionCount: folderSessions.length,
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
if (!collapsed) {
|
|
802
|
+
for (const s of folderSessions) {
|
|
803
|
+
addSessionWithChildren(s, true);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// Mark last session among today's sessions
|
|
809
|
+
const todaySessionNodes = treeNodes.filter(n => n.type === 'session' && !n.inFolder);
|
|
810
|
+
if (todaySessionNodes.length > 0) todaySessionNodes[todaySessionNodes.length - 1].isLast = true;
|
|
811
|
+
|
|
812
|
+
// Mark last session inside each folder
|
|
813
|
+
for (const dateStr of sortedDates) {
|
|
814
|
+
if (folderCollapsed[dateStr] !== false) continue;
|
|
815
|
+
const folderSessionNodes = treeNodes.filter(n => n.type === 'session' && n.inFolder);
|
|
816
|
+
// Find sessions belonging to this folder
|
|
817
|
+
const thisFolder = [];
|
|
818
|
+
let inThisFolder = false;
|
|
819
|
+
for (const n of treeNodes) {
|
|
820
|
+
if (n.type === 'date-folder' && n.date === dateStr) { inThisFolder = true; continue; }
|
|
821
|
+
if (n.type === 'date-folder' && n.date !== dateStr) { inThisFolder = false; continue; }
|
|
822
|
+
if (inThisFolder && n.type === 'session') thisFolder.push(n);
|
|
823
|
+
}
|
|
824
|
+
if (thisFolder.length > 0) thisFolder[thisFolder.length - 1].isLast = true;
|
|
825
|
+
}
|
|
751
826
|
|
|
752
827
|
if (treeCursor >= treeNodes.length) treeCursor = Math.max(0, treeNodes.length - 1);
|
|
753
828
|
}
|
|
754
829
|
|
|
755
830
|
function treePrefix(node) {
|
|
756
|
-
if (node.level === 0)
|
|
831
|
+
if (node.level === 0) {
|
|
832
|
+
return node.inFolder ? ' ' : '';
|
|
833
|
+
}
|
|
757
834
|
const branch = node.isLast ? '└── ' : '├── ';
|
|
758
835
|
if (node.level === 1) return branch;
|
|
759
836
|
// Level 2: need to check if parent agent is last
|
|
@@ -767,6 +844,17 @@ function getNodeHTML(node, idx) {
|
|
|
767
844
|
const isSelected = idx === treeCursor;
|
|
768
845
|
const selClass = isSelected ? ' selected' : '';
|
|
769
846
|
|
|
847
|
+
if (node.type === 'date-folder') {
|
|
848
|
+
const icon = node.collapsed ? '▸' : '▾';
|
|
849
|
+
return `<div class="tree-row tree-row-folder${selClass ? ' selected' : ''}">
|
|
850
|
+
<div class="tree-content" onclick="treeClick(${idx})" data-idx="${idx}">
|
|
851
|
+
<div class="tree-node folder-node">
|
|
852
|
+
${icon} 📁 ${node.date} <span style="font-size:10px;color:var(--dim);margin-left:4px">(${node.sessionCount})</span>
|
|
853
|
+
</div>
|
|
854
|
+
</div>
|
|
855
|
+
</div>`;
|
|
856
|
+
}
|
|
857
|
+
|
|
770
858
|
if (node.type === 'session') {
|
|
771
859
|
const displayName = node.title || folderName(node.projectPath) || node.id.slice(0, 14);
|
|
772
860
|
const parts = [];
|
|
@@ -774,12 +862,15 @@ function getNodeHTML(node, idx) {
|
|
|
774
862
|
const activeDot = isSessionActive(node) ? '<span class="active-dot on">🟢</span>' : '<span class="active-dot off">⚪</span>';
|
|
775
863
|
const subInfo = parts.length > 0 ? ` <span style="color:#6b7280;font-size:10px">${parts.join(' · ')}</span>` : '';
|
|
776
864
|
const agentCount = node.agents ? node.agents.filter(a => a.type === 'agent').length : 0;
|
|
777
|
-
|
|
865
|
+
const timeStr = formatTime(node.birthtimeMs);
|
|
866
|
+
const timeHtml = timeStr ? `<span style="margin-left:auto;font-size:10px;color:var(--dim);flex-shrink:0">${timeStr}</span>` : '';
|
|
867
|
+
return `<div class="tree-row tree-row-session${selClass ? ' selected' : ''}">
|
|
778
868
|
<div class="tree-content" onclick="treeClick(${idx})" data-idx="${idx}">
|
|
779
869
|
<div class="tree-node">
|
|
780
870
|
<span class="tree-prefix">${treePrefix(node)}</span>${activeDot} ${node.collapsed ? '▸' : '▾'} ${esc(displayName)}
|
|
781
871
|
${node.collapsed && agentCount > 0 ? `(${esc(String(agentCount))})` : ''}
|
|
782
872
|
${subInfo}
|
|
873
|
+
${timeHtml}
|
|
783
874
|
</div>
|
|
784
875
|
</div>
|
|
785
876
|
<span class="tree-actions">
|
|
@@ -862,20 +953,42 @@ function renderTree() {
|
|
|
862
953
|
treeCursorInfo.textContent = `${treeCursor + 1}/${treeNodes.length}`;
|
|
863
954
|
}
|
|
864
955
|
|
|
956
|
+
function updateTreeDots() {
|
|
957
|
+
const dots = treeEl.querySelectorAll('.active-dot');
|
|
958
|
+
const now = Date.now();
|
|
959
|
+
for (const dot of dots) {
|
|
960
|
+
const content = dot.closest('.tree-content');
|
|
961
|
+
if (!content) continue;
|
|
962
|
+
const idx = parseInt(content.getAttribute('data-idx'));
|
|
963
|
+
if (isNaN(idx)) continue;
|
|
964
|
+
const node = treeNodes[idx];
|
|
965
|
+
if (!node || node.type !== 'session') continue;
|
|
966
|
+
const active = isSessionActive(node);
|
|
967
|
+
const newCls = active ? 'active-dot on' : 'active-dot off';
|
|
968
|
+
const newHTML = active ? '🟢' : '⚪';
|
|
969
|
+
if (dot.className !== newCls) {
|
|
970
|
+
dot.className = newCls;
|
|
971
|
+
dot.innerHTML = newHTML;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
const ACTIVE_THRESHOLD = 600000; // 10 minutes
|
|
977
|
+
|
|
865
978
|
function isSessionActive(session) {
|
|
866
979
|
if (!session) return false;
|
|
867
980
|
const now = Date.now();
|
|
868
|
-
//
|
|
981
|
+
// Main agent: 10 minutes
|
|
869
982
|
const mainCtx = contextData[session.id + ':'];
|
|
870
|
-
if (mainCtx && (now - mainCtx.lastActivity) <
|
|
871
|
-
//
|
|
983
|
+
if (mainCtx && (now - mainCtx.lastActivity) < 600000) return true;
|
|
984
|
+
// Subagents: 3 minutes
|
|
872
985
|
for (const a of session.agents) {
|
|
873
986
|
if (a.id === '') continue;
|
|
874
987
|
const ctx = contextData[session.id + ':' + a.id];
|
|
875
|
-
if (ctx && (now - ctx.lastActivity) <
|
|
988
|
+
if (ctx && (now - ctx.lastActivity) < 180000) return true;
|
|
876
989
|
}
|
|
877
|
-
//
|
|
878
|
-
return (now - session.lastActivity) <
|
|
990
|
+
// Session fallback: 10 minutes
|
|
991
|
+
return (now - session.lastActivity) < 600000;
|
|
879
992
|
}
|
|
880
993
|
|
|
881
994
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
@@ -1051,6 +1164,13 @@ function refreshButtons() {
|
|
|
1051
1164
|
}
|
|
1052
1165
|
}
|
|
1053
1166
|
tokenInfo.textContent = tokStr;
|
|
1167
|
+
|
|
1168
|
+
// Footer version
|
|
1169
|
+
const vEl = document.getElementById('footer-version');
|
|
1170
|
+
if (vEl) {
|
|
1171
|
+
const v = appVersion ? `v${appVersion}` : '';
|
|
1172
|
+
vEl.innerHTML = `${v ? v + ' · ' : ''}<a href="https://github.com/shuxuecode/claude-watch" target="_blank" rel="noopener" style="color:var(--dim);display:inline-flex;align-items:center;gap:3px"><svg viewBox="0 0 16 16" width="12" height="12" fill="currentColor" style="vertical-align:middle"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg>claude-watch</a>`;
|
|
1173
|
+
}
|
|
1054
1174
|
}
|
|
1055
1175
|
|
|
1056
1176
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
@@ -1065,7 +1185,11 @@ function treeClick(idx) {
|
|
|
1065
1185
|
selectIndex(idx);
|
|
1066
1186
|
const node = treeNodes[idx];
|
|
1067
1187
|
if (!node) return;
|
|
1068
|
-
if (node.type === '
|
|
1188
|
+
if (node.type === 'date-folder') {
|
|
1189
|
+
node.collapsed = !node.collapsed;
|
|
1190
|
+
folderCollapsed[node.date] = node.collapsed;
|
|
1191
|
+
rebuildNodes();
|
|
1192
|
+
} else if (node.type === 'session') {
|
|
1069
1193
|
const session = sessions.find(s => s.id === node.id);
|
|
1070
1194
|
if (session) {
|
|
1071
1195
|
session.collapsed = !session.collapsed;
|
|
@@ -1206,7 +1330,7 @@ function toggleText() { showText = !showText; needsFullRender = true;
|
|
|
1206
1330
|
visibleDirty = true; renderStream(); refreshButtons(); }
|
|
1207
1331
|
function toggleHook() { showHook = !showHook; needsFullRender = true;
|
|
1208
1332
|
visibleDirty = true; renderStream(); refreshButtons(); }
|
|
1209
|
-
function toggleActivity() { showActivity = !showActivity; rebuildNodes();
|
|
1333
|
+
function toggleActivity() { showActivity = !showActivity; rebuildNodes(); scheduleTreeRender(); refreshButtons(); }
|
|
1210
1334
|
function toggleAutoScroll() { autoScroll = !autoScroll; if (autoScroll) streamEl.scrollTop = streamEl.scrollHeight; renderAll(); }
|
|
1211
1335
|
function toggleTree() { showTree = !showTree; document.getElementById('tree-panel').classList.toggle('hidden', !showTree); }
|
|
1212
1336
|
function toggleAutoDiscovery() { sendCmd('toggleAutoDiscovery'); }
|
|
@@ -1338,6 +1462,13 @@ function fmtDur(ms) {
|
|
|
1338
1462
|
return `(${(ms / 60000).toFixed(1)}m)`;
|
|
1339
1463
|
}
|
|
1340
1464
|
|
|
1465
|
+
function formatTime(ms) {
|
|
1466
|
+
if (!ms) return '';
|
|
1467
|
+
const d = new Date(ms);
|
|
1468
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
1469
|
+
return `${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1341
1472
|
function fmtTimestamp(ts) {
|
|
1342
1473
|
if (!ts) return '';
|
|
1343
1474
|
const d = ts instanceof Date ? ts : new Date(ts);
|
|
@@ -1363,6 +1494,17 @@ function renderAll() {
|
|
|
1363
1494
|
}
|
|
1364
1495
|
|
|
1365
1496
|
function scheduleRender() {
|
|
1497
|
+
if (!renderPending) {
|
|
1498
|
+
renderPending = true;
|
|
1499
|
+
requestAnimationFrame(() => {
|
|
1500
|
+
renderPending = false;
|
|
1501
|
+
renderStream();
|
|
1502
|
+
refreshButtons();
|
|
1503
|
+
});
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
function scheduleTreeRender() {
|
|
1366
1508
|
if (!renderPending) {
|
|
1367
1509
|
renderPending = true;
|
|
1368
1510
|
requestAnimationFrame(() => {
|
package/src/server/server.js
CHANGED
|
@@ -10,6 +10,8 @@ var { WebSocketServer } = require('ws');
|
|
|
10
10
|
var { Watcher, listSessions, listActiveSessions } = require('../watcher/watcher');
|
|
11
11
|
var { setDebugAll, contextWindowFor } = require('../parser/parser');
|
|
12
12
|
|
|
13
|
+
var PACKAGE_VERSION = require('../../package.json').version;
|
|
14
|
+
|
|
13
15
|
var MIME = {
|
|
14
16
|
'.html': 'text/html; charset=utf-8',
|
|
15
17
|
'.css': 'text/css; charset=utf-8',
|
|
@@ -20,7 +22,7 @@ var MIME = {
|
|
|
20
22
|
'.ico': 'image/x-icon',
|
|
21
23
|
};
|
|
22
24
|
|
|
23
|
-
var MAX_ITEM_BUFFER =
|
|
25
|
+
var MAX_ITEM_BUFFER = 9999;
|
|
24
26
|
var CONTEXT_STALE_MS = 60 * 60 * 1000; // 60 minutes
|
|
25
27
|
|
|
26
28
|
class DashboardServer {
|
|
@@ -279,6 +281,7 @@ class DashboardServer {
|
|
|
279
281
|
const sessions = this.watcher.getSessionsSnapshot().map(s => ({
|
|
280
282
|
id: s.id,
|
|
281
283
|
projectPath: s.projectPath,
|
|
284
|
+
birthtimeMs: s.birthtimeMs || 0,
|
|
282
285
|
subagents: Object.entries(s.subagentTypes || s.subagents || {}).reduce((acc, [id, type]) => {
|
|
283
286
|
acc[id] = typeof type === 'string' ? type : '';
|
|
284
287
|
return acc;
|
|
@@ -306,7 +309,7 @@ class DashboardServer {
|
|
|
306
309
|
}
|
|
307
310
|
|
|
308
311
|
sendConfig(ws) {
|
|
309
|
-
this.send(ws, 'config', { collapseAfter: this.collapseAfterMs });
|
|
312
|
+
this.send(ws, 'config', { collapseAfter: this.collapseAfterMs, version: PACKAGE_VERSION });
|
|
310
313
|
}
|
|
311
314
|
|
|
312
315
|
setupWatcher(watcherOpts) {
|
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
|
}
|