claude-code-watch 0.1.2 → 0.1.4

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/public/index.html +151 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-watch",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Web-based real-time monitor for Claude Code.",
5
5
  "main": "./src/server/server.js",
6
6
  "bin": {
package/public/index.html CHANGED
@@ -208,6 +208,33 @@ body {
208
208
  .tree-row:hover .tree-actions { display: flex; }
209
209
  .tree-row.selected>.tree-actions { display: flex; }
210
210
 
211
+ /* ── Tokens page ── */
212
+ #tokens-page { flex: 1; display: flex; flex-direction: column; overflow-y: auto; padding: 16px 24px; gap: 16px; background: var(--bg); }
213
+ .token-card { background: var(--bg2); border: 1px solid var(--border); border-radius: 8px; padding: 12px 16px; }
214
+ .token-card-title { font-size: 13px; font-weight: 600; color: var(--white); margin-bottom: 8px; display: flex; align-items: center; gap: 6px; }
215
+ .token-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 8px; }
216
+ .token-item { display: flex; flex-direction: column; gap: 2px; }
217
+ .token-label { font-size: 10px; color: var(--dim); text-transform: uppercase; letter-spacing: 0.5px; }
218
+ .token-value { font-size: 16px; font-weight: 600; color: var(--white); font-family: monospace; }
219
+ .token-bar { height: 4px; border-radius: 2px; background: var(--bg3); margin-top: 4px; }
220
+ .token-bar-fill { height: 100%; border-radius: 2px; background: var(--purple); transition: width 0.3s; }
221
+ .token-bar-fill.warn { background: var(--yellow); }
222
+ .token-bar-fill.danger { background: var(--red); }
223
+ .token-section-title { font-size: 12px; font-weight: 600; color: var(--dim); margin: 8px 0 4px; padding-bottom: 4px; border-bottom: 1px solid var(--border); }
224
+ .token-table { width: 100%; border-collapse: collapse; font-size: 12px; margin-top: 4px; }
225
+ .token-table th { background: var(--bg3); color: var(--white); padding: 6px 8px; text-align: left; font-weight: 600; font-size: 11px; border-bottom: 1px solid var(--border); }
226
+ .token-table td { padding: 5px 8px; color: var(--text); border-bottom: 1px solid var(--border); font-family: monospace; }
227
+ .token-table tr:hover td { background: var(--bg3); }
228
+ .token-detail-row { display: flex; gap: 4px; margin-top: 6px; font-size: 12px; flex-wrap: wrap; }
229
+ .token-detail-row .token-kv { color: var(--dim); }
230
+ .token-detail-row .token-kv b { color: var(--text); font-family: monospace; }
231
+ .token-usage-line { display: flex; align-items: baseline; gap: 8px; margin-top: 2px; }
232
+ .token-usage-line .token-pct { font-size: 12px; font-weight: 600; }
233
+ .token-usage-line .token-pct.warn { color: var(--yellow); }
234
+ .token-usage-line .token-pct.danger { color: var(--red); }
235
+ .token-usage-line .token-ctx-info { font-size: 11px; color: var(--dim); }
236
+ .token-active-dot { font-size: 10px; }
237
+
211
238
  /* ── Stream panel ── */
212
239
  #stream-panel-wrap {
213
240
  flex: 1; display: flex; flex-direction: column; overflow: hidden;
@@ -380,6 +407,9 @@ body {
380
407
  <body>
381
408
 
382
409
  <div id="header">
410
+ <button class="btn on" id="tab-stream" onclick="switchTab('stream')">📡 Stream</button>
411
+ <button class="btn" id="tab-tokens" onclick="switchTab('tokens')">📊 Tokens</button>
412
+ <span class="sep">│</span>
383
413
  <button class="btn on" id="btn-thinking" onclick="toggleThinking()" data-tooltip="Toggle thinking">🧠 Thinking</button>
384
414
  <button class="btn on" id="btn-tool-input" onclick="toggleToolInput()" data-tooltip="Toggle tool input">🔧 Tools</button>
385
415
  <button class="btn on" id="btn-tool-output" onclick="toggleToolOutput()" data-tooltip="Toggle tool output">📤 Output</button>
@@ -424,6 +454,22 @@ body {
424
454
  </div>
425
455
  </div>
426
456
 
457
+ <div id="tokens-page" style="display:none">
458
+ <div class="token-card" id="token-overview">
459
+ <div class="token-card-title">📊 Token Overview</div>
460
+ <div class="token-grid" id="token-overview-grid"></div>
461
+ </div>
462
+ <div class="token-section-title">按 Agent 分项</div>
463
+ <div id="token-agent-cards"></div>
464
+ <div class="token-section-title" style="cursor:pointer" onclick="toggleTokenTable()" id="token-table-toggle">明细表格 ▸</div>
465
+ <div id="token-table-wrap" style="display:none">
466
+ <table class="token-table" id="token-detail-table">
467
+ <thead><tr><th>Agent</th><th>Model</th><th>Input</th><th>Output</th><th>Cache+</th><th>Cache Read</th><th>Context</th><th>%</th><th>I/O</th></tr></thead>
468
+ <tbody id="token-table-body"></tbody>
469
+ </table>
470
+ </div>
471
+ </div>
472
+
427
473
  <div id="footer">
428
474
  <span id="scroll-pos">0%</span>
429
475
  <span class="sep">│</span>
@@ -516,6 +562,7 @@ let showActivity = true;
516
562
  let showTokenCount = true;
517
563
  let autoDiscovery = true;
518
564
  let appVersion = '';
565
+ let currentTab = 'stream';
519
566
 
520
567
  const HIDDEN_KEY = 'claude-watch-hidden';
521
568
  function loadHiddenSessions() {
@@ -546,8 +593,8 @@ function computeTokensFromContext() {
546
593
  for (const ctx of Object.values(contextData)) {
547
594
  totalInput += ctx.inputTokens || 0;
548
595
  totalOutput += ctx.outputTokens || 0;
549
- totalCacheCreate += ctx.cacheCreationTokens || 0;
550
- totalCacheRead += ctx.cacheReadTokens || 0;
596
+ totalCacheCreate += ctx.cacheCreation || 0;
597
+ totalCacheRead += ctx.cacheRead || 0;
551
598
  }
552
599
  }
553
600
 
@@ -676,7 +723,7 @@ function handleMessage(msg) {
676
723
  case 'newBackgroundTask': handleNewBgTask(msg.payload); break;
677
724
  case 'sessionRemoved': handleSessionRemoved(msg.payload); break;
678
725
  case 'autoDiscoveryChanged': autoDiscovery = msg.payload.enabled; scheduleRender(); break;
679
- case 'context': contextData = msg.payload; updateTreeDots(); refreshButtons(); break;
726
+ case 'context': contextData = msg.payload; updateTreeDots(); refreshButtons(); if (currentTab === 'tokens') renderTokenPage(); break;
680
727
  case 'config':
681
728
  if (msg.payload.version) appVersion = msg.payload.version;
682
729
  if (msg.payload.collapseAfter > 0 && !collapseTimer) {
@@ -1630,6 +1677,107 @@ function toggleTokenDisplay() {
1630
1677
  scheduleRender();
1631
1678
  refreshButtons();
1632
1679
  }
1680
+
1681
+ // ══════════════════════════════════════════════════════════════════════════════
1682
+ // Tab switching & Token page
1683
+ // ══════════════════════════════════════════════════════════════════════════════
1684
+
1685
+ function switchTab(tab) {
1686
+ currentTab = tab;
1687
+ document.getElementById('main').style.display = tab === 'stream' ? 'flex' : 'none';
1688
+ document.getElementById('tokens-page').style.display = tab === 'tokens' ? 'flex' : 'none';
1689
+ document.getElementById('tab-stream').classList.toggle('on', tab === 'stream');
1690
+ document.getElementById('tab-tokens').classList.toggle('on', tab === 'tokens');
1691
+ // footer 只在 stream 模式下有意义
1692
+ document.getElementById('footer').style.display = tab === 'stream' ? 'flex' : 'none';
1693
+ if (tab === 'tokens') renderTokenPage();
1694
+ }
1695
+
1696
+ let tokenTableVisible = false;
1697
+ function toggleTokenTable() {
1698
+ tokenTableVisible = !tokenTableVisible;
1699
+ document.getElementById('token-table-wrap').style.display = tokenTableVisible ? 'block' : 'none';
1700
+ document.getElementById('token-table-toggle').textContent = '明细表格 ' + (tokenTableVisible ? '▾' : '▸');
1701
+ }
1702
+
1703
+ function renderTokenPage() {
1704
+ computeTokensFromContext();
1705
+ const entries = Object.entries(contextData);
1706
+ if (entries.length === 0) {
1707
+ document.getElementById('token-overview-grid').innerHTML = '<div style="color:var(--dim);padding:8px">暂无 Token 数据</div>';
1708
+ document.getElementById('token-agent-cards').innerHTML = '';
1709
+ document.getElementById('token-table-body').innerHTML = '';
1710
+ return;
1711
+ }
1712
+
1713
+ // ── Overview card ──
1714
+ const overviewGrid = document.getElementById('token-overview-grid');
1715
+ const overviewItems = [
1716
+ { label: 'Input Tokens', value: fmtTok(totalInput), pct: null },
1717
+ { label: 'Output Tokens', value: fmtTok(totalOutput), pct: null },
1718
+ { label: 'Cache Creation', value: fmtTok(totalCacheCreate), pct: null },
1719
+ { label: 'Cache Read', value: fmtTok(totalCacheRead), pct: null },
1720
+ { label: 'I/O Ratio', value: totalOutput > 0 ? (totalInput / totalOutput).toFixed(1) + ' : 1' : '—', pct: null },
1721
+ ];
1722
+ overviewGrid.innerHTML = overviewItems.map(it =>
1723
+ `<div class="token-item"><span class="token-label">${it.label}</span><span class="token-value">${it.value}</span></div>`
1724
+ ).join('');
1725
+
1726
+ // ── Agent cards ──
1727
+ const agentCardsEl = document.getElementById('token-agent-cards');
1728
+ // Sort by lastActivity descending (active first)
1729
+ const sorted = entries.sort((a, b) => (b[1].lastActivity || 0) - (a[1].lastActivity || 0));
1730
+ agentCardsEl.innerHTML = sorted.map(([key, ctx]) => {
1731
+ const [sid, agentId] = key.split(':');
1732
+ const isMain = agentId === 'main' || !agentId.includes('-');
1733
+ const icon = isMain ? '🗣' : '🤖';
1734
+ const agentName = isMain ? 'Main' : agentId;
1735
+ const active = ctx.lastActivity && (Date.now() - ctx.lastActivity < 180000);
1736
+ const activeDot = active ? '<span class="token-active-dot">🟢</span>' : '<span class="token-active-dot">⚪</span>';
1737
+
1738
+ const pct = ctx.contextWindow > 0 ? Math.round(ctx.inputTokens / ctx.contextWindow * 100) : 0;
1739
+ const pctCls = pct > 80 ? 'danger' : pct > 50 ? 'warn' : '';
1740
+ const barCls = pct > 80 ? 'danger' : pct > 50 ? 'warn' : '';
1741
+ const barWidth = Math.min(pct, 100);
1742
+
1743
+ const ioRatio = ctx.outputTokens > 0 ? (ctx.inputTokens / ctx.outputTokens).toFixed(1) + ' : 1' : '—';
1744
+
1745
+ return `<div class="token-card">
1746
+ <div class="token-card-title">${icon} ${esc(agentName)} ${ctx.model ? '· ' + esc(ctx.model) : ''} ${activeDot}</div>
1747
+ <div class="token-usage-line">
1748
+ <span class="token-pct ${pctCls}">${pct}%</span>
1749
+ <span class="token-ctx-info">${fmtTok(ctx.inputTokens)} / ${fmtTok(ctx.contextWindow)}</span>
1750
+ </div>
1751
+ <div class="token-bar"><div class="token-bar-fill ${barCls}" style="width:${barWidth}%"></div></div>
1752
+ <div class="token-detail-row">
1753
+ <span class="token-kv">Output: <b>${fmtTok(ctx.outputTokens)}</b></span>
1754
+ <span class="token-kv">Cache+: <b>${fmtTok(ctx.cacheCreation)}</b></span>
1755
+ <span class="token-kv">Cache Read: <b>${fmtTok(ctx.cacheRead)}</b></span>
1756
+ <span class="token-kv">I/O: <b>${ioRatio}</b></span>
1757
+ </div>
1758
+ </div>`;
1759
+ }).join('');
1760
+
1761
+ // ── Detail table ──
1762
+ const tbody = document.getElementById('token-table-body');
1763
+ tbody.innerHTML = sorted.map(([key, ctx]) => {
1764
+ const [sid, agentId] = key.split(':');
1765
+ const agentName = (agentId === 'main' || !agentId.includes('-')) ? 'Main' : agentId;
1766
+ const pct = ctx.contextWindow > 0 ? (ctx.inputTokens / ctx.contextWindow * 100).toFixed(1) + '%' : '—';
1767
+ const ioRatio = ctx.outputTokens > 0 ? (ctx.inputTokens / ctx.outputTokens).toFixed(1) : '—';
1768
+ return `<tr>
1769
+ <td>${esc(agentName)}</td>
1770
+ <td>${esc(ctx.model || '—')}</td>
1771
+ <td>${ctx.inputTokens}</td>
1772
+ <td>${ctx.outputTokens}</td>
1773
+ <td>${ctx.cacheCreation}</td>
1774
+ <td>${ctx.cacheRead}</td>
1775
+ <td>${ctx.contextWindow}</td>
1776
+ <td>${pct}</td>
1777
+ <td>${ioRatio}</td>
1778
+ </tr>`;
1779
+ }).join('');
1780
+ }
1633
1781
  function toggleAutoScroll() { autoScroll = !autoScroll; if (autoScroll) streamEl.scrollTop = streamEl.scrollHeight; renderAll(); }
1634
1782
  function toggleTree() { showTree = !showTree; document.getElementById('tree-panel').classList.toggle('hidden', !showTree); }
1635
1783
  function toggleAutoDiscovery() { sendCmd('toggleAutoDiscovery'); }