claude-code-watch 0.0.23 → 0.0.25
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 +1 -0
- package/README.zh-CN.md +1 -0
- package/package.json +1 -1
- package/public/index.html +392 -22
package/README.md
CHANGED
|
@@ -18,6 +18,7 @@ Claude Code writes detailed JSONL logs under `~/.claude/projects/` as it works
|
|
|
18
18
|
- **Token & cost visibility** — tracks input/output/cache tokens per agent, with context window utilization
|
|
19
19
|
- **Filter controls** — toggle thinking, tool input, tool output, hook output, and text visibility independently
|
|
20
20
|
- **Auto-discovery** — automatically picks up new sessions as they start (toggleable)
|
|
21
|
+
- **HTML export** — export the current stream as a self-contained HTML file with embedded session list, token stats, filter state, and per-session filtering
|
|
21
22
|
|
|
22
23
|
## Quick Start
|
|
23
24
|
|
package/README.zh-CN.md
CHANGED
|
@@ -28,6 +28,7 @@ Claude Code 在运行时会将详细的 JSONL 日志写入 `~/.claude/projects/`
|
|
|
28
28
|
- **Token/成本追踪** — 每个代理的输入/输出/缓存 token 及上下文窗口利用率
|
|
29
29
|
- **过滤控制** — 独立切换 thinking、工具输入/输出、hook 输出、文本的可见性
|
|
30
30
|
- **自动发现** — 新会话启动时自动纳入监控
|
|
31
|
+
- **HTML 导出** — 将当前会话流导出为自包含 HTML 文件,内嵌 session 列表、token 统计、filter 状态,并支持按 session 筛选浏览
|
|
31
32
|
|
|
32
33
|
## 致谢
|
|
33
34
|
|
package/package.json
CHANGED
package/public/index.html
CHANGED
|
@@ -286,6 +286,73 @@ body {
|
|
|
286
286
|
|
|
287
287
|
/* Theme toggle button */
|
|
288
288
|
#btn-theme { font-size: 14px; }
|
|
289
|
+
|
|
290
|
+
/* ── Export modal ── */
|
|
291
|
+
.modal-overlay {
|
|
292
|
+
position: fixed; inset: 0;
|
|
293
|
+
background: rgba(0, 0, 0, 0.6);
|
|
294
|
+
z-index: 10000;
|
|
295
|
+
display: flex; align-items: center; justify-content: center;
|
|
296
|
+
}
|
|
297
|
+
:root[data-theme="light"] .modal-overlay { background: rgba(0, 0, 0, 0.3); }
|
|
298
|
+
:root[data-theme="light"] .modal-session-row.selected { background: rgba(124, 58, 237, 0.2); }
|
|
299
|
+
|
|
300
|
+
.modal-box {
|
|
301
|
+
background: var(--bg); border: 1px solid var(--border); border-radius: 8px;
|
|
302
|
+
width: 480px; max-width: 90vw; max-height: 80vh;
|
|
303
|
+
display: flex; flex-direction: column; overflow: hidden;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.modal-header {
|
|
307
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
308
|
+
padding: 8px 12px; border-bottom: 1px solid var(--border); background: var(--bg2); flex-shrink: 0;
|
|
309
|
+
}
|
|
310
|
+
.modal-title { font-size: 13px; font-weight: 600; color: var(--white); }
|
|
311
|
+
|
|
312
|
+
.modal-toolbar {
|
|
313
|
+
display: flex; align-items: center; gap: 4px;
|
|
314
|
+
padding: 6px 12px; border-bottom: 1px solid var(--border); flex-shrink: 0;
|
|
315
|
+
}
|
|
316
|
+
.modal-count { margin-left: auto; font-size: 11px; color: var(--dim); }
|
|
317
|
+
|
|
318
|
+
.modal-body { flex: 1; overflow-y: auto; padding: 6px 0; }
|
|
319
|
+
|
|
320
|
+
.modal-session-row {
|
|
321
|
+
display: flex; align-items: center; gap: 8px;
|
|
322
|
+
padding: 6px 12px; cursor: pointer; transition: background 0.1s; user-select: none;
|
|
323
|
+
}
|
|
324
|
+
.modal-session-row:hover { background: var(--bg2); }
|
|
325
|
+
.modal-session-row.selected { background: rgba(124, 58, 237, 0.15); }
|
|
326
|
+
|
|
327
|
+
.modal-checkbox {
|
|
328
|
+
appearance: none; width: 16px; height: 16px;
|
|
329
|
+
border: 1px solid var(--border); border-radius: 3px; background: var(--bg2);
|
|
330
|
+
cursor: pointer; position: relative; flex-shrink: 0; transition: all 0.15s;
|
|
331
|
+
}
|
|
332
|
+
.modal-checkbox:checked { background: var(--purple); border-color: var(--purple); }
|
|
333
|
+
.modal-checkbox:checked::after {
|
|
334
|
+
content: '✓'; position: absolute; inset: 0;
|
|
335
|
+
display: flex; align-items: center; justify-content: center;
|
|
336
|
+
color: var(--white); font-size: 11px; font-weight: bold;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.modal-session-prefix {
|
|
340
|
+
font-family: monospace; font-size: 12px; font-weight: 600; letter-spacing: 0.5px; flex-shrink: 0;
|
|
341
|
+
}
|
|
342
|
+
.modal-session-info {
|
|
343
|
+
flex: 1; min-width: 0; display: flex; align-items: baseline; gap: 4px; overflow: hidden;
|
|
344
|
+
}
|
|
345
|
+
.modal-session-project {
|
|
346
|
+
font-size: 12px; font-weight: 500; color: var(--text);
|
|
347
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
348
|
+
}
|
|
349
|
+
.modal-session-model { font-size: 10px; color: var(--dim); flex-shrink: 0; }
|
|
350
|
+
.modal-session-time { font-size: 10px; color: var(--dim); flex-shrink: 0; margin-left: auto; }
|
|
351
|
+
|
|
352
|
+
.modal-footer {
|
|
353
|
+
display: flex; align-items: center; justify-content: flex-end; gap: 6px;
|
|
354
|
+
padding: 8px 12px; border-top: 1px solid var(--border); background: var(--bg2); flex-shrink: 0;
|
|
355
|
+
}
|
|
289
356
|
</style>
|
|
290
357
|
</head>
|
|
291
358
|
<body>
|
|
@@ -302,6 +369,7 @@ body {
|
|
|
302
369
|
<span class="sep">│</span>
|
|
303
370
|
<span id="session-info">Connecting...</span>
|
|
304
371
|
<div class="auto">
|
|
372
|
+
<button class="btn btn-icon" id="btn-export" onclick="openExportModal()" data-tooltip="导出 HTML">💾</button>
|
|
305
373
|
<button class="btn btn-icon" id="btn-theme" onclick="toggleTheme()" data-tooltip="Toggle theme">🌙</button>
|
|
306
374
|
<button class="btn on" id="btn-autodisco" onclick="toggleAutoDiscovery()" data-tooltip="Auto-discover">🔍 Auto</button>
|
|
307
375
|
<span class="sep">│</span>
|
|
@@ -340,6 +408,25 @@ body {
|
|
|
340
408
|
<span id="footer-version" style="margin-left:auto;font-size:10px;color:var(--dim)"></span>
|
|
341
409
|
</div>
|
|
342
410
|
|
|
411
|
+
<div id="export-modal" class="modal-overlay" style="display:none">
|
|
412
|
+
<div class="modal-box">
|
|
413
|
+
<div class="modal-header">
|
|
414
|
+
<span class="modal-title">选择要导出的会话</span>
|
|
415
|
+
<button class="btn btn-icon" onclick="closeExportModal()" data-tooltip="关闭">✕</button>
|
|
416
|
+
</div>
|
|
417
|
+
<div class="modal-toolbar">
|
|
418
|
+
<button class="btn" onclick="exportModalToggleAll(true)">全选</button>
|
|
419
|
+
<button class="btn" onclick="exportModalToggleAll(false)">取消全选</button>
|
|
420
|
+
<span class="modal-count" id="modal-selected-count">已选 0 / 0</span>
|
|
421
|
+
</div>
|
|
422
|
+
<div class="modal-body" id="modal-session-list"></div>
|
|
423
|
+
<div class="modal-footer">
|
|
424
|
+
<button class="btn" onclick="closeExportModal()">取消</button>
|
|
425
|
+
<button class="btn on" id="modal-export-btn" onclick="confirmExport()" disabled>导出</button>
|
|
426
|
+
</div>
|
|
427
|
+
</div>
|
|
428
|
+
</div>
|
|
429
|
+
|
|
343
430
|
<script src="vendor/highlight.min.js"></script>
|
|
344
431
|
<script src="vendor/marked.min.js"></script>
|
|
345
432
|
<script src="vendor/purify.min.js"></script>
|
|
@@ -447,6 +534,11 @@ let needsFullRender = true;
|
|
|
447
534
|
let treeDirty = true;
|
|
448
535
|
let lastTreeCursor = -1;
|
|
449
536
|
|
|
537
|
+
// Cache highlight.js CSS for HTML export
|
|
538
|
+
let hljsDarkCSS = '', hljsLightCSS = '';
|
|
539
|
+
fetch('vendor/github-dark.min.css').then(r => r.text()).then(t => { hljsDarkCSS = t; }).catch(() => {});
|
|
540
|
+
fetch('vendor/github-light.min.css').then(r => r.text()).then(t => { hljsLightCSS = t; }).catch(() => {});
|
|
541
|
+
|
|
450
542
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
451
543
|
// Markdown renderer (marked + highlight.js)
|
|
452
544
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
@@ -1106,8 +1198,9 @@ function renderStream() {
|
|
|
1106
1198
|
let html;
|
|
1107
1199
|
if (lines.length > 0) {
|
|
1108
1200
|
html = lines.map(l => {
|
|
1109
|
-
|
|
1110
|
-
return `<div class="${esc(l.cls)}">${
|
|
1201
|
+
const sidAttr = l.sessionID ? ` data-session-id="${esc(l.sessionID)}"` : '';
|
|
1202
|
+
if (l.html) return `<div class="${esc(l.cls)}"${sidAttr}>${l.text}</div>`;
|
|
1203
|
+
return `<div class="${esc(l.cls)}"${sidAttr}>${esc(l.text)}</div>`;
|
|
1111
1204
|
}).join('\n');
|
|
1112
1205
|
} else if (streamItems.length > 0) {
|
|
1113
1206
|
html = `<div style="color:#fbbf24;padding:20px;text-align:center">${streamItems.length} items buffered, 0 visible — check toggles or tree selection</div>`;
|
|
@@ -1125,6 +1218,7 @@ function renderStream() {
|
|
|
1125
1218
|
for (const l of renderItem(visible[i])) {
|
|
1126
1219
|
const div = document.createElement('div');
|
|
1127
1220
|
div.className = l.cls;
|
|
1221
|
+
if (l.sessionID) div.dataset.sessionId = l.sessionID;
|
|
1128
1222
|
div.innerHTML = l.html ? l.text : esc(l.text);
|
|
1129
1223
|
streamEl.appendChild(div);
|
|
1130
1224
|
}
|
|
@@ -1144,16 +1238,17 @@ function renderItem(item) {
|
|
|
1144
1238
|
const isSub = !!item.agentID;
|
|
1145
1239
|
const agentTagCls = 'stream-line ' + (isSub ? 'agent-sub agent-tag' : 'agent-main agent-tag');
|
|
1146
1240
|
const sep = ' » ';
|
|
1241
|
+
const sid = item.sessionID || '';
|
|
1147
1242
|
|
|
1148
1243
|
if (item.type === 'turn_marker') {
|
|
1149
|
-
return [{ cls: 'stream-line marker', text: `── turn ended ${fmtDur(item.durationMs)}
|
|
1244
|
+
return [{ cls: 'stream-line marker', text: `── turn ended ${fmtDur(item.durationMs)} ──`, sessionID: sid }];
|
|
1150
1245
|
}
|
|
1151
1246
|
if (item.type === 'compact_marker') {
|
|
1152
1247
|
const label = item.content ? `compacted (${item.content})` : 'compacted';
|
|
1153
|
-
return [{ cls: 'stream-line marker', text: `── ${label}
|
|
1248
|
+
return [{ cls: 'stream-line marker', text: `── ${label} ──`, sessionID: sid }];
|
|
1154
1249
|
}
|
|
1155
1250
|
if (item.type === 'pr_link') {
|
|
1156
|
-
return [{ cls: 'stream-line marker', text: `── ${item.content}
|
|
1251
|
+
return [{ cls: 'stream-line marker', text: `── ${item.content} ──`, sessionID: sid }];
|
|
1157
1252
|
}
|
|
1158
1253
|
|
|
1159
1254
|
const agentName = item.agentName || 'Main';
|
|
@@ -1165,12 +1260,12 @@ function renderItem(item) {
|
|
|
1165
1260
|
|
|
1166
1261
|
switch (item.type) {
|
|
1167
1262
|
case 'thinking':
|
|
1168
|
-
lines.push({ cls: agentTagCls, text: `<span class="tag-label">${agentLabel}${sep}🧠 Thinking</span>${tsHtml}`, html: true });
|
|
1169
|
-
for (const l of truncContent(item.content)) lines.push({ cls: 'stream-line thinking', text: l });
|
|
1263
|
+
lines.push({ cls: agentTagCls, text: `<span class="tag-label">${agentLabel}${sep}🧠 Thinking</span>${tsHtml}`, html: true, sessionID: sid });
|
|
1264
|
+
for (const l of truncContent(item.content)) lines.push({ cls: 'stream-line thinking', text: l, sessionID: sid });
|
|
1170
1265
|
break;
|
|
1171
1266
|
case 'tool_input':
|
|
1172
|
-
lines.push({ cls: agentTagCls, text: `<span class="tag-label">${agentLabel}${sep}🔧 ${esc(item.toolName || '')}</span>${tsHtml}`, html: true });
|
|
1173
|
-
for (const l of truncContent(item.content)) lines.push({ cls: 'stream-line tool-input', text: l });
|
|
1267
|
+
lines.push({ cls: agentTagCls, text: `<span class="tag-label">${agentLabel}${sep}🔧 ${esc(item.toolName || '')}</span>${tsHtml}`, html: true, sessionID: sid });
|
|
1268
|
+
for (const l of truncContent(item.content)) lines.push({ cls: 'stream-line tool-input', text: l, sessionID: sid });
|
|
1174
1269
|
break;
|
|
1175
1270
|
case 'tool_output': {
|
|
1176
1271
|
let tn = '';
|
|
@@ -1179,43 +1274,43 @@ function renderItem(item) {
|
|
|
1179
1274
|
}
|
|
1180
1275
|
let label = tn ? `📤 ${tn} result` : '📤 Output';
|
|
1181
1276
|
if (item.durationMs > 0) label += ' ' + fmtDur(item.durationMs);
|
|
1182
|
-
lines.push({ cls: agentTagCls, text: `<span class="tag-label">${agentLabel}${sep}${esc(label)}</span>${tsHtml}`, html: true });
|
|
1183
|
-
for (const l of truncContent(item.content)) lines.push({ cls: 'stream-line tool-output', text: l });
|
|
1277
|
+
lines.push({ cls: agentTagCls, text: `<span class="tag-label">${agentLabel}${sep}${esc(label)}</span>${tsHtml}`, html: true, sessionID: sid });
|
|
1278
|
+
for (const l of truncContent(item.content)) lines.push({ cls: 'stream-line tool-output', text: l, sessionID: sid });
|
|
1184
1279
|
break;
|
|
1185
1280
|
}
|
|
1186
1281
|
case 'text':
|
|
1187
|
-
lines.push({ cls: agentTagCls, text: `<span class="tag-label">${agentLabel}${sep}💬 Response</span>${tsHtml}`, html: true });
|
|
1188
|
-
lines.push({ cls: 'stream-line text md-content', text: mdRender(item.content), html: true });
|
|
1282
|
+
lines.push({ cls: agentTagCls, text: `<span class="tag-label">${agentLabel}${sep}💬 Response</span>${tsHtml}`, html: true, sessionID: sid });
|
|
1283
|
+
lines.push({ cls: 'stream-line text md-content', text: mdRender(item.content), html: true, sessionID: sid });
|
|
1189
1284
|
break;
|
|
1190
1285
|
case 'hook_output': {
|
|
1191
1286
|
let label = '🪝 Hook';
|
|
1192
1287
|
if (item.toolName) label += ' ' + item.toolName;
|
|
1193
1288
|
if (item.durationMs > 0) label += ' ' + fmtDur(item.durationMs);
|
|
1194
|
-
lines.push({ cls: agentTagCls, text: `<span class="tag-label">${agentLabel}${sep}${esc(label)}</span>${tsHtml}`, html: true });
|
|
1195
|
-
if (item.hookCommand) lines.push({ cls: 'stream-line hook', text: `<span class="hook-label">command:</span> ${esc(item.hookCommand)}`, html: true });
|
|
1289
|
+
lines.push({ cls: agentTagCls, text: `<span class="tag-label">${agentLabel}${sep}${esc(label)}</span>${tsHtml}`, html: true, sessionID: sid });
|
|
1290
|
+
if (item.hookCommand) lines.push({ cls: 'stream-line hook', text: `<span class="hook-label">command:</span> ${esc(item.hookCommand)}`, html: true, sessionID: sid });
|
|
1196
1291
|
if (item.hookContent) {
|
|
1197
|
-
for (const l of truncContent(item.hookContent)) lines.push({ cls: 'stream-line hook', text: `<span class="hook-label">content:</span> ${esc(l)}`, html: true });
|
|
1292
|
+
for (const l of truncContent(item.hookContent)) lines.push({ cls: 'stream-line hook', text: `<span class="hook-label">content:</span> ${esc(l)}`, html: true, sessionID: sid });
|
|
1198
1293
|
}
|
|
1199
|
-
for (const l of truncContent(item.content)) lines.push({ cls: 'stream-line hook', text: `<span class="hook-label">stdout:</span> ${esc(l)}`, html: true });
|
|
1294
|
+
for (const l of truncContent(item.content)) lines.push({ cls: 'stream-line hook', text: `<span class="hook-label">stdout:</span> ${esc(l)}`, html: true, sessionID: sid });
|
|
1200
1295
|
break;
|
|
1201
1296
|
}
|
|
1202
1297
|
case 'diagnostics': {
|
|
1203
1298
|
let label = '⚠ Diagnostics';
|
|
1204
1299
|
if (item.toolName) label += ' ' + item.toolName;
|
|
1205
|
-
lines.push({ cls: agentTagCls, text: `<span class="tag-label">${agentLabel}${sep}${esc(label)}</span>${tsHtml}`, html: true });
|
|
1206
|
-
for (const l of truncContent(item.content)) lines.push({ cls: 'stream-line diag', text: l });
|
|
1300
|
+
lines.push({ cls: agentTagCls, text: `<span class="tag-label">${agentLabel}${sep}${esc(label)}</span>${tsHtml}`, html: true, sessionID: sid });
|
|
1301
|
+
for (const l of truncContent(item.content)) lines.push({ cls: 'stream-line diag', text: l, sessionID: sid });
|
|
1207
1302
|
break;
|
|
1208
1303
|
}
|
|
1209
1304
|
case 'debug': {
|
|
1210
1305
|
let label = '🔍 Debug';
|
|
1211
1306
|
if (item.toolName) label += ' ' + item.toolName;
|
|
1212
|
-
lines.push({ cls: agentTagCls, text: `<span class="tag-label">${agentLabel}${sep}${esc(label)}</span>${tsHtml}`, html: true });
|
|
1213
|
-
for (const l of truncContent(item.content)) lines.push({ cls: 'stream-line debug', text: l });
|
|
1307
|
+
lines.push({ cls: agentTagCls, text: `<span class="tag-label">${agentLabel}${sep}${esc(label)}</span>${tsHtml}`, html: true, sessionID: sid });
|
|
1308
|
+
for (const l of truncContent(item.content)) lines.push({ cls: 'stream-line debug', text: l, sessionID: sid });
|
|
1214
1309
|
break;
|
|
1215
1310
|
}
|
|
1216
1311
|
}
|
|
1217
1312
|
|
|
1218
|
-
lines.push({ cls: 'stream-line separator', text: '─'.repeat(60) });
|
|
1313
|
+
lines.push({ cls: 'stream-line separator', text: '─'.repeat(60), sessionID: sid });
|
|
1219
1314
|
return lines;
|
|
1220
1315
|
}
|
|
1221
1316
|
|
|
@@ -1624,6 +1719,281 @@ function scheduleRender() {
|
|
|
1624
1719
|
}
|
|
1625
1720
|
}
|
|
1626
1721
|
|
|
1722
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
1723
|
+
// Export modal — session selection
|
|
1724
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
1725
|
+
|
|
1726
|
+
let exportModalSelected = new Set();
|
|
1727
|
+
|
|
1728
|
+
function openExportModal() {
|
|
1729
|
+
if (sessions.length === 0) {
|
|
1730
|
+
const btn = document.getElementById('btn-export');
|
|
1731
|
+
const orig = btn.textContent;
|
|
1732
|
+
btn.textContent = '✕ 无会话';
|
|
1733
|
+
setTimeout(() => { btn.textContent = orig; }, 2000);
|
|
1734
|
+
return;
|
|
1735
|
+
}
|
|
1736
|
+
exportModalSelected = new Set(sessions.map(s => s.id));
|
|
1737
|
+
renderModalSessionList();
|
|
1738
|
+
updateModalCount();
|
|
1739
|
+
document.getElementById('export-modal').style.display = 'flex';
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
function renderModalSessionList() {
|
|
1743
|
+
const listEl = document.getElementById('modal-session-list');
|
|
1744
|
+
const sorted = [...sessions].sort((a, b) => (a.colorRank || 0) - (b.colorRank || 0));
|
|
1745
|
+
listEl.innerHTML = sorted.map(s => {
|
|
1746
|
+
const color = idColor(s.colorRank || 0);
|
|
1747
|
+
const project = folderName(s.projectPath) || s.projectPath || '';
|
|
1748
|
+
const prefix = s.id.split('-')[0].toUpperCase();
|
|
1749
|
+
const model = s.model || '';
|
|
1750
|
+
const time = formatTime(s.birthtimeMs);
|
|
1751
|
+
const checked = exportModalSelected.has(s.id) ? 'checked' : '';
|
|
1752
|
+
const selectedClass = exportModalSelected.has(s.id) ? ' selected' : '';
|
|
1753
|
+
return `<div class="modal-session-row${selectedClass}" data-sid="${esc(s.id)}" onclick="toggleModalSession('${esc(s.id)}', this)">
|
|
1754
|
+
<input type="checkbox" class="modal-checkbox" data-sid="${esc(s.id)}" ${checked} onclick="event.stopPropagation(); toggleModalSession('${esc(s.id)}', this.parentElement)">
|
|
1755
|
+
<span class="modal-session-prefix" style="color:${color}">${esc(prefix)}</span>
|
|
1756
|
+
<div class="modal-session-info">
|
|
1757
|
+
<span class="modal-session-project">${esc(project)}</span>
|
|
1758
|
+
${model ? `<span class="modal-session-model">${esc(model)}</span>` : ''}
|
|
1759
|
+
</div>
|
|
1760
|
+
${time ? `<span class="modal-session-time">${esc(time)}</span>` : ''}
|
|
1761
|
+
</div>`;
|
|
1762
|
+
}).join('\n');
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
function toggleModalSession(sid, rowEl) {
|
|
1766
|
+
if (exportModalSelected.has(sid)) {
|
|
1767
|
+
exportModalSelected.delete(sid);
|
|
1768
|
+
} else {
|
|
1769
|
+
exportModalSelected.add(sid);
|
|
1770
|
+
}
|
|
1771
|
+
const checkbox = rowEl.querySelector('.modal-checkbox');
|
|
1772
|
+
checkbox.checked = exportModalSelected.has(sid);
|
|
1773
|
+
rowEl.classList.toggle('selected', exportModalSelected.has(sid));
|
|
1774
|
+
updateModalCount();
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
function exportModalToggleAll(selectAll) {
|
|
1778
|
+
if (selectAll) {
|
|
1779
|
+
exportModalSelected = new Set(sessions.map(s => s.id));
|
|
1780
|
+
} else {
|
|
1781
|
+
exportModalSelected.clear();
|
|
1782
|
+
}
|
|
1783
|
+
document.querySelectorAll('#modal-session-list .modal-session-row').forEach(row => {
|
|
1784
|
+
const sid = row.dataset.sid;
|
|
1785
|
+
const checkbox = row.querySelector('.modal-checkbox');
|
|
1786
|
+
checkbox.checked = exportModalSelected.has(sid);
|
|
1787
|
+
row.classList.toggle('selected', exportModalSelected.has(sid));
|
|
1788
|
+
});
|
|
1789
|
+
updateModalCount();
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
function updateModalCount() {
|
|
1793
|
+
const total = sessions.length;
|
|
1794
|
+
const selected = exportModalSelected.size;
|
|
1795
|
+
document.getElementById('modal-selected-count').textContent = `已选 ${selected} / ${total}`;
|
|
1796
|
+
document.getElementById('modal-export-btn').disabled = selected === 0;
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
function closeExportModal() {
|
|
1800
|
+
document.getElementById('export-modal').style.display = 'none';
|
|
1801
|
+
exportModalSelected.clear();
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
// Esc key closes modal
|
|
1805
|
+
document.addEventListener('keydown', (e) => {
|
|
1806
|
+
if (e.key === 'Escape') {
|
|
1807
|
+
const modal = document.getElementById('export-modal');
|
|
1808
|
+
if (modal.style.display !== 'none') {
|
|
1809
|
+
closeExportModal();
|
|
1810
|
+
e.stopPropagation();
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
});
|
|
1814
|
+
|
|
1815
|
+
function confirmExport() {
|
|
1816
|
+
if (exportModalSelected.size === 0) return;
|
|
1817
|
+
const selectedIds = new Set(exportModalSelected);
|
|
1818
|
+
closeExportModal();
|
|
1819
|
+
exportHTML(selectedIds);
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
1823
|
+
// Export HTML
|
|
1824
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
1825
|
+
|
|
1826
|
+
function exportHTML(selectedIds = null) {
|
|
1827
|
+
const theme = document.documentElement.getAttribute('data-theme') || 'dark';
|
|
1828
|
+
|
|
1829
|
+
// Collect sessions to export
|
|
1830
|
+
let sidsInExport;
|
|
1831
|
+
if (selectedIds) {
|
|
1832
|
+
sidsInExport = selectedIds;
|
|
1833
|
+
} else {
|
|
1834
|
+
sidsInExport = new Set();
|
|
1835
|
+
for (const item of visibleItems) {
|
|
1836
|
+
if (item.sessionID) sidsInExport.add(item.sessionID);
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
const exportSessions = [];
|
|
1840
|
+
for (const sid of sidsInExport) {
|
|
1841
|
+
const s = sessionsMap.get(sid);
|
|
1842
|
+
if (s) exportSessions.push(s);
|
|
1843
|
+
}
|
|
1844
|
+
// Sort by colorRank to match the order in the tree
|
|
1845
|
+
exportSessions.sort((a, b) => (a.colorRank || 0) - (b.colorRank || 0));
|
|
1846
|
+
|
|
1847
|
+
// Build session list header
|
|
1848
|
+
let sessionListHTML = '';
|
|
1849
|
+
if (exportSessions.length > 0) {
|
|
1850
|
+
const items = exportSessions.map(s => {
|
|
1851
|
+
const color = idColor(s.colorRank || 0);
|
|
1852
|
+
const project = folderName(s.projectPath) || s.projectPath || '';
|
|
1853
|
+
const model = s.model || '';
|
|
1854
|
+
return `<div class="export-session-item" data-sid="${esc(s.id)}" onclick="filterBySession('${esc(s.id)}')"><div class="export-item-top"><span class="export-project">${esc(project)}</span>${model ? ` <span class="export-model" style="color:var(--dim)">${esc(model)}</span>` : ''}</div><div class="export-item-sid" style="color:${color}">${esc(s.id)}</div></div>`;
|
|
1855
|
+
}).join('\n');
|
|
1856
|
+
sessionListHTML = `<div class="export-session-list">
|
|
1857
|
+
<div class="export-session-item export-all-btn active" onclick="filterBySession(null)">全部</div>
|
|
1858
|
+
${items}
|
|
1859
|
+
</div>`;
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
// Token info
|
|
1863
|
+
computeTokensFromContext();
|
|
1864
|
+
let tokenHTML = '';
|
|
1865
|
+
if (totalInput > 0 || totalOutput > 0) {
|
|
1866
|
+
let tokStr = `Input: ${fmtTok(totalInput)} · Output: ${fmtTok(totalOutput)}`;
|
|
1867
|
+
if (totalCacheCreate > 0 || totalCacheRead > 0) tokStr += ` · Cache: ${fmtTok(totalCacheCreate)}+${fmtTok(totalCacheRead)}`;
|
|
1868
|
+
tokenHTML = `<div class="export-meta-line" style="color:var(--dim)">Tokens: ${tokStr}</div>`;
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
// Filter state
|
|
1872
|
+
const filterState = [];
|
|
1873
|
+
if (!showThinking) filterState.push('thinking hidden');
|
|
1874
|
+
if (!showToolInput) filterState.push('tools hidden');
|
|
1875
|
+
if (!showToolOutput) filterState.push('output hidden');
|
|
1876
|
+
if (!showText) filterState.push('text hidden');
|
|
1877
|
+
if (!showHook) filterState.push('hook hidden');
|
|
1878
|
+
let filterHTML = '';
|
|
1879
|
+
if (filterState.length > 0) filterHTML = `<div class="export-meta-line" style="color:var(--dim)">Filters: ${filterState.join(', ')}</div>`;
|
|
1880
|
+
|
|
1881
|
+
// Export timestamp
|
|
1882
|
+
const now = new Date();
|
|
1883
|
+
const exportTime = fmtTimestamp(now);
|
|
1884
|
+
const timeHTML = `<div class="export-meta-line" style="color:var(--dim)">Exported: ${exportTime}</div>`;
|
|
1885
|
+
|
|
1886
|
+
// Clone stream content and strip interactive elements
|
|
1887
|
+
const clone = streamEl.cloneNode(true);
|
|
1888
|
+
clone.querySelectorAll('.copy-btn').forEach(el => el.remove());
|
|
1889
|
+
clone.querySelectorAll('[onclick]').forEach(el => el.removeAttribute('onclick'));
|
|
1890
|
+
|
|
1891
|
+
// Filter out stream lines from non-selected sessions
|
|
1892
|
+
if (selectedIds) {
|
|
1893
|
+
clone.querySelectorAll('[data-session-id]').forEach(el => {
|
|
1894
|
+
if (!selectedIds.has(el.dataset.sessionId)) el.remove();
|
|
1895
|
+
});
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
// Get the cleaned innerHTML
|
|
1899
|
+
const streamHTML = clone.innerHTML;
|
|
1900
|
+
|
|
1901
|
+
// Get page CSS
|
|
1902
|
+
const pageStyleEl = document.querySelector('style');
|
|
1903
|
+
const appCSS = pageStyleEl ? pageStyleEl.textContent : '';
|
|
1904
|
+
|
|
1905
|
+
// Get highlight.js CSS from cache
|
|
1906
|
+
const hlCSS = theme === 'dark' ? hljsDarkCSS : hljsLightCSS;
|
|
1907
|
+
|
|
1908
|
+
// Export-specific CSS
|
|
1909
|
+
const exportCSS = `
|
|
1910
|
+
.export-session-list { display: flex; flex-wrap: wrap; gap: 6px; padding: 8px 0; }
|
|
1911
|
+
.export-session-item { cursor: pointer; padding: 6px 8px; border-radius: 4px; border: 1px solid var(--border); opacity: 0.7; transition: all 0.15s; font-size: 12px; display: flex; flex-direction: column; gap: 2px; }
|
|
1912
|
+
.export-session-item:hover { opacity: 1; border-color: var(--dim); }
|
|
1913
|
+
.export-session-item.active { opacity: 1; border-color: var(--purple); background: var(--purple); color: var(--white); }
|
|
1914
|
+
.export-all-btn { font-weight: 600; align-items: center; }
|
|
1915
|
+
.export-item-top { display: flex; align-items: baseline; gap: 4px; }
|
|
1916
|
+
.export-item-sid { font-family: monospace; font-size: 10px; opacity: 0.8; }
|
|
1917
|
+
.export-session-item.active .export-item-sid { opacity: 1; color: var(--white); }
|
|
1918
|
+
.export-project { font-weight: 500; }
|
|
1919
|
+
.export-model { font-size: 11px; }
|
|
1920
|
+
.export-meta-line { padding: 2px 0; font-size: 11px; }
|
|
1921
|
+
.export-header { padding: 12px; border-bottom: 1px solid var(--border); position: sticky; top: 0; background: var(--bg); z-index: 100; }
|
|
1922
|
+
.export-header h1 { margin: 0 0 4px 0; font-size: 16px; color: var(--white); }
|
|
1923
|
+
`;
|
|
1924
|
+
|
|
1925
|
+
// Export-specific JS for session filtering
|
|
1926
|
+
const exportJS = `
|
|
1927
|
+
let _activeSid = null;
|
|
1928
|
+
function filterBySession(sid) {
|
|
1929
|
+
_activeSid = sid;
|
|
1930
|
+
const lines = document.querySelectorAll('#export-stream [data-session-id]');
|
|
1931
|
+
lines.forEach(el => {
|
|
1932
|
+
el.style.display = (sid === null || el.dataset.sessionId === sid) ? '' : 'none';
|
|
1933
|
+
});
|
|
1934
|
+
document.querySelectorAll('.export-session-item[data-sid]').forEach(el => {
|
|
1935
|
+
el.classList.toggle('active', sid !== null && el.dataset.sid === sid);
|
|
1936
|
+
});
|
|
1937
|
+
document.querySelector('.export-all-btn').classList.toggle('active', sid === null);
|
|
1938
|
+
}
|
|
1939
|
+
`;
|
|
1940
|
+
|
|
1941
|
+
// Assemble complete HTML document
|
|
1942
|
+
const htmlAttrs = theme === 'light' ? ' lang="en" data-theme="light"' : ' lang="en"';
|
|
1943
|
+
const fullDoc = `<!DOCTYPE html>
|
|
1944
|
+
<html${htmlAttrs}>
|
|
1945
|
+
<head>
|
|
1946
|
+
<meta charset="UTF-8">
|
|
1947
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1948
|
+
<title>claude-watch Export</title>
|
|
1949
|
+
<style>
|
|
1950
|
+
${appCSS}
|
|
1951
|
+
${hlCSS}
|
|
1952
|
+
${exportCSS}
|
|
1953
|
+
</style>
|
|
1954
|
+
</head>
|
|
1955
|
+
<body style="overflow-y:auto;height:auto">
|
|
1956
|
+
<div class="export-header">
|
|
1957
|
+
<h1>claude-watch Export</h1>
|
|
1958
|
+
${sessionListHTML}
|
|
1959
|
+
${tokenHTML}
|
|
1960
|
+
${filterHTML}
|
|
1961
|
+
${timeHTML}
|
|
1962
|
+
</div>
|
|
1963
|
+
<div id="export-stream" style="padding:8px 12px;font-size:12px">
|
|
1964
|
+
${streamHTML}
|
|
1965
|
+
</div>
|
|
1966
|
+
<script>${exportJS}<\/script>
|
|
1967
|
+
</body>
|
|
1968
|
+
</html>`;
|
|
1969
|
+
|
|
1970
|
+
// Blob download
|
|
1971
|
+
const blob = new Blob([fullDoc], { type: 'text/html;charset=utf-8' });
|
|
1972
|
+
const url = URL.createObjectURL(blob);
|
|
1973
|
+
const a = document.createElement('a');
|
|
1974
|
+
|
|
1975
|
+
let filePrefix;
|
|
1976
|
+
if (sidsInExport.size === 1) {
|
|
1977
|
+
filePrefix = [...sidsInExport][0].split('-')[0].toUpperCase();
|
|
1978
|
+
} else {
|
|
1979
|
+
filePrefix = 'multi';
|
|
1980
|
+
}
|
|
1981
|
+
const pad = (n, len) => String(n).padStart(len, '0');
|
|
1982
|
+
const ts = `${pad(now.getFullYear(),4)}${pad(now.getMonth()+1,2)}${pad(now.getDate(),2)}-${pad(now.getHours(),2)}${pad(now.getMinutes(),2)}${pad(now.getSeconds(),2)}`;
|
|
1983
|
+
a.download = `claude-watch-${filePrefix}-${ts}.html`;
|
|
1984
|
+
a.href = url;
|
|
1985
|
+
document.body.appendChild(a);
|
|
1986
|
+
a.click();
|
|
1987
|
+
document.body.removeChild(a);
|
|
1988
|
+
URL.revokeObjectURL(url);
|
|
1989
|
+
|
|
1990
|
+
// Visual feedback
|
|
1991
|
+
const btn = document.getElementById('btn-export');
|
|
1992
|
+
const orig = btn.textContent;
|
|
1993
|
+
btn.textContent = '✓';
|
|
1994
|
+
setTimeout(() => { btn.textContent = orig; }, 2000);
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1627
1997
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
1628
1998
|
// Theme toggle
|
|
1629
1999
|
// ══════════════════════════════════════════════════════════════════════════════
|