claude-code-watch 0.0.24 → 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.
- package/bin/claude-watch.js +1 -1
- package/package.json +1 -1
- package/public/index.html +305 -16
- 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 {
|
|
@@ -286,6 +308,73 @@ body {
|
|
|
286
308
|
|
|
287
309
|
/* Theme toggle button */
|
|
288
310
|
#btn-theme { font-size: 14px; }
|
|
311
|
+
|
|
312
|
+
/* ── Export modal ── */
|
|
313
|
+
.modal-overlay {
|
|
314
|
+
position: fixed; inset: 0;
|
|
315
|
+
background: rgba(0, 0, 0, 0.6);
|
|
316
|
+
z-index: 10000;
|
|
317
|
+
display: flex; align-items: center; justify-content: center;
|
|
318
|
+
}
|
|
319
|
+
:root[data-theme="light"] .modal-overlay { background: rgba(0, 0, 0, 0.3); }
|
|
320
|
+
:root[data-theme="light"] .modal-session-row.selected { background: rgba(124, 58, 237, 0.2); }
|
|
321
|
+
|
|
322
|
+
.modal-box {
|
|
323
|
+
background: var(--bg); border: 1px solid var(--border); border-radius: 8px;
|
|
324
|
+
width: 480px; max-width: 90vw; max-height: 80vh;
|
|
325
|
+
display: flex; flex-direction: column; overflow: hidden;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.modal-header {
|
|
329
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
330
|
+
padding: 8px 12px; border-bottom: 1px solid var(--border); background: var(--bg2); flex-shrink: 0;
|
|
331
|
+
}
|
|
332
|
+
.modal-title { font-size: 13px; font-weight: 600; color: var(--white); }
|
|
333
|
+
|
|
334
|
+
.modal-toolbar {
|
|
335
|
+
display: flex; align-items: center; gap: 4px;
|
|
336
|
+
padding: 6px 12px; border-bottom: 1px solid var(--border); flex-shrink: 0;
|
|
337
|
+
}
|
|
338
|
+
.modal-count { margin-left: auto; font-size: 11px; color: var(--dim); }
|
|
339
|
+
|
|
340
|
+
.modal-body { flex: 1; overflow-y: auto; padding: 6px 0; }
|
|
341
|
+
|
|
342
|
+
.modal-session-row {
|
|
343
|
+
display: flex; align-items: center; gap: 8px;
|
|
344
|
+
padding: 6px 12px; cursor: pointer; transition: background 0.1s; user-select: none;
|
|
345
|
+
}
|
|
346
|
+
.modal-session-row:hover { background: var(--bg2); }
|
|
347
|
+
.modal-session-row.selected { background: rgba(124, 58, 237, 0.15); }
|
|
348
|
+
|
|
349
|
+
.modal-checkbox {
|
|
350
|
+
appearance: none; width: 16px; height: 16px;
|
|
351
|
+
border: 1px solid var(--border); border-radius: 3px; background: var(--bg2);
|
|
352
|
+
cursor: pointer; position: relative; flex-shrink: 0; transition: all 0.15s;
|
|
353
|
+
}
|
|
354
|
+
.modal-checkbox:checked { background: var(--purple); border-color: var(--purple); }
|
|
355
|
+
.modal-checkbox:checked::after {
|
|
356
|
+
content: '✓'; position: absolute; inset: 0;
|
|
357
|
+
display: flex; align-items: center; justify-content: center;
|
|
358
|
+
color: var(--white); font-size: 11px; font-weight: bold;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
.modal-session-prefix {
|
|
362
|
+
font-family: monospace; font-size: 12px; font-weight: 600; letter-spacing: 0.5px; flex-shrink: 0;
|
|
363
|
+
}
|
|
364
|
+
.modal-session-info {
|
|
365
|
+
flex: 1; min-width: 0; display: flex; align-items: baseline; gap: 4px; overflow: hidden;
|
|
366
|
+
}
|
|
367
|
+
.modal-session-project {
|
|
368
|
+
font-size: 12px; font-weight: 500; color: var(--text);
|
|
369
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
370
|
+
}
|
|
371
|
+
.modal-session-model { font-size: 10px; color: var(--dim); flex-shrink: 0; }
|
|
372
|
+
.modal-session-time { font-size: 10px; color: var(--dim); flex-shrink: 0; margin-left: auto; }
|
|
373
|
+
|
|
374
|
+
.modal-footer {
|
|
375
|
+
display: flex; align-items: center; justify-content: flex-end; gap: 6px;
|
|
376
|
+
padding: 8px 12px; border-top: 1px solid var(--border); background: var(--bg2); flex-shrink: 0;
|
|
377
|
+
}
|
|
289
378
|
</style>
|
|
290
379
|
</head>
|
|
291
380
|
<body>
|
|
@@ -296,13 +385,14 @@ body {
|
|
|
296
385
|
<button class="btn on" id="btn-tool-output" onclick="toggleToolOutput()" data-tooltip="Toggle tool output">📤 Output</button>
|
|
297
386
|
<button class="btn on" id="btn-text" onclick="toggleText()" data-tooltip="Toggle text responses">💬 Text</button>
|
|
298
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>
|
|
299
389
|
<span class="sep">│</span>
|
|
300
390
|
<button class="btn on" id="btn-autoscroll" onclick="toggleAutoScroll()" data-tooltip="Auto-scroll">⇣ Auto</button>
|
|
301
391
|
<button class="btn" id="btn-tree-toggle" onclick="toggleTree()" data-tooltip="Toggle tree panel">📂 Tree</button>
|
|
302
392
|
<span class="sep">│</span>
|
|
303
393
|
<span id="session-info">Connecting...</span>
|
|
304
394
|
<div class="auto">
|
|
305
|
-
<button class="btn btn-icon" id="btn-export" onclick="
|
|
395
|
+
<button class="btn btn-icon" id="btn-export" onclick="openExportModal()" data-tooltip="导出 HTML">💾</button>
|
|
306
396
|
<button class="btn btn-icon" id="btn-theme" onclick="toggleTheme()" data-tooltip="Toggle theme">🌙</button>
|
|
307
397
|
<button class="btn on" id="btn-autodisco" onclick="toggleAutoDiscovery()" data-tooltip="Auto-discover">🔍 Auto</button>
|
|
308
398
|
<span class="sep">│</span>
|
|
@@ -341,6 +431,25 @@ body {
|
|
|
341
431
|
<span id="footer-version" style="margin-left:auto;font-size:10px;color:var(--dim)"></span>
|
|
342
432
|
</div>
|
|
343
433
|
|
|
434
|
+
<div id="export-modal" class="modal-overlay" style="display:none">
|
|
435
|
+
<div class="modal-box">
|
|
436
|
+
<div class="modal-header">
|
|
437
|
+
<span class="modal-title">选择要导出的会话</span>
|
|
438
|
+
<button class="btn btn-icon" onclick="closeExportModal()" data-tooltip="关闭">✕</button>
|
|
439
|
+
</div>
|
|
440
|
+
<div class="modal-toolbar">
|
|
441
|
+
<button class="btn" onclick="exportModalToggleAll(true)">全选</button>
|
|
442
|
+
<button class="btn" onclick="exportModalToggleAll(false)">取消全选</button>
|
|
443
|
+
<span class="modal-count" id="modal-selected-count">已选 0 / 0</span>
|
|
444
|
+
</div>
|
|
445
|
+
<div class="modal-body" id="modal-session-list"></div>
|
|
446
|
+
<div class="modal-footer">
|
|
447
|
+
<button class="btn" onclick="closeExportModal()">取消</button>
|
|
448
|
+
<button class="btn on" id="modal-export-btn" onclick="confirmExport()" disabled>导出</button>
|
|
449
|
+
</div>
|
|
450
|
+
</div>
|
|
451
|
+
</div>
|
|
452
|
+
|
|
344
453
|
<script src="vendor/highlight.min.js"></script>
|
|
345
454
|
<script src="vendor/marked.min.js"></script>
|
|
346
455
|
<script src="vendor/purify.min.js"></script>
|
|
@@ -385,11 +494,13 @@ class LRUCache {
|
|
|
385
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; }
|
|
386
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; }
|
|
387
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(); }
|
|
388
499
|
}
|
|
389
500
|
const seenToolIDs = new LRUCache(20000);
|
|
390
501
|
const toolNameMap = new LRUCache(2000);
|
|
391
|
-
const agentActivity = new
|
|
392
|
-
const taskDescriptions = new
|
|
502
|
+
const agentActivity = new LRUCache(500); // "sessionID:agentID" → { toolName, content }
|
|
503
|
+
const taskDescriptions = new LRUCache(2000); // toolID → description string
|
|
393
504
|
const MAX_DESC_STORE = 200;
|
|
394
505
|
let filters = new Map();
|
|
395
506
|
let visibleFilterCount = 0;
|
|
@@ -399,6 +510,7 @@ let showToolInput = true;
|
|
|
399
510
|
let showToolOutput = true;
|
|
400
511
|
let showText = true;
|
|
401
512
|
let showHook = true;
|
|
513
|
+
let showUserPrompt = true;
|
|
402
514
|
let showActivity = true;
|
|
403
515
|
let autoDiscovery = true;
|
|
404
516
|
let appVersion = '';
|
|
@@ -526,7 +638,7 @@ function connect() {
|
|
|
526
638
|
reconnectTimer = setTimeout(connect, reconnectDelay);
|
|
527
639
|
reconnectDelay = Math.min(reconnectDelay * 2, MaxReconnectDelay);
|
|
528
640
|
};
|
|
529
|
-
ws.onerror = () => {};
|
|
641
|
+
ws.onerror = (e) => { console.warn('[ws] connection error', e); };
|
|
530
642
|
|
|
531
643
|
ws.onmessage = (e) => {
|
|
532
644
|
lastMsgTime = Date.now();
|
|
@@ -583,6 +695,14 @@ function sendCmd(action, extra = {}) {
|
|
|
583
695
|
|
|
584
696
|
function handleSnapshot(payload) {
|
|
585
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
|
+
}
|
|
586
706
|
for (const s of (payload.sessions || [])) {
|
|
587
707
|
if (hiddenSessionIDs.has(s.id)) continue;
|
|
588
708
|
let session = sessionsMap.get(s.id);
|
|
@@ -672,10 +792,16 @@ function handleNewBgTask(payload) {
|
|
|
672
792
|
}
|
|
673
793
|
|
|
674
794
|
function handleSessionRemoved(payload) {
|
|
675
|
-
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);
|
|
676
802
|
if (idx >= 0) {
|
|
677
803
|
sessions.splice(idx, 1);
|
|
678
|
-
sessionsMap.delete(
|
|
804
|
+
sessionsMap.delete(sid);
|
|
679
805
|
}
|
|
680
806
|
updateFilters();
|
|
681
807
|
rebuildNodes();
|
|
@@ -769,7 +895,7 @@ function isItemVisible(item) {
|
|
|
769
895
|
case 'tool_output': return showToolOutput;
|
|
770
896
|
case 'text': return showText;
|
|
771
897
|
case 'hook_output': return showHook;
|
|
772
|
-
case 'user_text': return
|
|
898
|
+
case 'user_text': return showUserPrompt;
|
|
773
899
|
default: return true;
|
|
774
900
|
}
|
|
775
901
|
}
|
|
@@ -919,7 +1045,7 @@ function getNodeHTML(node, idx) {
|
|
|
919
1045
|
}
|
|
920
1046
|
|
|
921
1047
|
if (node.type === 'session') {
|
|
922
|
-
const displayName = node.
|
|
1048
|
+
const displayName = folderName(node.projectPath) || node.title || node.id.slice(0, 14);
|
|
923
1049
|
const parts = [];
|
|
924
1050
|
if (node.model) parts.push(`🧠 ${esc(node.model)}`);
|
|
925
1051
|
const activeDot = isSessionActive(node) ? '<span class="active-dot on">🟢</span>' : '<span class="active-dot off">⚪</span>';
|
|
@@ -930,7 +1056,7 @@ function getNodeHTML(node, idx) {
|
|
|
930
1056
|
return `<div class="tree-row tree-row-session${selClass ? ' selected' : ''}">
|
|
931
1057
|
<div class="tree-content" onclick="treeClick(${idx})" data-idx="${idx}">
|
|
932
1058
|
<div class="tree-node">
|
|
933
|
-
<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)}
|
|
934
1060
|
${node.collapsed && agentCount > 0 ? `(${esc(String(agentCount))})` : ''}
|
|
935
1061
|
${subInfo}
|
|
936
1062
|
${timeHtml}
|
|
@@ -1222,6 +1348,10 @@ function renderItem(item) {
|
|
|
1222
1348
|
for (const l of truncContent(item.content)) lines.push({ cls: 'stream-line debug', text: l, sessionID: sid });
|
|
1223
1349
|
break;
|
|
1224
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;
|
|
1225
1355
|
}
|
|
1226
1356
|
|
|
1227
1357
|
lines.push({ cls: 'stream-line separator', text: '─'.repeat(60), sessionID: sid });
|
|
@@ -1243,6 +1373,7 @@ function refreshButtons() {
|
|
|
1243
1373
|
document.getElementById('btn-tool-output').classList.toggle('on', showToolOutput);
|
|
1244
1374
|
document.getElementById('btn-text').classList.toggle('on', showText);
|
|
1245
1375
|
document.getElementById('btn-hook').classList.toggle('on', showHook);
|
|
1376
|
+
document.getElementById('btn-user-prompt').classList.toggle('on', showUserPrompt);
|
|
1246
1377
|
document.getElementById('btn-activity').classList.toggle('on', showActivity);
|
|
1247
1378
|
document.getElementById('btn-autoscroll').classList.toggle('on', autoScroll);
|
|
1248
1379
|
document.getElementById('btn-tree-toggle').classList.toggle('on', showTree);
|
|
@@ -1253,7 +1384,7 @@ function refreshButtons() {
|
|
|
1253
1384
|
if (sessions.length === 0) info = 'Waiting...';
|
|
1254
1385
|
else if (sessions.length === 1) {
|
|
1255
1386
|
const s = sessions[0];
|
|
1256
|
-
info = (s.
|
|
1387
|
+
info = (folderName(s.projectPath) || s.title || s.id.slice(0, 14));
|
|
1257
1388
|
} else info = sessions.length + ' sessions';
|
|
1258
1389
|
if (!autoDiscovery) info += ' [paused]';
|
|
1259
1390
|
sessionInfo.textContent = info;
|
|
@@ -1277,6 +1408,50 @@ function refreshButtons() {
|
|
|
1277
1408
|
}
|
|
1278
1409
|
}
|
|
1279
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
|
+
|
|
1280
1455
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
1281
1456
|
// Actions
|
|
1282
1457
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
@@ -1436,6 +1611,8 @@ function toggleText() { showText = !showText; needsFullRender = true;
|
|
|
1436
1611
|
visibleDirty = true; renderStream(); refreshButtons(); }
|
|
1437
1612
|
function toggleHook() { showHook = !showHook; needsFullRender = true;
|
|
1438
1613
|
visibleDirty = true; renderStream(); refreshButtons(); }
|
|
1614
|
+
function toggleUserPrompt() { showUserPrompt = !showUserPrompt; needsFullRender = true;
|
|
1615
|
+
visibleDirty = true; renderStream(); refreshButtons(); }
|
|
1439
1616
|
function toggleActivity() { showActivity = !showActivity; rebuildNodes(); scheduleRender(); refreshButtons(); }
|
|
1440
1617
|
function toggleAutoScroll() { autoScroll = !autoScroll; if (autoScroll) streamEl.scrollTop = streamEl.scrollHeight; renderAll(); }
|
|
1441
1618
|
function toggleTree() { showTree = !showTree; document.getElementById('tree-panel').classList.toggle('hidden', !showTree); }
|
|
@@ -1580,7 +1757,7 @@ function folderName(projectPath) {
|
|
|
1580
1757
|
}
|
|
1581
1758
|
|
|
1582
1759
|
function esc(s) {
|
|
1583
|
-
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, '\');
|
|
1584
1761
|
}
|
|
1585
1762
|
|
|
1586
1763
|
function fmtDur(ms) {
|
|
@@ -1633,17 +1810,122 @@ function scheduleRender() {
|
|
|
1633
1810
|
}
|
|
1634
1811
|
}
|
|
1635
1812
|
|
|
1813
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
1814
|
+
// Export modal — session selection
|
|
1815
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
1816
|
+
|
|
1817
|
+
let exportModalSelected = new Set();
|
|
1818
|
+
|
|
1819
|
+
function openExportModal() {
|
|
1820
|
+
if (sessions.length === 0) {
|
|
1821
|
+
const btn = document.getElementById('btn-export');
|
|
1822
|
+
const orig = btn.textContent;
|
|
1823
|
+
btn.textContent = '✕ 无会话';
|
|
1824
|
+
setTimeout(() => { btn.textContent = orig; }, 2000);
|
|
1825
|
+
return;
|
|
1826
|
+
}
|
|
1827
|
+
exportModalSelected = new Set(sessions.map(s => s.id));
|
|
1828
|
+
renderModalSessionList();
|
|
1829
|
+
updateModalCount();
|
|
1830
|
+
document.getElementById('export-modal').style.display = 'flex';
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
function renderModalSessionList() {
|
|
1834
|
+
const listEl = document.getElementById('modal-session-list');
|
|
1835
|
+
const sorted = [...sessions].sort((a, b) => (a.colorRank || 0) - (b.colorRank || 0));
|
|
1836
|
+
listEl.innerHTML = sorted.map(s => {
|
|
1837
|
+
const color = idColor(s.colorRank || 0);
|
|
1838
|
+
const project = folderName(s.projectPath) || s.projectPath || '';
|
|
1839
|
+
const prefix = s.id.split('-')[0].toUpperCase();
|
|
1840
|
+
const model = s.model || '';
|
|
1841
|
+
const time = formatTime(s.birthtimeMs);
|
|
1842
|
+
const checked = exportModalSelected.has(s.id) ? 'checked' : '';
|
|
1843
|
+
const selectedClass = exportModalSelected.has(s.id) ? ' selected' : '';
|
|
1844
|
+
return `<div class="modal-session-row${selectedClass}" data-sid="${esc(s.id)}" onclick="toggleModalSession('${esc(s.id)}', this)">
|
|
1845
|
+
<input type="checkbox" class="modal-checkbox" data-sid="${esc(s.id)}" ${checked} onclick="event.stopPropagation(); toggleModalSession('${esc(s.id)}', this.parentElement)">
|
|
1846
|
+
<span class="modal-session-prefix" style="color:${color}">${esc(prefix)}</span>
|
|
1847
|
+
<div class="modal-session-info">
|
|
1848
|
+
<span class="modal-session-project">${esc(project)}</span>
|
|
1849
|
+
${model ? `<span class="modal-session-model">${esc(model)}</span>` : ''}
|
|
1850
|
+
</div>
|
|
1851
|
+
${time ? `<span class="modal-session-time">${esc(time)}</span>` : ''}
|
|
1852
|
+
</div>`;
|
|
1853
|
+
}).join('\n');
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
function toggleModalSession(sid, rowEl) {
|
|
1857
|
+
if (exportModalSelected.has(sid)) {
|
|
1858
|
+
exportModalSelected.delete(sid);
|
|
1859
|
+
} else {
|
|
1860
|
+
exportModalSelected.add(sid);
|
|
1861
|
+
}
|
|
1862
|
+
const checkbox = rowEl.querySelector('.modal-checkbox');
|
|
1863
|
+
checkbox.checked = exportModalSelected.has(sid);
|
|
1864
|
+
rowEl.classList.toggle('selected', exportModalSelected.has(sid));
|
|
1865
|
+
updateModalCount();
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
function exportModalToggleAll(selectAll) {
|
|
1869
|
+
if (selectAll) {
|
|
1870
|
+
exportModalSelected = new Set(sessions.map(s => s.id));
|
|
1871
|
+
} else {
|
|
1872
|
+
exportModalSelected.clear();
|
|
1873
|
+
}
|
|
1874
|
+
document.querySelectorAll('#modal-session-list .modal-session-row').forEach(row => {
|
|
1875
|
+
const sid = row.dataset.sid;
|
|
1876
|
+
const checkbox = row.querySelector('.modal-checkbox');
|
|
1877
|
+
checkbox.checked = exportModalSelected.has(sid);
|
|
1878
|
+
row.classList.toggle('selected', exportModalSelected.has(sid));
|
|
1879
|
+
});
|
|
1880
|
+
updateModalCount();
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
function updateModalCount() {
|
|
1884
|
+
const total = sessions.length;
|
|
1885
|
+
const selected = exportModalSelected.size;
|
|
1886
|
+
document.getElementById('modal-selected-count').textContent = `已选 ${selected} / ${total}`;
|
|
1887
|
+
document.getElementById('modal-export-btn').disabled = selected === 0;
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
function closeExportModal() {
|
|
1891
|
+
document.getElementById('export-modal').style.display = 'none';
|
|
1892
|
+
exportModalSelected.clear();
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
// Esc key closes modal
|
|
1896
|
+
document.addEventListener('keydown', (e) => {
|
|
1897
|
+
if (e.key === 'Escape') {
|
|
1898
|
+
const modal = document.getElementById('export-modal');
|
|
1899
|
+
if (modal.style.display !== 'none') {
|
|
1900
|
+
closeExportModal();
|
|
1901
|
+
e.stopPropagation();
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
});
|
|
1905
|
+
|
|
1906
|
+
function confirmExport() {
|
|
1907
|
+
if (exportModalSelected.size === 0) return;
|
|
1908
|
+
const selectedIds = new Set(exportModalSelected);
|
|
1909
|
+
closeExportModal();
|
|
1910
|
+
exportHTML(selectedIds);
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1636
1913
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
1637
1914
|
// Export HTML
|
|
1638
1915
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
1639
1916
|
|
|
1640
|
-
function exportHTML() {
|
|
1917
|
+
function exportHTML(selectedIds = null) {
|
|
1641
1918
|
const theme = document.documentElement.getAttribute('data-theme') || 'dark';
|
|
1642
1919
|
|
|
1643
|
-
// Collect
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1920
|
+
// Collect sessions to export
|
|
1921
|
+
let sidsInExport;
|
|
1922
|
+
if (selectedIds) {
|
|
1923
|
+
sidsInExport = selectedIds;
|
|
1924
|
+
} else {
|
|
1925
|
+
sidsInExport = new Set();
|
|
1926
|
+
for (const item of visibleItems) {
|
|
1927
|
+
if (item.sessionID) sidsInExport.add(item.sessionID);
|
|
1928
|
+
}
|
|
1647
1929
|
}
|
|
1648
1930
|
const exportSessions = [];
|
|
1649
1931
|
for (const sid of sidsInExport) {
|
|
@@ -1697,6 +1979,13 @@ ${items}
|
|
|
1697
1979
|
clone.querySelectorAll('.copy-btn').forEach(el => el.remove());
|
|
1698
1980
|
clone.querySelectorAll('[onclick]').forEach(el => el.removeAttribute('onclick'));
|
|
1699
1981
|
|
|
1982
|
+
// Filter out stream lines from non-selected sessions
|
|
1983
|
+
if (selectedIds) {
|
|
1984
|
+
clone.querySelectorAll('[data-session-id]').forEach(el => {
|
|
1985
|
+
if (!selectedIds.has(el.dataset.sessionId)) el.remove();
|
|
1986
|
+
});
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1700
1989
|
// Get the cleaned innerHTML
|
|
1701
1990
|
const streamHTML = clone.innerHTML;
|
|
1702
1991
|
|
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
|
}
|