claude-code-watch 0.0.25 → 0.1.1
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/bin/claude-watch.js +1 -1
- package/package.json +1 -1
- package/public/index.html +101 -10
- package/src/parser/parser.js +13 -3
- package/src/server/server.js +21 -3
- package/src/watcher/watcher.js +43 -13
package/bin/claude-watch.js
CHANGED
package/package.json
CHANGED
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
|
|
478
|
-
const taskDescriptions = new
|
|
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
|
|
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(
|
|
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
|
|
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.
|
|
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.
|
|
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 = `<button class="tip-copy-btn" onclick="event.stopPropagation();copySessionId(this)">Copy</button><code>${esc(sid)}</code>`;
|
|
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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
1760
|
+
return (s ?? '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''').replace(/\\/g, '\');
|
|
1670
1761
|
}
|
|
1671
1762
|
|
|
1672
1763
|
function fmtDur(ms) {
|
package/src/parser/parser.js
CHANGED
|
@@ -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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
|
package/src/server/server.js
CHANGED
|
@@ -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
|
-
|
|
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)
|
|
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
|
|
package/src/watcher/watcher.js
CHANGED
|
@@ -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))
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
}
|