claude-code-watch 0.0.25 → 0.1.0

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.
@@ -286,7 +286,7 @@ async function main() {
286
286
  }
287
287
 
288
288
  checkForUpdate();
289
- startServer(options);
289
+ await startServer(options);
290
290
  }
291
291
 
292
292
  main().catch(err => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-watch",
3
- "version": "0.0.25",
3
+ "version": "0.1.0",
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
@@ -190,7 +190,20 @@ body {
190
190
  background: rgba(255,255,255,0.08);
191
191
  padding: 0 3px; border-radius: 3px; flex-shrink: 0; font-family: monospace;
192
192
  letter-spacing: 0.5px; vertical-align: middle; font-weight: 600;
193
+ position: relative; cursor: pointer;
193
194
  }
195
+ .session-id-tip {
196
+ position: fixed; z-index: 10000;
197
+ background: var(--bg2); border: 1px solid var(--border); border-radius: 4px;
198
+ padding: 4px 8px; font-size: 11px; white-space: nowrap; color: var(--text);
199
+ box-shadow: 0 4px 12px rgba(0,0,0,0.4); display: flex; align-items: center; gap: 6px;
200
+ }
201
+ .session-id-tip code { font-family: monospace; user-select: all; color: var(--white); }
202
+ .session-id-tip .tip-copy-btn {
203
+ background: var(--bg3); border: 1px solid var(--border); border-radius: 3px;
204
+ color: var(--text); font-size: 10px; padding: 1px 5px; cursor: pointer;
205
+ }
206
+ .session-id-tip .tip-copy-btn:hover { background: var(--accent); color: #fff; }
194
207
  .tree-actions { display: none; gap: 2px; padding-right: 4px; }
195
208
  .tree-row:hover .tree-actions { display: flex; }
196
209
  .tree-row.selected>.tree-actions { display: flex; }
@@ -226,6 +239,15 @@ body {
226
239
  .stream-line.agent-tag .tag-label { flex-shrink: 0; }
227
240
  .stream-line.agent-tag .timestamp { font-weight: normal; font-size: 0.85em; color: var(--dim); white-space: nowrap; }
228
241
  .stream-line.separator { color: var(--dim); }
242
+ .stream-line.user-prompt-block {
243
+ background: rgba(59,130,246,0.08);
244
+ border-left: 8px solid var(--blue);
245
+ border-radius: 4px;
246
+ padding: 6px 10px;
247
+ margin: 4px 0;
248
+ color: var(--white);
249
+ line-height: 1.5;
250
+ }
229
251
 
230
252
  /* ── Footer ── */
231
253
  #footer {
@@ -363,6 +385,7 @@ body {
363
385
  <button class="btn on" id="btn-tool-output" onclick="toggleToolOutput()" data-tooltip="Toggle tool output">📤 Output</button>
364
386
  <button class="btn on" id="btn-text" onclick="toggleText()" data-tooltip="Toggle text responses">💬 Text</button>
365
387
  <button class="btn on" id="btn-hook" onclick="toggleHook()" data-tooltip="Toggle hook output">🪝 Hook</button>
388
+ <button class="btn on" id="btn-user-prompt" onclick="toggleUserPrompt()" data-tooltip="Toggle user prompt">👤 Prompt</button>
366
389
  <span class="sep">│</span>
367
390
  <button class="btn on" id="btn-autoscroll" onclick="toggleAutoScroll()" data-tooltip="Auto-scroll">⇣ Auto</button>
368
391
  <button class="btn" id="btn-tree-toggle" onclick="toggleTree()" data-tooltip="Toggle tree panel">📂 Tree</button>
@@ -471,11 +494,13 @@ class LRUCache {
471
494
  has(key) { if (!this.map.has(key)) return false; const v = this.map.get(key); this.map.delete(key); this.map.set(key, v); return true; }
472
495
  get(key) { if (!this.map.has(key)) return undefined; const v = this.map.get(key); this.map.delete(key); this.map.set(key, v); return v; }
473
496
  set(key, val) { if (this.map.has(key)) this.map.delete(key); this.map.set(key, val); if (this.map.size > this.max) { const oldest = this.map.keys().next().value; this.map.delete(oldest); } }
497
+ delete(key) { return this.map.delete(key); }
498
+ keys() { return this.map.keys(); }
474
499
  }
475
500
  const seenToolIDs = new LRUCache(20000);
476
501
  const toolNameMap = new LRUCache(2000);
477
- const agentActivity = new Map(); // "sessionID:agentID" → { toolName, content }
478
- const taskDescriptions = new Map(); // toolID → description string
502
+ const agentActivity = new LRUCache(500); // "sessionID:agentID" → { toolName, content }
503
+ const taskDescriptions = new LRUCache(2000); // toolID → description string
479
504
  const MAX_DESC_STORE = 200;
480
505
  let filters = new Map();
481
506
  let visibleFilterCount = 0;
@@ -485,6 +510,7 @@ let showToolInput = true;
485
510
  let showToolOutput = true;
486
511
  let showText = true;
487
512
  let showHook = true;
513
+ let showUserPrompt = true;
488
514
  let showActivity = true;
489
515
  let autoDiscovery = true;
490
516
  let appVersion = '';
@@ -612,7 +638,7 @@ function connect() {
612
638
  reconnectTimer = setTimeout(connect, reconnectDelay);
613
639
  reconnectDelay = Math.min(reconnectDelay * 2, MaxReconnectDelay);
614
640
  };
615
- ws.onerror = () => {};
641
+ ws.onerror = (e) => { console.warn('[ws] connection error', e); };
616
642
 
617
643
  ws.onmessage = (e) => {
618
644
  lastMsgTime = Date.now();
@@ -669,6 +695,14 @@ function sendCmd(action, extra = {}) {
669
695
 
670
696
  function handleSnapshot(payload) {
671
697
  autoDiscovery = payload.autoDiscovery;
698
+ const incomingIDs = new Set((payload.sessions || []).map(s => s.id));
699
+ for (let i = sessions.length - 1; i >= 0; i--) {
700
+ const s = sessions[i];
701
+ if (!incomingIDs.has(s.id) && !s.pinned) {
702
+ sessions.splice(i, 1);
703
+ sessionsMap.delete(s.id);
704
+ }
705
+ }
672
706
  for (const s of (payload.sessions || [])) {
673
707
  if (hiddenSessionIDs.has(s.id)) continue;
674
708
  let session = sessionsMap.get(s.id);
@@ -758,10 +792,16 @@ function handleNewBgTask(payload) {
758
792
  }
759
793
 
760
794
  function handleSessionRemoved(payload) {
761
- const idx = sessions.findIndex(s => s.id === payload.sessionID);
795
+ const sid = payload.sessionID;
796
+ const s = sessionsMap.get(sid);
797
+ if (s) {
798
+ for (const a of s.agents) agentActivity.delete(sid + ':' + a.id);
799
+ for (const t of s.tasks) taskDescriptions.delete(t.id);
800
+ }
801
+ const idx = sessions.findIndex(s => s.id === sid);
762
802
  if (idx >= 0) {
763
803
  sessions.splice(idx, 1);
764
- sessionsMap.delete(payload.sessionID);
804
+ sessionsMap.delete(sid);
765
805
  }
766
806
  updateFilters();
767
807
  rebuildNodes();
@@ -855,7 +895,7 @@ function isItemVisible(item) {
855
895
  case 'tool_output': return showToolOutput;
856
896
  case 'text': return showText;
857
897
  case 'hook_output': return showHook;
858
- case 'user_text': return false;
898
+ case 'user_text': return showUserPrompt;
859
899
  default: return true;
860
900
  }
861
901
  }
@@ -1005,7 +1045,7 @@ function getNodeHTML(node, idx) {
1005
1045
  }
1006
1046
 
1007
1047
  if (node.type === 'session') {
1008
- const displayName = node.title || folderName(node.projectPath) || node.id.slice(0, 14);
1048
+ const displayName = folderName(node.projectPath) || node.title || node.id.slice(0, 14);
1009
1049
  const parts = [];
1010
1050
  if (node.model) parts.push(`🧠 ${esc(node.model)}`);
1011
1051
  const activeDot = isSessionActive(node) ? '<span class="active-dot on">🟢</span>' : '<span class="active-dot off">⚪</span>';
@@ -1016,7 +1056,7 @@ function getNodeHTML(node, idx) {
1016
1056
  return `<div class="tree-row tree-row-session${selClass ? ' selected' : ''}">
1017
1057
  <div class="tree-content" onclick="treeClick(${idx})" data-idx="${idx}">
1018
1058
  <div class="tree-node">
1019
- <span class="tree-prefix">${treePrefix(node)}</span>${activeDot} ${node.collapsed ? '▸' : '▾'} <span class="session-prefix" style="color:${idColor(node.colorRank)}">${esc(node.id.split('-')[0].toUpperCase())}</span> ${esc(displayName)}
1059
+ <span class="tree-prefix">${treePrefix(node)}</span>${activeDot} ${node.collapsed ? '▸' : '▾'} <span class="session-prefix" style="color:${idColor(node.colorRank)}" data-sid="${esc(node.id)}" onmouseenter="showSessionIdTip(this)" onmouseleave="hideSessionIdTip(this)">${esc(node.id.split('-')[0].toUpperCase())}</span> ${esc(displayName)}
1020
1060
  ${node.collapsed && agentCount > 0 ? `(${esc(String(agentCount))})` : ''}
1021
1061
  ${subInfo}
1022
1062
  ${timeHtml}
@@ -1308,6 +1348,10 @@ function renderItem(item) {
1308
1348
  for (const l of truncContent(item.content)) lines.push({ cls: 'stream-line debug', text: l, sessionID: sid });
1309
1349
  break;
1310
1350
  }
1351
+ case 'user_text':
1352
+ lines.push({ cls: agentTagCls, text: `<span class="tag-label">${agentLabel}${sep}👤 User Prompt</span>${tsHtml}`, html: true, sessionID: sid });
1353
+ lines.push({ cls: 'stream-line user-prompt-block md-content', text: mdRender(item.content), html: true, sessionID: sid });
1354
+ break;
1311
1355
  }
1312
1356
 
1313
1357
  lines.push({ cls: 'stream-line separator', text: '─'.repeat(60), sessionID: sid });
@@ -1329,6 +1373,7 @@ function refreshButtons() {
1329
1373
  document.getElementById('btn-tool-output').classList.toggle('on', showToolOutput);
1330
1374
  document.getElementById('btn-text').classList.toggle('on', showText);
1331
1375
  document.getElementById('btn-hook').classList.toggle('on', showHook);
1376
+ document.getElementById('btn-user-prompt').classList.toggle('on', showUserPrompt);
1332
1377
  document.getElementById('btn-activity').classList.toggle('on', showActivity);
1333
1378
  document.getElementById('btn-autoscroll').classList.toggle('on', autoScroll);
1334
1379
  document.getElementById('btn-tree-toggle').classList.toggle('on', showTree);
@@ -1339,7 +1384,7 @@ function refreshButtons() {
1339
1384
  if (sessions.length === 0) info = 'Waiting...';
1340
1385
  else if (sessions.length === 1) {
1341
1386
  const s = sessions[0];
1342
- info = (s.title || folderName(s.projectPath) || s.id.slice(0, 14));
1387
+ info = (folderName(s.projectPath) || s.title || s.id.slice(0, 14));
1343
1388
  } else info = sessions.length + ' sessions';
1344
1389
  if (!autoDiscovery) info += ' [paused]';
1345
1390
  sessionInfo.textContent = info;
@@ -1363,6 +1408,50 @@ function refreshButtons() {
1363
1408
  }
1364
1409
  }
1365
1410
 
1411
+ // ══════════════════════════════════════════════════════════════════════════════
1412
+ // Session ID tooltip
1413
+ // ══════════════════════════════════════════════════════════════════════════════
1414
+
1415
+ let sessionIdTipTimer = null;
1416
+ let sessionIdTipEl = null;
1417
+ function showSessionIdTip(el) {
1418
+ hideAllSessionIdTips();
1419
+ const sid = el.getAttribute('data-sid');
1420
+ if (!sid) return;
1421
+ sessionIdTipTimer = setTimeout(() => {
1422
+ const rect = el.getBoundingClientRect();
1423
+ const tip = document.createElement('div');
1424
+ tip.className = 'session-id-tip';
1425
+ tip.style.top = (rect.bottom + 4) + 'px';
1426
+ tip.style.left = rect.left + 'px';
1427
+ tip.innerHTML = `<code>${esc(sid)}</code><button class="tip-copy-btn" onclick="event.stopPropagation();copySessionId(this)">Copy</button>`;
1428
+ tip.onmouseenter = () => clearTimeout(sessionIdTipTimer);
1429
+ tip.onmouseleave = () => { hideAllSessionIdTips(); };
1430
+ document.body.appendChild(tip);
1431
+ sessionIdTipEl = tip;
1432
+ el._tip = tip;
1433
+ }, 300);
1434
+ }
1435
+ function hideSessionIdTip(el) {
1436
+ sessionIdTipTimer = setTimeout(() => {
1437
+ if (el._tip) { el._tip.remove(); el._tip = null; }
1438
+ sessionIdTipEl = null;
1439
+ }, 200);
1440
+ }
1441
+ function hideAllSessionIdTips() {
1442
+ clearTimeout(sessionIdTipTimer);
1443
+ document.querySelectorAll('.session-id-tip').forEach(t => t.remove());
1444
+ sessionIdTipEl = null;
1445
+ }
1446
+ function copySessionId(btn) {
1447
+ const code = btn.parentElement.querySelector('code');
1448
+ if (!code) return;
1449
+ navigator.clipboard.writeText(code.textContent).then(() => {
1450
+ btn.textContent = 'Copied!';
1451
+ setTimeout(() => { btn.closest('.session-id-tip')?.remove(); }, 800);
1452
+ });
1453
+ }
1454
+
1366
1455
  // ══════════════════════════════════════════════════════════════════════════════
1367
1456
  // Actions
1368
1457
  // ══════════════════════════════════════════════════════════════════════════════
@@ -1522,6 +1611,8 @@ function toggleText() { showText = !showText; needsFullRender = true;
1522
1611
  visibleDirty = true; renderStream(); refreshButtons(); }
1523
1612
  function toggleHook() { showHook = !showHook; needsFullRender = true;
1524
1613
  visibleDirty = true; renderStream(); refreshButtons(); }
1614
+ function toggleUserPrompt() { showUserPrompt = !showUserPrompt; needsFullRender = true;
1615
+ visibleDirty = true; renderStream(); refreshButtons(); }
1525
1616
  function toggleActivity() { showActivity = !showActivity; rebuildNodes(); scheduleRender(); refreshButtons(); }
1526
1617
  function toggleAutoScroll() { autoScroll = !autoScroll; if (autoScroll) streamEl.scrollTop = streamEl.scrollHeight; renderAll(); }
1527
1618
  function toggleTree() { showTree = !showTree; document.getElementById('tree-panel').classList.toggle('hidden', !showTree); }
@@ -1666,7 +1757,7 @@ function folderName(projectPath) {
1666
1757
  }
1667
1758
 
1668
1759
  function esc(s) {
1669
- return (s ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#x27;');
1760
+ return (s ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#x27;').replace(/\\/g, '&#x5C;');
1670
1761
  }
1671
1762
 
1672
1763
  function fmtDur(ms) {
@@ -196,11 +196,21 @@ function formatTokenCount(n) {
196
196
  return String(n);
197
197
  }
198
198
 
199
+ const CONTEXT_WINDOWS = {
200
+ 'claude-opus-4-7': 1000000,
201
+ 'claude-opus-4-6': 200000,
202
+ 'claude-sonnet-4-6': 1000000,
203
+ 'claude-sonnet-4-5': 200000,
204
+ 'claude-haiku-4-5': 200000,
205
+ 'claude-haiku-4': 200000,
206
+ };
207
+
199
208
  function contextWindowFor(model) {
200
209
  if (!model) return 200000;
201
- if (model.startsWith('claude-opus-4-7') || model.startsWith('claude-sonnet-4-6')) return 1000000;
202
- if (model.startsWith('claude-haiku-4-5') || model.startsWith('claude-opus-4-6') ||
203
- model.startsWith('claude-sonnet-4-5') || model.startsWith('claude-haiku-4')) return 200000;
210
+ for (const [prefix, win] of Object.entries(CONTEXT_WINDOWS)) {
211
+ if (model.startsWith(prefix)) return win;
212
+ }
213
+ if (/claude-(opus|sonnet)/.test(model)) return 1000000;
204
214
  return 200000;
205
215
  }
206
216
 
@@ -255,7 +255,9 @@ class DashboardServer {
255
255
  this.clients.delete(ws);
256
256
  });
257
257
 
258
- ws.on('error', () => {});
258
+ ws.on('error', (err) => {
259
+ if (this.debugAll) console.error('[server] WS client error:', err.message);
260
+ });
259
261
 
260
262
  this.sendSnapshot(ws);
261
263
  this.sendItemBatch(ws);
@@ -396,7 +398,10 @@ class DashboardServer {
396
398
  try {
397
399
  const result = cp.execSync(cmd, { encoding: 'utf-8' }).trim();
398
400
  if (!result) return false;
399
- const pids = result.split('\n').map(s => s.trim()).filter(Boolean);
401
+ let pids = result.split('\n').map(s => s.trim()).filter(Boolean);
402
+ if (process.platform === 'win32') {
403
+ pids = pids.map(line => line.split(/\s+/).pop());
404
+ }
400
405
 
401
406
  // Ask user for confirmation before killing
402
407
  const confirmed = await askYesNo(`Port ${port} is occupied by process(es) ${pids.join(', ')}. Kill them? [y/N] `);
@@ -539,7 +544,14 @@ class DashboardServer {
539
544
  stop() {
540
545
  if (this._contextCleanupTimer) clearInterval(this._contextCleanupTimer);
541
546
  if (this._heartbeatTimer) clearInterval(this._heartbeatTimer);
542
- if (this._flushTimer) clearTimeout(this._flushTimer);
547
+ if (this._flushTimer) {
548
+ clearTimeout(this._flushTimer);
549
+ this._flushTimer = null;
550
+ }
551
+ if (this._pendingItems.length > 0) {
552
+ this.broadcast('itemBatch', this._pendingItems);
553
+ this._pendingItems = [];
554
+ }
543
555
  if (this.wss) this.wss.close();
544
556
  if (this.server) this.server.close();
545
557
  if (this.watcher) this.watcher.stop();
@@ -549,6 +561,12 @@ class DashboardServer {
549
561
 
550
562
  async function startServer(options = {}) {
551
563
  const ds = new DashboardServer(options);
564
+ const shutdown = () => {
565
+ ds.stop();
566
+ process.exit(0);
567
+ };
568
+ process.on('SIGINT', shutdown);
569
+ process.on('SIGTERM', shutdown);
552
570
  return ds.start(options);
553
571
  }
554
572
 
@@ -33,9 +33,22 @@ function getClaudeProjectsDir() {
33
33
  }
34
34
 
35
35
  const _projectPathCache = new Map();
36
+ const _PROJECT_PATH_CACHE_MAX = 500;
37
+
38
+ function _projectPathCacheSet(key, value) {
39
+ _projectPathCache.set(key, value);
40
+ if (_projectPathCache.size > _PROJECT_PATH_CACHE_MAX) {
41
+ _projectPathCache.delete(_projectPathCache.keys().next().value);
42
+ }
43
+ }
36
44
 
37
45
  async function resolveProjectPath(encoded) {
38
- if (_projectPathCache.has(encoded)) return _projectPathCache.get(encoded);
46
+ if (_projectPathCache.has(encoded)) {
47
+ const v = _projectPathCache.get(encoded);
48
+ _projectPathCache.delete(encoded);
49
+ _projectPathCacheSet(encoded, v);
50
+ return v;
51
+ }
39
52
  let s = encoded;
40
53
  if (s.startsWith('-')) s = s.slice(1);
41
54
  if (!s) return '';
@@ -48,7 +61,7 @@ async function resolveProjectPath(encoded) {
48
61
  // Strategy 1: try direct decoded path on disk (handles dots correctly)
49
62
  try {
50
63
  await fsp.access('/' + directDecoded);
51
- _projectPathCache.set(encoded, directDecoded);
64
+ _projectPathCacheSet(encoded, directDecoded);
52
65
  return directDecoded;
53
66
  } catch {}
54
67
 
@@ -77,7 +90,7 @@ async function resolveProjectPath(encoded) {
77
90
  try {
78
91
  await fsp.access(testPath);
79
92
  const result = `${pathPart}/${dirPart}`;
80
- _projectPathCache.set(encoded, result);
93
+ _projectPathCacheSet(encoded, result);
81
94
  return result;
82
95
  } catch {
83
96
  // Path doesn't exist, try next combination
@@ -85,7 +98,7 @@ async function resolveProjectPath(encoded) {
85
98
  }
86
99
 
87
100
  // Fallback: return direct decoded path (correct even if path no longer exists on disk)
88
- _projectPathCache.set(encoded, directDecoded);
101
+ _projectPathCacheSet(encoded, directDecoded);
89
102
  return directDecoded;
90
103
  }
91
104
 
@@ -609,10 +622,10 @@ class Watcher extends EventEmitter {
609
622
  const pending = this.pendingSubagents.get(sessionID) || [];
610
623
  if (!pending.includes(p)) pending.push(p);
611
624
  this.pendingSubagents.set(sessionID, pending);
612
- return;
625
+ return Promise.resolve();
613
626
  }
614
627
 
615
- this._registerSubagent(session, sessionID, agentID, p).catch(err => {
628
+ return this._registerSubagent(session, sessionID, agentID, p).catch(err => {
616
629
  if (this.debug) console.error('[watcher] _registerSubagent error:', err.message);
617
630
  });
618
631
  }
@@ -914,7 +927,7 @@ class Watcher extends EventEmitter {
914
927
 
915
928
  async _initializeSessionReading(sessions) {
916
929
  let shouldSkip = this.skipHistory;
917
- if (!shouldSkip) {
930
+ if (!shouldSkip && !this._sessionID) {
918
931
  let totalEstimate = 0;
919
932
  for (const session of sessions) {
920
933
  totalEstimate += await this._estimateFileLines(session.mainFile);
@@ -1007,15 +1020,15 @@ class Watcher extends EventEmitter {
1007
1020
  await prev;
1008
1021
 
1009
1022
  let handle;
1010
- const pos = this.filePositions.get(filePath) || 0;
1023
+ let pos = this.filePositions.get(filePath) || 0;
1011
1024
  let newPos = pos;
1012
1025
  try {
1013
1026
  handle = await fsp.open(filePath, 'r');
1014
1027
  const stats = await handle.stat();
1015
1028
  if (pos > stats.size) {
1016
- // File was truncated — reset position to 0 so we re-read from the start
1029
+ // File was truncated — reset position to 0 and re-read from the start
1030
+ pos = 0;
1017
1031
  this.filePositions.set(filePath, 0);
1018
- await handle.close(); handle = null; return;
1019
1032
  }
1020
1033
  if (pos === stats.size) { await handle.close(); handle = null; return; }
1021
1034
 
@@ -1027,7 +1040,8 @@ class Watcher extends EventEmitter {
1027
1040
  const buf = Buffer.alloc(MaxReadChunk);
1028
1041
 
1029
1042
  while (true) {
1030
- const readFrom = newPos + carryOverBytes;
1043
+ const prevCarryOverBytes = carryOverBytes;
1044
+ const readFrom = newPos + prevCarryOverBytes;
1031
1045
  if (readFrom >= fileSize) break;
1032
1046
 
1033
1047
  const readLen = Math.min(MaxReadChunk, fileSize - readFrom);
@@ -1055,7 +1069,9 @@ class Watcher extends EventEmitter {
1055
1069
  carryOverBytes = 0;
1056
1070
  }
1057
1071
 
1058
- let chunkBytes = 0;
1072
+ // Start from prevCarryOverBytes so carryOver bytes from the previous
1073
+ // iteration are counted exactly once toward newPos advancement.
1074
+ let chunkBytes = prevCarryOverBytes;
1059
1075
 
1060
1076
  for (let i = 0; i < rawLines.length; i++) {
1061
1077
  let rawLine = rawLines[i];
@@ -1155,6 +1171,7 @@ class Watcher extends EventEmitter {
1155
1171
  try { await fsp.access(p); } catch {
1156
1172
  this.filePositions.delete(p);
1157
1173
  this.fileContexts.delete(p);
1174
+ this._readLocks.delete(p);
1158
1175
  }
1159
1176
  }
1160
1177
 
@@ -1172,6 +1189,10 @@ class Watcher extends EventEmitter {
1172
1189
  this.emit('broadcast', 'sessionRemoved', { sessionID });
1173
1190
  }
1174
1191
  }
1192
+
1193
+ for (const sid of this.pendingSubagents.keys()) {
1194
+ if (!this.sessions.has(sid)) this.pendingSubagents.delete(sid);
1195
+ }
1175
1196
  }
1176
1197
 
1177
1198
  // =========================================================================
@@ -1190,15 +1211,20 @@ class Watcher extends EventEmitter {
1190
1211
  if (p) {
1191
1212
  this.fileContexts.delete(p);
1192
1213
  this.filePositions.delete(p);
1214
+ this._readLocks.delete(p);
1193
1215
  const timer = this.debounceTimers.get(p);
1194
1216
  if (timer) {
1195
1217
  clearTimeout(timer);
1196
1218
  this.debounceTimers.delete(p);
1197
1219
  }
1220
+ if (this.watcher) {
1221
+ this.watcher.unwatch(p);
1222
+ }
1198
1223
  }
1199
1224
  }
1200
1225
  }
1201
1226
  this.sessions.delete(sessionID);
1227
+ this.pendingSubagents.delete(sessionID);
1202
1228
  if (session) {
1203
1229
  this.emit('sessionRemoved', { sessionID });
1204
1230
  }
@@ -1237,7 +1263,11 @@ function createWalkDir(readdirFn) {
1237
1263
  callback(fullPath, stats);
1238
1264
  }
1239
1265
  }
1240
- } catch {}
1266
+ } catch (err) {
1267
+ if (err.code !== 'ENOENT' && err.code !== 'EACCES') {
1268
+ console.error(`[watcher] _walkDir error on ${dir}: ${err.message}`);
1269
+ }
1270
+ }
1241
1271
  };
1242
1272
  return walk;
1243
1273
  }