claude-code-watch 0.0.22 → 0.0.24
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 +194 -22
- package/src/watcher/watcher.js +33 -6
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
|
@@ -302,6 +302,7 @@ body {
|
|
|
302
302
|
<span class="sep">│</span>
|
|
303
303
|
<span id="session-info">Connecting...</span>
|
|
304
304
|
<div class="auto">
|
|
305
|
+
<button class="btn btn-icon" id="btn-export" onclick="exportHTML()" data-tooltip="导出 HTML">💾</button>
|
|
305
306
|
<button class="btn btn-icon" id="btn-theme" onclick="toggleTheme()" data-tooltip="Toggle theme">🌙</button>
|
|
306
307
|
<button class="btn on" id="btn-autodisco" onclick="toggleAutoDiscovery()" data-tooltip="Auto-discover">🔍 Auto</button>
|
|
307
308
|
<span class="sep">│</span>
|
|
@@ -447,6 +448,11 @@ let needsFullRender = true;
|
|
|
447
448
|
let treeDirty = true;
|
|
448
449
|
let lastTreeCursor = -1;
|
|
449
450
|
|
|
451
|
+
// Cache highlight.js CSS for HTML export
|
|
452
|
+
let hljsDarkCSS = '', hljsLightCSS = '';
|
|
453
|
+
fetch('vendor/github-dark.min.css').then(r => r.text()).then(t => { hljsDarkCSS = t; }).catch(() => {});
|
|
454
|
+
fetch('vendor/github-light.min.css').then(r => r.text()).then(t => { hljsLightCSS = t; }).catch(() => {});
|
|
455
|
+
|
|
450
456
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
451
457
|
// Markdown renderer (marked + highlight.js)
|
|
452
458
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
@@ -1106,8 +1112,9 @@ function renderStream() {
|
|
|
1106
1112
|
let html;
|
|
1107
1113
|
if (lines.length > 0) {
|
|
1108
1114
|
html = lines.map(l => {
|
|
1109
|
-
|
|
1110
|
-
return `<div class="${esc(l.cls)}">${
|
|
1115
|
+
const sidAttr = l.sessionID ? ` data-session-id="${esc(l.sessionID)}"` : '';
|
|
1116
|
+
if (l.html) return `<div class="${esc(l.cls)}"${sidAttr}>${l.text}</div>`;
|
|
1117
|
+
return `<div class="${esc(l.cls)}"${sidAttr}>${esc(l.text)}</div>`;
|
|
1111
1118
|
}).join('\n');
|
|
1112
1119
|
} else if (streamItems.length > 0) {
|
|
1113
1120
|
html = `<div style="color:#fbbf24;padding:20px;text-align:center">${streamItems.length} items buffered, 0 visible — check toggles or tree selection</div>`;
|
|
@@ -1125,6 +1132,7 @@ function renderStream() {
|
|
|
1125
1132
|
for (const l of renderItem(visible[i])) {
|
|
1126
1133
|
const div = document.createElement('div');
|
|
1127
1134
|
div.className = l.cls;
|
|
1135
|
+
if (l.sessionID) div.dataset.sessionId = l.sessionID;
|
|
1128
1136
|
div.innerHTML = l.html ? l.text : esc(l.text);
|
|
1129
1137
|
streamEl.appendChild(div);
|
|
1130
1138
|
}
|
|
@@ -1144,16 +1152,17 @@ function renderItem(item) {
|
|
|
1144
1152
|
const isSub = !!item.agentID;
|
|
1145
1153
|
const agentTagCls = 'stream-line ' + (isSub ? 'agent-sub agent-tag' : 'agent-main agent-tag');
|
|
1146
1154
|
const sep = ' » ';
|
|
1155
|
+
const sid = item.sessionID || '';
|
|
1147
1156
|
|
|
1148
1157
|
if (item.type === 'turn_marker') {
|
|
1149
|
-
return [{ cls: 'stream-line marker', text: `── turn ended ${fmtDur(item.durationMs)}
|
|
1158
|
+
return [{ cls: 'stream-line marker', text: `── turn ended ${fmtDur(item.durationMs)} ──`, sessionID: sid }];
|
|
1150
1159
|
}
|
|
1151
1160
|
if (item.type === 'compact_marker') {
|
|
1152
1161
|
const label = item.content ? `compacted (${item.content})` : 'compacted';
|
|
1153
|
-
return [{ cls: 'stream-line marker', text: `── ${label}
|
|
1162
|
+
return [{ cls: 'stream-line marker', text: `── ${label} ──`, sessionID: sid }];
|
|
1154
1163
|
}
|
|
1155
1164
|
if (item.type === 'pr_link') {
|
|
1156
|
-
return [{ cls: 'stream-line marker', text: `── ${item.content}
|
|
1165
|
+
return [{ cls: 'stream-line marker', text: `── ${item.content} ──`, sessionID: sid }];
|
|
1157
1166
|
}
|
|
1158
1167
|
|
|
1159
1168
|
const agentName = item.agentName || 'Main';
|
|
@@ -1165,12 +1174,12 @@ function renderItem(item) {
|
|
|
1165
1174
|
|
|
1166
1175
|
switch (item.type) {
|
|
1167
1176
|
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 });
|
|
1177
|
+
lines.push({ cls: agentTagCls, text: `<span class="tag-label">${agentLabel}${sep}🧠 Thinking</span>${tsHtml}`, html: true, sessionID: sid });
|
|
1178
|
+
for (const l of truncContent(item.content)) lines.push({ cls: 'stream-line thinking', text: l, sessionID: sid });
|
|
1170
1179
|
break;
|
|
1171
1180
|
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 });
|
|
1181
|
+
lines.push({ cls: agentTagCls, text: `<span class="tag-label">${agentLabel}${sep}🔧 ${esc(item.toolName || '')}</span>${tsHtml}`, html: true, sessionID: sid });
|
|
1182
|
+
for (const l of truncContent(item.content)) lines.push({ cls: 'stream-line tool-input', text: l, sessionID: sid });
|
|
1174
1183
|
break;
|
|
1175
1184
|
case 'tool_output': {
|
|
1176
1185
|
let tn = '';
|
|
@@ -1179,43 +1188,43 @@ function renderItem(item) {
|
|
|
1179
1188
|
}
|
|
1180
1189
|
let label = tn ? `📤 ${tn} result` : '📤 Output';
|
|
1181
1190
|
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 });
|
|
1191
|
+
lines.push({ cls: agentTagCls, text: `<span class="tag-label">${agentLabel}${sep}${esc(label)}</span>${tsHtml}`, html: true, sessionID: sid });
|
|
1192
|
+
for (const l of truncContent(item.content)) lines.push({ cls: 'stream-line tool-output', text: l, sessionID: sid });
|
|
1184
1193
|
break;
|
|
1185
1194
|
}
|
|
1186
1195
|
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 });
|
|
1196
|
+
lines.push({ cls: agentTagCls, text: `<span class="tag-label">${agentLabel}${sep}💬 Response</span>${tsHtml}`, html: true, sessionID: sid });
|
|
1197
|
+
lines.push({ cls: 'stream-line text md-content', text: mdRender(item.content), html: true, sessionID: sid });
|
|
1189
1198
|
break;
|
|
1190
1199
|
case 'hook_output': {
|
|
1191
1200
|
let label = '🪝 Hook';
|
|
1192
1201
|
if (item.toolName) label += ' ' + item.toolName;
|
|
1193
1202
|
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 });
|
|
1203
|
+
lines.push({ cls: agentTagCls, text: `<span class="tag-label">${agentLabel}${sep}${esc(label)}</span>${tsHtml}`, html: true, sessionID: sid });
|
|
1204
|
+
if (item.hookCommand) lines.push({ cls: 'stream-line hook', text: `<span class="hook-label">command:</span> ${esc(item.hookCommand)}`, html: true, sessionID: sid });
|
|
1196
1205
|
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 });
|
|
1206
|
+
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
1207
|
}
|
|
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 });
|
|
1208
|
+
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
1209
|
break;
|
|
1201
1210
|
}
|
|
1202
1211
|
case 'diagnostics': {
|
|
1203
1212
|
let label = '⚠ Diagnostics';
|
|
1204
1213
|
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 });
|
|
1214
|
+
lines.push({ cls: agentTagCls, text: `<span class="tag-label">${agentLabel}${sep}${esc(label)}</span>${tsHtml}`, html: true, sessionID: sid });
|
|
1215
|
+
for (const l of truncContent(item.content)) lines.push({ cls: 'stream-line diag', text: l, sessionID: sid });
|
|
1207
1216
|
break;
|
|
1208
1217
|
}
|
|
1209
1218
|
case 'debug': {
|
|
1210
1219
|
let label = '🔍 Debug';
|
|
1211
1220
|
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 });
|
|
1221
|
+
lines.push({ cls: agentTagCls, text: `<span class="tag-label">${agentLabel}${sep}${esc(label)}</span>${tsHtml}`, html: true, sessionID: sid });
|
|
1222
|
+
for (const l of truncContent(item.content)) lines.push({ cls: 'stream-line debug', text: l, sessionID: sid });
|
|
1214
1223
|
break;
|
|
1215
1224
|
}
|
|
1216
1225
|
}
|
|
1217
1226
|
|
|
1218
|
-
lines.push({ cls: 'stream-line separator', text: '─'.repeat(60) });
|
|
1227
|
+
lines.push({ cls: 'stream-line separator', text: '─'.repeat(60), sessionID: sid });
|
|
1219
1228
|
return lines;
|
|
1220
1229
|
}
|
|
1221
1230
|
|
|
@@ -1624,6 +1633,169 @@ function scheduleRender() {
|
|
|
1624
1633
|
}
|
|
1625
1634
|
}
|
|
1626
1635
|
|
|
1636
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
1637
|
+
// Export HTML
|
|
1638
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
1639
|
+
|
|
1640
|
+
function exportHTML() {
|
|
1641
|
+
const theme = document.documentElement.getAttribute('data-theme') || 'dark';
|
|
1642
|
+
|
|
1643
|
+
// Collect unique sessions from visible items
|
|
1644
|
+
const sidsInExport = new Set();
|
|
1645
|
+
for (const item of visibleItems) {
|
|
1646
|
+
if (item.sessionID) sidsInExport.add(item.sessionID);
|
|
1647
|
+
}
|
|
1648
|
+
const exportSessions = [];
|
|
1649
|
+
for (const sid of sidsInExport) {
|
|
1650
|
+
const s = sessionsMap.get(sid);
|
|
1651
|
+
if (s) exportSessions.push(s);
|
|
1652
|
+
}
|
|
1653
|
+
// Sort by colorRank to match the order in the tree
|
|
1654
|
+
exportSessions.sort((a, b) => (a.colorRank || 0) - (b.colorRank || 0));
|
|
1655
|
+
|
|
1656
|
+
// Build session list header
|
|
1657
|
+
let sessionListHTML = '';
|
|
1658
|
+
if (exportSessions.length > 0) {
|
|
1659
|
+
const items = exportSessions.map(s => {
|
|
1660
|
+
const color = idColor(s.colorRank || 0);
|
|
1661
|
+
const project = folderName(s.projectPath) || s.projectPath || '';
|
|
1662
|
+
const model = s.model || '';
|
|
1663
|
+
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>`;
|
|
1664
|
+
}).join('\n');
|
|
1665
|
+
sessionListHTML = `<div class="export-session-list">
|
|
1666
|
+
<div class="export-session-item export-all-btn active" onclick="filterBySession(null)">全部</div>
|
|
1667
|
+
${items}
|
|
1668
|
+
</div>`;
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
// Token info
|
|
1672
|
+
computeTokensFromContext();
|
|
1673
|
+
let tokenHTML = '';
|
|
1674
|
+
if (totalInput > 0 || totalOutput > 0) {
|
|
1675
|
+
let tokStr = `Input: ${fmtTok(totalInput)} · Output: ${fmtTok(totalOutput)}`;
|
|
1676
|
+
if (totalCacheCreate > 0 || totalCacheRead > 0) tokStr += ` · Cache: ${fmtTok(totalCacheCreate)}+${fmtTok(totalCacheRead)}`;
|
|
1677
|
+
tokenHTML = `<div class="export-meta-line" style="color:var(--dim)">Tokens: ${tokStr}</div>`;
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
// Filter state
|
|
1681
|
+
const filterState = [];
|
|
1682
|
+
if (!showThinking) filterState.push('thinking hidden');
|
|
1683
|
+
if (!showToolInput) filterState.push('tools hidden');
|
|
1684
|
+
if (!showToolOutput) filterState.push('output hidden');
|
|
1685
|
+
if (!showText) filterState.push('text hidden');
|
|
1686
|
+
if (!showHook) filterState.push('hook hidden');
|
|
1687
|
+
let filterHTML = '';
|
|
1688
|
+
if (filterState.length > 0) filterHTML = `<div class="export-meta-line" style="color:var(--dim)">Filters: ${filterState.join(', ')}</div>`;
|
|
1689
|
+
|
|
1690
|
+
// Export timestamp
|
|
1691
|
+
const now = new Date();
|
|
1692
|
+
const exportTime = fmtTimestamp(now);
|
|
1693
|
+
const timeHTML = `<div class="export-meta-line" style="color:var(--dim)">Exported: ${exportTime}</div>`;
|
|
1694
|
+
|
|
1695
|
+
// Clone stream content and strip interactive elements
|
|
1696
|
+
const clone = streamEl.cloneNode(true);
|
|
1697
|
+
clone.querySelectorAll('.copy-btn').forEach(el => el.remove());
|
|
1698
|
+
clone.querySelectorAll('[onclick]').forEach(el => el.removeAttribute('onclick'));
|
|
1699
|
+
|
|
1700
|
+
// Get the cleaned innerHTML
|
|
1701
|
+
const streamHTML = clone.innerHTML;
|
|
1702
|
+
|
|
1703
|
+
// Get page CSS
|
|
1704
|
+
const pageStyleEl = document.querySelector('style');
|
|
1705
|
+
const appCSS = pageStyleEl ? pageStyleEl.textContent : '';
|
|
1706
|
+
|
|
1707
|
+
// Get highlight.js CSS from cache
|
|
1708
|
+
const hlCSS = theme === 'dark' ? hljsDarkCSS : hljsLightCSS;
|
|
1709
|
+
|
|
1710
|
+
// Export-specific CSS
|
|
1711
|
+
const exportCSS = `
|
|
1712
|
+
.export-session-list { display: flex; flex-wrap: wrap; gap: 6px; padding: 8px 0; }
|
|
1713
|
+
.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; }
|
|
1714
|
+
.export-session-item:hover { opacity: 1; border-color: var(--dim); }
|
|
1715
|
+
.export-session-item.active { opacity: 1; border-color: var(--purple); background: var(--purple); color: var(--white); }
|
|
1716
|
+
.export-all-btn { font-weight: 600; align-items: center; }
|
|
1717
|
+
.export-item-top { display: flex; align-items: baseline; gap: 4px; }
|
|
1718
|
+
.export-item-sid { font-family: monospace; font-size: 10px; opacity: 0.8; }
|
|
1719
|
+
.export-session-item.active .export-item-sid { opacity: 1; color: var(--white); }
|
|
1720
|
+
.export-project { font-weight: 500; }
|
|
1721
|
+
.export-model { font-size: 11px; }
|
|
1722
|
+
.export-meta-line { padding: 2px 0; font-size: 11px; }
|
|
1723
|
+
.export-header { padding: 12px; border-bottom: 1px solid var(--border); position: sticky; top: 0; background: var(--bg); z-index: 100; }
|
|
1724
|
+
.export-header h1 { margin: 0 0 4px 0; font-size: 16px; color: var(--white); }
|
|
1725
|
+
`;
|
|
1726
|
+
|
|
1727
|
+
// Export-specific JS for session filtering
|
|
1728
|
+
const exportJS = `
|
|
1729
|
+
let _activeSid = null;
|
|
1730
|
+
function filterBySession(sid) {
|
|
1731
|
+
_activeSid = sid;
|
|
1732
|
+
const lines = document.querySelectorAll('#export-stream [data-session-id]');
|
|
1733
|
+
lines.forEach(el => {
|
|
1734
|
+
el.style.display = (sid === null || el.dataset.sessionId === sid) ? '' : 'none';
|
|
1735
|
+
});
|
|
1736
|
+
document.querySelectorAll('.export-session-item[data-sid]').forEach(el => {
|
|
1737
|
+
el.classList.toggle('active', sid !== null && el.dataset.sid === sid);
|
|
1738
|
+
});
|
|
1739
|
+
document.querySelector('.export-all-btn').classList.toggle('active', sid === null);
|
|
1740
|
+
}
|
|
1741
|
+
`;
|
|
1742
|
+
|
|
1743
|
+
// Assemble complete HTML document
|
|
1744
|
+
const htmlAttrs = theme === 'light' ? ' lang="en" data-theme="light"' : ' lang="en"';
|
|
1745
|
+
const fullDoc = `<!DOCTYPE html>
|
|
1746
|
+
<html${htmlAttrs}>
|
|
1747
|
+
<head>
|
|
1748
|
+
<meta charset="UTF-8">
|
|
1749
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1750
|
+
<title>claude-watch Export</title>
|
|
1751
|
+
<style>
|
|
1752
|
+
${appCSS}
|
|
1753
|
+
${hlCSS}
|
|
1754
|
+
${exportCSS}
|
|
1755
|
+
</style>
|
|
1756
|
+
</head>
|
|
1757
|
+
<body style="overflow-y:auto;height:auto">
|
|
1758
|
+
<div class="export-header">
|
|
1759
|
+
<h1>claude-watch Export</h1>
|
|
1760
|
+
${sessionListHTML}
|
|
1761
|
+
${tokenHTML}
|
|
1762
|
+
${filterHTML}
|
|
1763
|
+
${timeHTML}
|
|
1764
|
+
</div>
|
|
1765
|
+
<div id="export-stream" style="padding:8px 12px;font-size:12px">
|
|
1766
|
+
${streamHTML}
|
|
1767
|
+
</div>
|
|
1768
|
+
<script>${exportJS}<\/script>
|
|
1769
|
+
</body>
|
|
1770
|
+
</html>`;
|
|
1771
|
+
|
|
1772
|
+
// Blob download
|
|
1773
|
+
const blob = new Blob([fullDoc], { type: 'text/html;charset=utf-8' });
|
|
1774
|
+
const url = URL.createObjectURL(blob);
|
|
1775
|
+
const a = document.createElement('a');
|
|
1776
|
+
|
|
1777
|
+
let filePrefix;
|
|
1778
|
+
if (sidsInExport.size === 1) {
|
|
1779
|
+
filePrefix = [...sidsInExport][0].split('-')[0].toUpperCase();
|
|
1780
|
+
} else {
|
|
1781
|
+
filePrefix = 'multi';
|
|
1782
|
+
}
|
|
1783
|
+
const pad = (n, len) => String(n).padStart(len, '0');
|
|
1784
|
+
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)}`;
|
|
1785
|
+
a.download = `claude-watch-${filePrefix}-${ts}.html`;
|
|
1786
|
+
a.href = url;
|
|
1787
|
+
document.body.appendChild(a);
|
|
1788
|
+
a.click();
|
|
1789
|
+
document.body.removeChild(a);
|
|
1790
|
+
URL.revokeObjectURL(url);
|
|
1791
|
+
|
|
1792
|
+
// Visual feedback
|
|
1793
|
+
const btn = document.getElementById('btn-export');
|
|
1794
|
+
const orig = btn.textContent;
|
|
1795
|
+
btn.textContent = '✓';
|
|
1796
|
+
setTimeout(() => { btn.textContent = orig; }, 2000);
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1627
1799
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
1628
1800
|
// Theme toggle
|
|
1629
1801
|
// ══════════════════════════════════════════════════════════════════════════════
|
package/src/watcher/watcher.js
CHANGED
|
@@ -39,9 +39,37 @@ async function resolveProjectPath(encoded) {
|
|
|
39
39
|
let s = encoded;
|
|
40
40
|
if (s.startsWith('-')) s = s.slice(1);
|
|
41
41
|
if (!s) return '';
|
|
42
|
-
const parts = s.split('-');
|
|
43
42
|
|
|
44
|
-
//
|
|
43
|
+
// Claude path encoding: '/' → '-', '.' → '-' (extra dash)
|
|
44
|
+
// So '--' = '/.' (path separator + dot/hidden dir like .claude)
|
|
45
|
+
// Correct decode: '--' → '/.', then '-' → '/'
|
|
46
|
+
const directDecoded = s.replace(/--/g, '/.').replace(/-/g, '/');
|
|
47
|
+
|
|
48
|
+
// Strategy 1: try direct decoded path on disk (handles dots correctly)
|
|
49
|
+
try {
|
|
50
|
+
await fsp.access('/' + directDecoded);
|
|
51
|
+
_projectPathCache.set(encoded, directDecoded);
|
|
52
|
+
return directDecoded;
|
|
53
|
+
} catch {}
|
|
54
|
+
|
|
55
|
+
// Strategy 2: progressive join for directory names containing dashes
|
|
56
|
+
// First, merge '--' empty elements with the next element as dot-prefix directories
|
|
57
|
+
const rawParts = s.split('-');
|
|
58
|
+
const parts = [];
|
|
59
|
+
for (let i = 0; i < rawParts.length; i++) {
|
|
60
|
+
if (rawParts[i] === '') {
|
|
61
|
+
// Empty element from '--': combine with next as dot-prefix dir (e.g. ".claude")
|
|
62
|
+
if (i + 1 < rawParts.length) {
|
|
63
|
+
parts.push('.' + rawParts[i + 1]);
|
|
64
|
+
i++;
|
|
65
|
+
} else {
|
|
66
|
+
parts.push('.');
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
parts.push(rawParts[i]);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
45
73
|
for (let joinFrom = parts.length - 1; joinFrom >= 1; joinFrom--) {
|
|
46
74
|
const pathPart = parts.slice(0, joinFrom).join('/');
|
|
47
75
|
const dirPart = parts.slice(joinFrom).join('-');
|
|
@@ -56,10 +84,9 @@ async function resolveProjectPath(encoded) {
|
|
|
56
84
|
}
|
|
57
85
|
}
|
|
58
86
|
|
|
59
|
-
// Fallback
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
return result;
|
|
87
|
+
// Fallback: return direct decoded path (correct even if path no longer exists on disk)
|
|
88
|
+
_projectPathCache.set(encoded, directDecoded);
|
|
89
|
+
return directDecoded;
|
|
63
90
|
}
|
|
64
91
|
|
|
65
92
|
function isMainSessionFile(filePath, stats) {
|