@towles/tool 0.0.20 → 0.0.48

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 (45) hide show
  1. package/{LICENSE.md → LICENSE} +1 -1
  2. package/README.md +86 -85
  3. package/bin/run.ts +5 -0
  4. package/package.json +84 -64
  5. package/patches/prompts.patch +34 -0
  6. package/src/commands/base.ts +27 -0
  7. package/src/commands/config.test.ts +15 -0
  8. package/src/commands/config.ts +44 -0
  9. package/src/commands/doctor.ts +136 -0
  10. package/src/commands/gh/branch-clean.ts +116 -0
  11. package/src/commands/gh/branch.test.ts +124 -0
  12. package/src/commands/gh/branch.ts +135 -0
  13. package/src/commands/gh/pr.ts +175 -0
  14. package/src/commands/graph-template.html +1214 -0
  15. package/src/commands/graph.test.ts +176 -0
  16. package/src/commands/graph.ts +970 -0
  17. package/src/commands/install.ts +154 -0
  18. package/src/commands/journal/daily-notes.ts +70 -0
  19. package/src/commands/journal/meeting.ts +89 -0
  20. package/src/commands/journal/note.ts +89 -0
  21. package/src/commands/ralph/plan/add.ts +75 -0
  22. package/src/commands/ralph/plan/done.ts +82 -0
  23. package/src/commands/ralph/plan/list.test.ts +48 -0
  24. package/src/commands/ralph/plan/list.ts +99 -0
  25. package/src/commands/ralph/plan/remove.ts +71 -0
  26. package/src/commands/ralph/run.test.ts +521 -0
  27. package/src/commands/ralph/run.ts +345 -0
  28. package/src/commands/ralph/show.ts +88 -0
  29. package/src/config/settings.ts +136 -0
  30. package/src/lib/journal/utils.ts +399 -0
  31. package/src/lib/ralph/execution.ts +292 -0
  32. package/src/lib/ralph/formatter.ts +238 -0
  33. package/src/lib/ralph/index.ts +4 -0
  34. package/src/lib/ralph/state.ts +166 -0
  35. package/src/types/journal.ts +16 -0
  36. package/src/utils/date-utils.test.ts +97 -0
  37. package/src/utils/date-utils.ts +54 -0
  38. package/src/utils/git/gh-cli-wrapper.test.ts +14 -0
  39. package/src/utils/git/gh-cli-wrapper.ts +54 -0
  40. package/src/utils/git/git-wrapper.test.ts +26 -0
  41. package/src/utils/git/git-wrapper.ts +15 -0
  42. package/src/utils/render.test.ts +71 -0
  43. package/src/utils/render.ts +34 -0
  44. package/dist/index.d.mts +0 -1
  45. package/dist/index.mjs +0 -805
@@ -0,0 +1,1214 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Claude Code Usage</title>
7
+ <style>
8
+ * { margin: 0; padding: 0; box-sizing: border-box; }
9
+ body {
10
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
11
+ background: #1a1a2e;
12
+ color: #eee;
13
+ min-height: 100vh;
14
+ padding: 20px;
15
+ }
16
+ h1 {
17
+ font-size: 1.5rem;
18
+ margin-bottom: 15px;
19
+ color: #fff;
20
+ }
21
+ .container {
22
+ max-width: 1540px;
23
+ margin: 0 auto;
24
+ }
25
+ .main-content {
26
+ display: flex;
27
+ gap: 20px;
28
+ }
29
+ .treemap-wrapper {
30
+ flex: 1;
31
+ }
32
+ .detail-panel {
33
+ width: 280px;
34
+ flex-shrink: 0;
35
+ background: #16213e;
36
+ border-radius: 8px;
37
+ padding: 16px;
38
+ height: fit-content;
39
+ max-height: 800px;
40
+ overflow-y: auto;
41
+ }
42
+ .detail-panel.empty {
43
+ color: #666;
44
+ font-size: 0.9rem;
45
+ text-align: center;
46
+ padding: 40px 16px;
47
+ }
48
+ .detail-title {
49
+ font-weight: 600;
50
+ font-size: 1rem;
51
+ margin-bottom: 12px;
52
+ color: #fff;
53
+ word-break: break-word;
54
+ }
55
+ .detail-row {
56
+ display: flex;
57
+ justify-content: space-between;
58
+ gap: 12px;
59
+ margin: 6px 0;
60
+ font-size: 0.85rem;
61
+ }
62
+ .detail-label { color: #888; }
63
+ .detail-value { font-weight: 500; color: #ccc; }
64
+ .detail-actions {
65
+ margin-top: 16px;
66
+ padding-top: 12px;
67
+ border-top: 1px solid #333;
68
+ display: flex;
69
+ flex-direction: column;
70
+ gap: 8px;
71
+ }
72
+ .detail-btn {
73
+ background: #2a2a4a;
74
+ border: 1px solid #444;
75
+ color: #6b9fff;
76
+ padding: 8px 12px;
77
+ border-radius: 4px;
78
+ cursor: pointer;
79
+ font-size: 0.85rem;
80
+ text-align: left;
81
+ }
82
+ .detail-btn:hover {
83
+ background: #3a3a5a;
84
+ border-color: #666;
85
+ }
86
+ .legend {
87
+ display: flex;
88
+ gap: 20px;
89
+ margin-bottom: 15px;
90
+ font-size: 0.85rem;
91
+ }
92
+ .legend-item {
93
+ display: flex;
94
+ align-items: center;
95
+ gap: 6px;
96
+ }
97
+ .legend-color {
98
+ width: 20px;
99
+ height: 14px;
100
+ border-radius: 3px;
101
+ }
102
+ #treemap {
103
+ position: relative;
104
+ width: {{WIDTH}}px;
105
+ height: {{HEIGHT}}px;
106
+ background: #16213e;
107
+ border-radius: 8px;
108
+ overflow: hidden;
109
+ }
110
+ #barchart {
111
+ display: none;
112
+ width: {{WIDTH}}px;
113
+ height: {{HEIGHT}}px;
114
+ background: #16213e;
115
+ border-radius: 8px;
116
+ overflow: hidden;
117
+ }
118
+ #barchart svg {
119
+ display: block;
120
+ }
121
+ .bar-segment {
122
+ cursor: pointer;
123
+ transition: opacity 0.15s;
124
+ }
125
+ .bar-segment:hover {
126
+ opacity: 0.85;
127
+ }
128
+ .bar-segment.selected {
129
+ stroke: #fff;
130
+ stroke-width: 2;
131
+ }
132
+ .axis text {
133
+ fill: #888;
134
+ font-size: 11px;
135
+ }
136
+ .axis line, .axis path {
137
+ stroke: #444;
138
+ }
139
+ .axis-label {
140
+ fill: #888;
141
+ font-size: 12px;
142
+ }
143
+ .node {
144
+ position: absolute;
145
+ overflow: hidden;
146
+ border-radius: 3px;
147
+ transition: opacity 0.15s;
148
+ cursor: pointer;
149
+ }
150
+ .node:hover {
151
+ opacity: 0.85;
152
+ }
153
+ .node-label {
154
+ font-size: 11px;
155
+ padding: 2px 4px;
156
+ white-space: nowrap;
157
+ overflow: hidden;
158
+ text-overflow: ellipsis;
159
+ color: #fff;
160
+ text-shadow: 0 1px 2px rgba(0,0,0,0.5);
161
+ }
162
+ .node-group {
163
+ background: rgba(255,255,255,0.05);
164
+ border: 1px solid rgba(255,255,255,0.1);
165
+ }
166
+ .node-group .node-label {
167
+ font-weight: 600;
168
+ font-size: 12px;
169
+ color: rgba(255,255,255,0.9);
170
+ }
171
+ .tooltip {
172
+ position: fixed;
173
+ background: #2a2a4a;
174
+ border: 1px solid #444;
175
+ border-radius: 6px;
176
+ padding: 12px;
177
+ font-size: 0.85rem;
178
+ pointer-events: none;
179
+ z-index: 1000;
180
+ max-width: 300px;
181
+ box-shadow: 0 4px 12px rgba(0,0,0,0.4);
182
+ display: none;
183
+ }
184
+ .tooltip-title {
185
+ font-weight: 600;
186
+ margin-bottom: 8px;
187
+ color: #fff;
188
+ }
189
+ .tooltip-row {
190
+ display: flex;
191
+ justify-content: space-between;
192
+ gap: 20px;
193
+ margin: 4px 0;
194
+ color: #ccc;
195
+ }
196
+ .tooltip-label { color: #888; }
197
+ .tooltip-value { font-weight: 500; }
198
+ .ratio-good { color: #4ade80; }
199
+ .ratio-moderate { color: #fbbf24; }
200
+ .ratio-high { color: #f87171; }
201
+ .tooltip-link {
202
+ color: #6b9fff;
203
+ cursor: pointer;
204
+ text-decoration: none;
205
+ }
206
+ .tooltip-link:hover {
207
+ text-decoration: underline;
208
+ }
209
+ .tooltip-actions {
210
+ margin-top: 10px;
211
+ padding-top: 8px;
212
+ border-top: 1px solid #444;
213
+ display: flex;
214
+ gap: 12px;
215
+ font-size: 0.8rem;
216
+ }
217
+ .tool-table {
218
+ margin-top: 8px;
219
+ width: 100%;
220
+ border-collapse: collapse;
221
+ font-size: 0.8rem;
222
+ }
223
+ .tool-table th {
224
+ text-align: left;
225
+ color: #888;
226
+ font-weight: 500;
227
+ padding: 3px 6px 3px 0;
228
+ border-bottom: 1px solid #444;
229
+ }
230
+ .tool-table td {
231
+ padding: 3px 6px 3px 0;
232
+ color: #ccc;
233
+ }
234
+ .tool-table td:last-child,
235
+ .tool-table th:last-child {
236
+ text-align: right;
237
+ padding-right: 0;
238
+ }
239
+ .tool-table-header {
240
+ color: #888;
241
+ font-size: 0.75rem;
242
+ margin-top: 10px;
243
+ margin-bottom: 4px;
244
+ }
245
+ .stats {
246
+ margin-top: 15px;
247
+ font-size: 0.85rem;
248
+ color: #888;
249
+ }
250
+ .breadcrumb {
251
+ margin-bottom: 10px;
252
+ font-size: 0.9rem;
253
+ color: #aaa;
254
+ }
255
+ .crumb {
256
+ color: #6b9fff;
257
+ }
258
+ .crumb:hover:not(.current) {
259
+ text-decoration: underline;
260
+ }
261
+ .crumb.current {
262
+ color: #fff;
263
+ cursor: default;
264
+ }
265
+ .crumb-sep {
266
+ color: #666;
267
+ margin: 0 4px;
268
+ }
269
+ .controls {
270
+ display: flex;
271
+ align-items: center;
272
+ gap: 30px;
273
+ margin-bottom: 15px;
274
+ flex-wrap: wrap;
275
+ }
276
+ .tile-selector, .min-tokens {
277
+ display: flex;
278
+ align-items: center;
279
+ gap: 8px;
280
+ }
281
+ .tile-selector label, .min-tokens label {
282
+ color: #888;
283
+ font-size: 0.85rem;
284
+ }
285
+ .tile-selector select, .min-tokens select {
286
+ background: #2a2a4a;
287
+ color: #fff;
288
+ border: 1px solid #444;
289
+ border-radius: 4px;
290
+ padding: 4px 8px;
291
+ font-size: 0.85rem;
292
+ cursor: pointer;
293
+ }
294
+ .tile-selector select:hover, .min-tokens select:hover {
295
+ border-color: #666;
296
+ }
297
+ .view-toggle {
298
+ display: flex;
299
+ align-items: center;
300
+ gap: 8px;
301
+ }
302
+ .view-toggle label {
303
+ color: #888;
304
+ font-size: 0.85rem;
305
+ }
306
+ .view-toggle-btn {
307
+ background: #2a2a4a;
308
+ color: #fff;
309
+ border: 1px solid #444;
310
+ border-radius: 4px;
311
+ padding: 4px 12px;
312
+ font-size: 0.85rem;
313
+ cursor: pointer;
314
+ display: flex;
315
+ align-items: center;
316
+ gap: 6px;
317
+ }
318
+ .view-toggle-btn:hover {
319
+ border-color: #666;
320
+ background: #3a3a5a;
321
+ }
322
+ .view-toggle-btn.active {
323
+ background: #4a4a6a;
324
+ border-color: #6b9fff;
325
+ }
326
+ </style>
327
+ <script src="https://cdn.jsdelivr.net/npm/d3-hierarchy@3"></script>
328
+ </head>
329
+ <body>
330
+ <div class="container">
331
+ <h1>Claude Code Usage</h1>
332
+
333
+ <div class="controls">
334
+ <div class="legend" id="legend"></div>
335
+ <div class="tile-selector">
336
+ <label for="tileMethod">Layout:</label>
337
+ <select id="tileMethod">
338
+ <option value="squarify" selected>Squarify (readable)</option>
339
+ <option value="binary">Binary (balanced)</option>
340
+ <option value="sliceDice">Slice & Dice (alternating)</option>
341
+ </select>
342
+ </div>
343
+ <div class="min-tokens">
344
+ <label for="minTokens">Min tokens:</label>
345
+ <select id="minTokens">
346
+ <option value="0">All</option>
347
+ <option value="100">100+</option>
348
+ <option value="500">500+</option>
349
+ <option value="1000" selected>1K+</option>
350
+ <option value="5000">5K+</option>
351
+ <option value="10000">10K+</option>
352
+ </select>
353
+ </div>
354
+ <div class="view-toggle">
355
+ <label>View:</label>
356
+ <button id="viewTreemap" class="view-toggle-btn">Treemap</button>
357
+ <button id="viewBarChart" class="view-toggle-btn active">Bar Chart</button>
358
+ </div>
359
+ </div>
360
+
361
+ <div class="main-content">
362
+ <div class="treemap-wrapper">
363
+ <div id="treemap"></div>
364
+ <div id="barchart"></div>
365
+ <div class="tooltip" id="tooltip"></div>
366
+ <div class="breadcrumb" id="breadcrumb"></div>
367
+ <div class="stats" id="stats"></div>
368
+ </div>
369
+ <div class="detail-panel empty" id="detailPanel">Click a tile to see details</div>
370
+ </div>
371
+ </div>
372
+
373
+ <script>
374
+ const treeData = {{DATA}};
375
+ const barChartData = {{BAR_CHART_DATA}};
376
+ const container = document.getElementById('treemap');
377
+ const barChartContainer = document.getElementById('barchart');
378
+ const tooltip = document.getElementById('tooltip');
379
+ const stats = document.getElementById('stats');
380
+ const breadcrumb = document.getElementById('breadcrumb');
381
+ const detailPanel = document.getElementById('detailPanel');
382
+ const WIDTH = {{WIDTH}};
383
+ const HEIGHT = {{HEIGHT}};
384
+
385
+ // Controls
386
+ const tileMethodSelect = document.getElementById('tileMethod');
387
+ const minTokensSelect = document.getElementById('minTokens');
388
+ const viewTreemapBtn = document.getElementById('viewTreemap');
389
+ const viewBarChartBtn = document.getElementById('viewBarChart');
390
+ const legendContainer = document.getElementById('legend');
391
+
392
+ // Tool colors for treemap legend
393
+ const toolLegendItems = [
394
+ { color: '#4ade80', label: 'Read' },
395
+ { color: '#f87171', label: 'Write' },
396
+ { color: '#fb923c', label: 'Edit' },
397
+ { color: '#a78bfa', label: 'Bash' },
398
+ { color: '#38bdf8', label: 'Glob' },
399
+ { color: '#22d3ee', label: 'Grep' },
400
+ { color: '#facc15', label: 'Task' },
401
+ { color: '#60a5fa', label: 'MCP' },
402
+ ];
403
+
404
+ function renderLegend(items) {
405
+ legendContainer.replaceChildren();
406
+ for (const item of items) {
407
+ const div = document.createElement('div');
408
+ div.className = 'legend-item';
409
+ const colorBox = document.createElement('div');
410
+ colorBox.className = 'legend-color';
411
+ colorBox.style.background = item.color;
412
+ const label = document.createElement('span');
413
+ label.textContent = item.label;
414
+ div.appendChild(colorBox);
415
+ div.appendChild(label);
416
+ legendContainer.appendChild(div);
417
+ }
418
+ }
419
+
420
+ // View state: 'treemap' | 'bar'
421
+ let currentView = 'bar';
422
+
423
+ function showTreemap() {
424
+ currentView = 'treemap';
425
+ container.style.display = 'block';
426
+ barChartContainer.style.display = 'none';
427
+ viewTreemapBtn.classList.add('active');
428
+ viewBarChartBtn.classList.remove('active');
429
+ // Show treemap-specific controls
430
+ tileMethodSelect.parentElement.style.display = 'flex';
431
+ renderLegend(toolLegendItems);
432
+ render();
433
+ }
434
+
435
+ function showBarChart() {
436
+ currentView = 'bar';
437
+ container.style.display = 'none';
438
+ barChartContainer.style.display = 'block';
439
+ viewTreemapBtn.classList.remove('active');
440
+ viewBarChartBtn.classList.add('active');
441
+ // Hide treemap-specific controls (layout doesn't apply to bar chart)
442
+ tileMethodSelect.parentElement.style.display = 'none';
443
+ // Legend will be populated after rendering with actual projects
444
+ renderBarChart(barChartData);
445
+ }
446
+
447
+ // View toggle handlers
448
+ viewTreemapBtn.addEventListener('click', showTreemap);
449
+ viewBarChartBtn.addEventListener('click', showBarChart);
450
+
451
+ // Navigation stack for zoom
452
+ let navStack = [treeData];
453
+
454
+ function getCurrentNode() {
455
+ return navStack[navStack.length - 1];
456
+ }
457
+
458
+ function zoomTo(node) {
459
+ if (node.children && node.children.length > 0) {
460
+ navStack.push(node);
461
+ render();
462
+ }
463
+ }
464
+
465
+ function zoomOut(index) {
466
+ navStack = navStack.slice(0, index + 1);
467
+ render();
468
+ }
469
+
470
+ // Filter nodes below minTokens threshold
471
+ function filterByMinTokens(node, minTokens) {
472
+ if (minTokens <= 0) return node;
473
+
474
+ function sumValue(n) {
475
+ if (n.value !== undefined && n.value > 0) return n.value;
476
+ if (!n.children) return 0;
477
+ return n.children.reduce((sum, c) => sum + sumValue(c), 0);
478
+ }
479
+
480
+ function filterNode(n) {
481
+ const val = sumValue(n);
482
+ if (val < minTokens && !n.children) return null;
483
+
484
+ if (!n.children) return n;
485
+
486
+ const filteredChildren = n.children
487
+ .map(filterNode)
488
+ .filter(c => c !== null);
489
+
490
+ if (filteredChildren.length === 0 && val < minTokens) return null;
491
+
492
+ return { ...n, children: filteredChildren.length > 0 ? filteredChildren : undefined };
493
+ }
494
+
495
+ return filterNode(node) || node;
496
+ }
497
+
498
+ // Get d3 tile method
499
+ function getTileMethod() {
500
+ const method = tileMethodSelect.value;
501
+ switch (method) {
502
+ case 'binary': return d3.treemapBinary;
503
+ case 'sliceDice': return d3.treemapSliceDice;
504
+ default: return d3.treemapSquarify;
505
+ }
506
+ }
507
+
508
+ function computeLayout(data) {
509
+ const minTokens = parseInt(minTokensSelect.value) || 0;
510
+ const filteredData = filterByMinTokens(data, minTokens);
511
+
512
+ // Use d3-hierarchy for layout
513
+ const root = d3.hierarchy(filteredData)
514
+ .sum(d => d.value || 0)
515
+ .sort((a, b) => (b.value || 0) - (a.value || 0));
516
+
517
+ const layout = d3.treemap()
518
+ .size([WIDTH, HEIGHT])
519
+ .paddingOuter(3)
520
+ .paddingTop(19)
521
+ .paddingInner(1)
522
+ .tile(getTileMethod());
523
+
524
+ layout(root);
525
+
526
+ // Convert to rect array
527
+ return root.descendants().map(d => ({
528
+ x: d.x0,
529
+ y: d.y0,
530
+ width: d.x1 - d.x0,
531
+ height: d.y1 - d.y0,
532
+ depth: d.depth,
533
+ name: d.data.name,
534
+ value: d.value || 0,
535
+ hasChildren: !!d.children?.length,
536
+ sessionId: d.data.sessionId,
537
+ fullSessionId: d.data.fullSessionId,
538
+ filePath: d.data.filePath,
539
+ startTime: d.data.startTime,
540
+ model: d.data.model,
541
+ inputTokens: d.data.inputTokens,
542
+ outputTokens: d.data.outputTokens,
543
+ ratio: d.data.ratio,
544
+ date: d.data.date,
545
+ project: d.data.project,
546
+ repeatedReads: d.data.repeatedReads,
547
+ modelEfficiency: d.data.modelEfficiency,
548
+ tools: d.data.tools,
549
+ toolName: d.data.toolName,
550
+ nodeRef: d.data
551
+ }));
552
+ }
553
+
554
+ // Re-render on control changes
555
+ tileMethodSelect.addEventListener('change', render);
556
+ minTokensSelect.addEventListener('change', render);
557
+
558
+ function render() {
559
+ // Clear container using replaceChildren (safe)
560
+ container.replaceChildren();
561
+ const currentNode = getCurrentNode();
562
+ const rects = computeLayout(currentNode);
563
+
564
+ // Update breadcrumb using DOM methods (safe)
565
+ breadcrumb.replaceChildren();
566
+ navStack.forEach((node, i) => {
567
+ const crumb = document.createElement('span');
568
+ crumb.className = 'crumb' + (i === navStack.length - 1 ? ' current' : '');
569
+ crumb.textContent = node.name;
570
+ if (i < navStack.length - 1) {
571
+ crumb.style.cursor = 'pointer';
572
+ crumb.onclick = () => zoomOut(i);
573
+ }
574
+ breadcrumb.appendChild(crumb);
575
+ if (i < navStack.length - 1) {
576
+ const sep = document.createElement('span');
577
+ sep.className = 'crumb-sep';
578
+ sep.textContent = ' > ';
579
+ breadcrumb.appendChild(sep);
580
+ }
581
+ });
582
+
583
+ // Calculate totals for current view
584
+ let totalTokens = 0, totalInput = 0, totalOutput = 0;
585
+ rects.forEach(r => {
586
+ if (!r.hasChildren && r.depth > 0) {
587
+ totalTokens += r.value || 0;
588
+ totalInput += r.inputTokens || 0;
589
+ totalOutput += r.outputTokens || 0;
590
+ }
591
+ });
592
+
593
+ const overallRatio = totalOutput > 0 ? (totalInput / totalOutput).toFixed(1) : 'N/A';
594
+ stats.textContent = 'Total: ' + formatTokens(totalTokens) + ' tokens | Input: ' + formatTokens(totalInput) + ' | Output: ' + formatTokens(totalOutput) + ' | Ratio: ' + overallRatio + ':1';
595
+
596
+ // Render nodes
597
+ rects.forEach((r) => {
598
+ if (r.width < 1 || r.height < 1) return;
599
+
600
+ const node = document.createElement('div');
601
+ node.className = 'node' + (r.hasChildren ? ' node-group' : '');
602
+ node.style.left = r.x + 'px';
603
+ node.style.top = r.y + 'px';
604
+ node.style.width = r.width + 'px';
605
+ node.style.height = r.height + 'px';
606
+
607
+ if (r.toolName && r.depth > 0) {
608
+ node.style.background = getColor(r.toolName);
609
+ }
610
+
611
+ if (r.width > 30 && r.height > 15) {
612
+ const label = document.createElement('div');
613
+ label.className = 'node-label';
614
+ label.textContent = r.name + (r.value > 0 && !r.hasChildren ? ' (' + formatTokens(r.value) + ')' : '');
615
+ node.appendChild(label);
616
+ }
617
+
618
+ // Click to zoom and/or show detail
619
+ node.addEventListener('click', (e) => {
620
+ e.stopPropagation();
621
+ showDetail(r);
622
+ if (r.hasChildren && r.nodeRef) {
623
+ zoomTo(r.nodeRef);
624
+ }
625
+ });
626
+
627
+ node.addEventListener('mouseenter', (e) => showTooltip(e, r));
628
+ node.addEventListener('mousemove', (e) => moveTooltip(e));
629
+ node.addEventListener('mouseleave', hideTooltip);
630
+
631
+ container.appendChild(node);
632
+ });
633
+ }
634
+
635
+ const toolColors = {
636
+ Read: '#4ade80', // green
637
+ Write: '#f87171', // red
638
+ Edit: '#fb923c', // orange
639
+ MultiEdit: '#f97316', // darker orange
640
+ Bash: '#a78bfa', // purple
641
+ Glob: '#38bdf8', // sky blue
642
+ Grep: '#22d3ee', // cyan
643
+ Task: '#facc15', // yellow
644
+ WebFetch: '#2dd4bf', // teal
645
+ WebSearch: '#14b8a6', // darker teal
646
+ TodoWrite: '#e879f9', // pink
647
+ LSP: '#818cf8', // indigo
648
+ Response: '#cbd5e1', // light gray for text-only responses
649
+ AskUserQuestion: '#f472b6', // pink
650
+ default: '#94a3b8' // gray
651
+ };
652
+
653
+ function getColor(toolName) {
654
+ if (!toolName) return '#4a5568';
655
+ // Check for MCP tools (mcp__*)
656
+ if (toolName.startsWith('mcp__')) return '#60a5fa'; // blue for MCP
657
+ return toolColors[toolName] || toolColors.default;
658
+ }
659
+
660
+ function getRatioClass(ratio) {
661
+ if (ratio === undefined || ratio === null) return '';
662
+ if (ratio < 2) return 'ratio-good';
663
+ if (ratio < 5) return 'ratio-moderate';
664
+ return 'ratio-high';
665
+ }
666
+
667
+ function formatTokens(n) {
668
+ if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
669
+ if (n >= 1000) return (n / 1000).toFixed(1) + 'K';
670
+ return n.toString();
671
+ }
672
+
673
+ function showTooltip(e, r) {
674
+ // Build tooltip using DOM methods (safe from XSS)
675
+ tooltip.replaceChildren();
676
+
677
+ const title = document.createElement('div');
678
+ title.className = 'tooltip-title';
679
+ title.textContent = r.name;
680
+ tooltip.appendChild(title);
681
+
682
+ function addRow(label, value, extraClass) {
683
+ const row = document.createElement('div');
684
+ row.className = 'tooltip-row';
685
+ const labelEl = document.createElement('span');
686
+ labelEl.className = 'tooltip-label';
687
+ labelEl.textContent = label;
688
+ const valueEl = document.createElement('span');
689
+ valueEl.className = 'tooltip-value' + (extraClass ? ' ' + extraClass : '');
690
+ valueEl.textContent = value;
691
+ row.appendChild(labelEl);
692
+ row.appendChild(valueEl);
693
+ tooltip.appendChild(row);
694
+ }
695
+
696
+ function addLinkRow(label, text, onClick, title) {
697
+ const row = document.createElement('div');
698
+ row.className = 'tooltip-row';
699
+ const labelEl = document.createElement('span');
700
+ labelEl.className = 'tooltip-label';
701
+ labelEl.textContent = label;
702
+ const link = document.createElement('span');
703
+ link.className = 'tooltip-link';
704
+ link.textContent = text;
705
+ if (title) link.title = title;
706
+ link.onclick = (e) => { e.stopPropagation(); onClick(); };
707
+ row.appendChild(labelEl);
708
+ row.appendChild(link);
709
+ tooltip.appendChild(row);
710
+ }
711
+
712
+ if (r.sessionId) {
713
+ addLinkRow('Session:', r.sessionId, () => {
714
+ navigator.clipboard.writeText(r.fullSessionId || r.sessionId);
715
+ }, 'Click to copy full session ID');
716
+ }
717
+ if (r.date) addRow('Date:', r.date);
718
+ if (r.startTime) addRow('Started:', r.startTime);
719
+ if (r.model) addRow('Model:', r.model);
720
+ if (r.value > 0) addRow('Total tokens:', formatTokens(r.value));
721
+ if (r.inputTokens !== undefined) addRow('Input:', formatTokens(r.inputTokens));
722
+ if (r.outputTokens !== undefined) addRow('Output:', formatTokens(r.outputTokens));
723
+ if (r.ratio !== undefined && r.ratio !== null) {
724
+ addRow('Ratio (in:out):', r.ratio.toFixed(1) + ':1', getRatioClass(r.ratio));
725
+ }
726
+ if (r.repeatedReads !== undefined && r.repeatedReads > 0) {
727
+ addRow('Repeated reads:', r.repeatedReads.toString());
728
+ }
729
+ if (r.modelEfficiency !== undefined && r.modelEfficiency > 0) {
730
+ addRow('Opus usage:', (r.modelEfficiency * 100).toFixed(0) + '%');
731
+ }
732
+
733
+ // Tool breakdown table
734
+ if (r.tools && r.tools.length > 0) {
735
+ const header = document.createElement('div');
736
+ header.className = 'tool-table-header';
737
+ header.textContent = 'Tool Usage';
738
+ tooltip.appendChild(header);
739
+
740
+ const table = document.createElement('table');
741
+ table.className = 'tool-table';
742
+
743
+ const thead = document.createElement('thead');
744
+ const headerRow = document.createElement('tr');
745
+ ['Tool', 'Detail', 'Tokens'].forEach(text => {
746
+ const th = document.createElement('th');
747
+ th.textContent = text;
748
+ headerRow.appendChild(th);
749
+ });
750
+ thead.appendChild(headerRow);
751
+ table.appendChild(thead);
752
+
753
+ const tbody = document.createElement('tbody');
754
+ r.tools.forEach(tool => {
755
+ const tr = document.createElement('tr');
756
+ const tdName = document.createElement('td');
757
+ tdName.textContent = tool.name;
758
+ const tdDetail = document.createElement('td');
759
+ tdDetail.textContent = tool.detail || '';
760
+ const tdTokens = document.createElement('td');
761
+ tdTokens.textContent = formatTokens(tool.inputTokens + tool.outputTokens);
762
+ tr.appendChild(tdName);
763
+ tr.appendChild(tdDetail);
764
+ tr.appendChild(tdTokens);
765
+ tbody.appendChild(tr);
766
+ });
767
+ table.appendChild(tbody);
768
+ tooltip.appendChild(table);
769
+ }
770
+
771
+ // Actions section with links
772
+ if (r.fullSessionId || r.filePath) {
773
+ const actions = document.createElement('div');
774
+ actions.className = 'tooltip-actions';
775
+
776
+ if (r.filePath) {
777
+ const fileLink = document.createElement('span');
778
+ fileLink.className = 'tooltip-link';
779
+ fileLink.textContent = '📄 Copy path';
780
+ fileLink.title = r.filePath;
781
+ fileLink.onclick = (e) => {
782
+ e.stopPropagation();
783
+ navigator.clipboard.writeText(r.filePath);
784
+ };
785
+ actions.appendChild(fileLink);
786
+ }
787
+
788
+ if (r.fullSessionId) {
789
+ const transcriptLink = document.createElement('span');
790
+ transcriptLink.className = 'tooltip-link';
791
+ transcriptLink.textContent = '📜 View transcript';
792
+ transcriptLink.title = 'Copy command to view with claude-code-transcripts';
793
+ transcriptLink.onclick = (e) => {
794
+ e.stopPropagation();
795
+ navigator.clipboard.writeText('uvx claude-code-transcripts ' + r.fullSessionId);
796
+ };
797
+ actions.appendChild(transcriptLink);
798
+ }
799
+
800
+ tooltip.appendChild(actions);
801
+ }
802
+
803
+ tooltip.style.display = 'block';
804
+ moveTooltip(e);
805
+ }
806
+
807
+ function moveTooltip(e) {
808
+ const x = e.clientX + 15;
809
+ const y = e.clientY + 15;
810
+ const rect = tooltip.getBoundingClientRect();
811
+
812
+ tooltip.style.left = (x + rect.width > window.innerWidth ? e.clientX - rect.width - 15 : x) + 'px';
813
+ tooltip.style.top = (y + rect.height > window.innerHeight ? e.clientY - rect.height - 15 : y) + 'px';
814
+ }
815
+
816
+ function hideTooltip() {
817
+ tooltip.style.display = 'none';
818
+ }
819
+
820
+ function showDetail(r) {
821
+ detailPanel.className = 'detail-panel';
822
+ detailPanel.replaceChildren();
823
+
824
+ const title = document.createElement('div');
825
+ title.className = 'detail-title';
826
+ title.textContent = r.name;
827
+ detailPanel.appendChild(title);
828
+
829
+ function addDetailRow(label, value, extraClass) {
830
+ const row = document.createElement('div');
831
+ row.className = 'detail-row';
832
+ const labelEl = document.createElement('span');
833
+ labelEl.className = 'detail-label';
834
+ labelEl.textContent = label;
835
+ const valueEl = document.createElement('span');
836
+ valueEl.className = 'detail-value' + (extraClass ? ' ' + extraClass : '');
837
+ valueEl.textContent = value;
838
+ row.appendChild(labelEl);
839
+ row.appendChild(valueEl);
840
+ detailPanel.appendChild(row);
841
+ }
842
+
843
+ if (r.sessionId) addDetailRow('Session:', r.fullSessionId || r.sessionId);
844
+ if (r.date) addDetailRow('Date:', r.date);
845
+ if (r.startTime) addDetailRow('Started:', r.startTime);
846
+ if (r.model) addDetailRow('Model:', r.model);
847
+ if (r.value > 0) addDetailRow('Total tokens:', formatTokens(r.value));
848
+ if (r.inputTokens !== undefined) addDetailRow('Input:', formatTokens(r.inputTokens));
849
+ if (r.outputTokens !== undefined) addDetailRow('Output:', formatTokens(r.outputTokens));
850
+ if (r.ratio !== undefined && r.ratio !== null) {
851
+ addDetailRow('Ratio (in:out):', r.ratio.toFixed(1) + ':1', getRatioClass(r.ratio));
852
+ }
853
+
854
+ // Action buttons
855
+ if (r.fullSessionId || r.filePath) {
856
+ const actions = document.createElement('div');
857
+ actions.className = 'detail-actions';
858
+
859
+ if (r.filePath) {
860
+ const fileBtn = document.createElement('button');
861
+ fileBtn.className = 'detail-btn';
862
+ fileBtn.textContent = '📄 Copy file path';
863
+ fileBtn.onclick = () => {
864
+ navigator.clipboard.writeText(r.filePath);
865
+ fileBtn.textContent = '✓ Copied!';
866
+ setTimeout(() => fileBtn.textContent = '📄 Copy file path', 1500);
867
+ };
868
+ actions.appendChild(fileBtn);
869
+ }
870
+
871
+ if (r.fullSessionId) {
872
+ const copyIdBtn = document.createElement('button');
873
+ copyIdBtn.className = 'detail-btn';
874
+ copyIdBtn.textContent = '🔗 Copy session ID';
875
+ copyIdBtn.onclick = () => {
876
+ navigator.clipboard.writeText(r.fullSessionId);
877
+ copyIdBtn.textContent = '✓ Copied!';
878
+ setTimeout(() => copyIdBtn.textContent = '🔗 Copy session ID', 1500);
879
+ };
880
+ actions.appendChild(copyIdBtn);
881
+
882
+ const transcriptBtn = document.createElement('button');
883
+ transcriptBtn.className = 'detail-btn';
884
+ transcriptBtn.textContent = '📜 View transcript';
885
+ transcriptBtn.title = 'Copy command - use start time above when prompted';
886
+ transcriptBtn.onclick = () => {
887
+ navigator.clipboard.writeText('uvx claude-code-transcripts ' + r.fullSessionId);
888
+ transcriptBtn.textContent = '✓ Copied!';
889
+ setTimeout(() => transcriptBtn.textContent = '📜 View transcript', 1500);
890
+ };
891
+ actions.appendChild(transcriptBtn);
892
+ }
893
+
894
+ detailPanel.appendChild(actions);
895
+ }
896
+ }
897
+
898
+ render();
899
+
900
+ // Initialize bar chart view after defining renderBarChart below
901
+ setTimeout(() => showBarChart(), 0);
902
+
903
+ // Project colors for bar chart (distinct colors for different projects)
904
+ const projectColorPalette = [
905
+ '#60a5fa', // blue
906
+ '#4ade80', // green
907
+ '#a78bfa', // purple
908
+ '#f87171', // red
909
+ '#fbbf24', // amber
910
+ '#22d3ee', // cyan
911
+ '#f472b6', // pink
912
+ '#fb923c', // orange
913
+ '#2dd4bf', // teal
914
+ '#818cf8', // indigo
915
+ ];
916
+ const projectColorMap = new Map();
917
+ let nextColorIndex = 0;
918
+
919
+ function getProjectColor(project) {
920
+ if (!projectColorMap.has(project)) {
921
+ projectColorMap.set(project, projectColorPalette[nextColorIndex % projectColorPalette.length]);
922
+ nextColorIndex++;
923
+ }
924
+ return projectColorMap.get(project);
925
+ }
926
+
927
+ // Selected bar segment state
928
+ let selectedBarSegment = null;
929
+
930
+ /**
931
+ * Render stacked bar chart visualization.
932
+ * Each day shows stacked bars by project folder.
933
+ */
934
+ function renderBarChart(data) {
935
+ barChartContainer.replaceChildren();
936
+ projectColorMap.clear();
937
+ nextColorIndex = 0;
938
+
939
+ if (!data || !data.days || data.days.length === 0) {
940
+ const msg = document.createElement('div');
941
+ msg.style.cssText = 'color: #666; text-align: center; padding: 40px;';
942
+ msg.textContent = 'No bar chart data available';
943
+ barChartContainer.appendChild(msg);
944
+ return;
945
+ }
946
+
947
+ const margin = { top: 30, right: 30, bottom: 60, left: 70 };
948
+ const width = WIDTH - margin.left - margin.right;
949
+ const height = HEIGHT - margin.top - margin.bottom;
950
+
951
+ // Create SVG
952
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
953
+ svg.setAttribute('width', WIDTH);
954
+ svg.setAttribute('height', HEIGHT);
955
+ barChartContainer.appendChild(svg);
956
+
957
+ const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
958
+ g.setAttribute('transform', `translate(${margin.left},${margin.top})`);
959
+ svg.appendChild(g);
960
+
961
+ // Calculate max tokens per day for y-axis scale
962
+ let maxDayTokens = 0;
963
+ for (const day of data.days) {
964
+ let dayTotal = 0;
965
+ for (const proj of day.projects) {
966
+ dayTotal += proj.totalTokens;
967
+ }
968
+ if (dayTotal > maxDayTokens) maxDayTokens = dayTotal;
969
+ }
970
+
971
+ // X scale: dates
972
+ const xDomain = data.days.map(d => d.date);
973
+ const xBandWidth = width / xDomain.length;
974
+ const xScale = (date) => xDomain.indexOf(date) * xBandWidth + xBandWidth / 2;
975
+
976
+ // Y scale: tokens
977
+ const yScale = (tokens) => height - (tokens / maxDayTokens) * height;
978
+ const yHeight = (tokens) => (tokens / maxDayTokens) * height;
979
+
980
+ // Draw axes
981
+ // X axis
982
+ const xAxisG = document.createElementNS('http://www.w3.org/2000/svg', 'g');
983
+ xAxisG.setAttribute('class', 'axis');
984
+ xAxisG.setAttribute('transform', `translate(0,${height})`);
985
+ g.appendChild(xAxisG);
986
+
987
+ // X axis line
988
+ const xLine = document.createElementNS('http://www.w3.org/2000/svg', 'line');
989
+ xLine.setAttribute('x1', 0);
990
+ xLine.setAttribute('x2', width);
991
+ xLine.setAttribute('stroke', '#444');
992
+ xAxisG.appendChild(xLine);
993
+
994
+ // X axis labels
995
+ for (const date of xDomain) {
996
+ const x = xScale(date);
997
+ const tick = document.createElementNS('http://www.w3.org/2000/svg', 'line');
998
+ tick.setAttribute('x1', x);
999
+ tick.setAttribute('x2', x);
1000
+ tick.setAttribute('y1', 0);
1001
+ tick.setAttribute('y2', 6);
1002
+ tick.setAttribute('stroke', '#444');
1003
+ xAxisG.appendChild(tick);
1004
+
1005
+ const label = document.createElementNS('http://www.w3.org/2000/svg', 'text');
1006
+ label.setAttribute('x', x);
1007
+ label.setAttribute('y', 20);
1008
+ label.setAttribute('text-anchor', 'middle');
1009
+ label.setAttribute('fill', '#888');
1010
+ label.setAttribute('font-size', '11');
1011
+ // Format date as MM/DD
1012
+ const parts = date.split('-');
1013
+ label.textContent = parts[1] + '/' + parts[2];
1014
+ xAxisG.appendChild(label);
1015
+ }
1016
+
1017
+ // Y axis
1018
+ const yAxisG = document.createElementNS('http://www.w3.org/2000/svg', 'g');
1019
+ yAxisG.setAttribute('class', 'axis');
1020
+ g.appendChild(yAxisG);
1021
+
1022
+ // Y axis line
1023
+ const yLine = document.createElementNS('http://www.w3.org/2000/svg', 'line');
1024
+ yLine.setAttribute('y1', 0);
1025
+ yLine.setAttribute('y2', height);
1026
+ yLine.setAttribute('stroke', '#444');
1027
+ yAxisG.appendChild(yLine);
1028
+
1029
+ // Y axis ticks (5 ticks)
1030
+ const yTicks = 5;
1031
+ for (let i = 0; i <= yTicks; i++) {
1032
+ const tokens = (maxDayTokens / yTicks) * i;
1033
+ const y = yScale(tokens);
1034
+
1035
+ const tick = document.createElementNS('http://www.w3.org/2000/svg', 'line');
1036
+ tick.setAttribute('x1', -6);
1037
+ tick.setAttribute('x2', 0);
1038
+ tick.setAttribute('y1', y);
1039
+ tick.setAttribute('y2', y);
1040
+ tick.setAttribute('stroke', '#444');
1041
+ yAxisG.appendChild(tick);
1042
+
1043
+ // Grid line
1044
+ const grid = document.createElementNS('http://www.w3.org/2000/svg', 'line');
1045
+ grid.setAttribute('x1', 0);
1046
+ grid.setAttribute('x2', width);
1047
+ grid.setAttribute('y1', y);
1048
+ grid.setAttribute('y2', y);
1049
+ grid.setAttribute('stroke', '#333');
1050
+ grid.setAttribute('stroke-dasharray', '2,2');
1051
+ yAxisG.appendChild(grid);
1052
+
1053
+ const label = document.createElementNS('http://www.w3.org/2000/svg', 'text');
1054
+ label.setAttribute('x', -10);
1055
+ label.setAttribute('y', y + 4);
1056
+ label.setAttribute('text-anchor', 'end');
1057
+ label.setAttribute('fill', '#888');
1058
+ label.setAttribute('font-size', '11');
1059
+ label.textContent = formatTokens(Math.round(tokens));
1060
+ yAxisG.appendChild(label);
1061
+ }
1062
+
1063
+ // Y axis label
1064
+ const yLabel = document.createElementNS('http://www.w3.org/2000/svg', 'text');
1065
+ yLabel.setAttribute('transform', `rotate(-90)`);
1066
+ yLabel.setAttribute('x', -height / 2);
1067
+ yLabel.setAttribute('y', -50);
1068
+ yLabel.setAttribute('text-anchor', 'middle');
1069
+ yLabel.setAttribute('fill', '#888');
1070
+ yLabel.setAttribute('font-size', '12');
1071
+ yLabel.textContent = 'Tokens';
1072
+ yAxisG.appendChild(yLabel);
1073
+
1074
+ // Bar width (leave gaps between days)
1075
+ const barWidth = Math.min(xBandWidth * 0.7, 60);
1076
+
1077
+ // Draw stacked bars for each day (by project)
1078
+ for (const day of data.days) {
1079
+ const x = xScale(day.date) - barWidth / 2;
1080
+ let yOffset = 0; // Stack from bottom
1081
+
1082
+ for (const proj of day.projects) {
1083
+ const projHeight = yHeight(proj.totalTokens);
1084
+ if (projHeight < 1) continue;
1085
+
1086
+ const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
1087
+ rect.setAttribute('class', 'bar-segment');
1088
+ rect.setAttribute('x', x);
1089
+ rect.setAttribute('y', height - yOffset - projHeight);
1090
+ rect.setAttribute('width', barWidth);
1091
+ rect.setAttribute('height', projHeight);
1092
+ rect.setAttribute('fill', getProjectColor(proj.project));
1093
+ rect.setAttribute('data-project', proj.project);
1094
+
1095
+ // Store project data for click/hover
1096
+ rect._projectData = {
1097
+ project: proj.project,
1098
+ totalTokens: proj.totalTokens,
1099
+ date: day.date
1100
+ };
1101
+
1102
+ // Hover events
1103
+ rect.addEventListener('mouseenter', (e) => showBarTooltip(e, rect._projectData));
1104
+ rect.addEventListener('mousemove', (e) => moveTooltip(e));
1105
+ rect.addEventListener('mouseleave', hideTooltip);
1106
+
1107
+ // Click to show detail
1108
+ rect.addEventListener('click', (e) => {
1109
+ e.stopPropagation();
1110
+ // Clear previous selection
1111
+ if (selectedBarSegment) {
1112
+ selectedBarSegment.classList.remove('selected');
1113
+ }
1114
+ selectedBarSegment = rect;
1115
+ rect.classList.add('selected');
1116
+ showBarDetail(rect._projectData);
1117
+ });
1118
+
1119
+ g.appendChild(rect);
1120
+ yOffset += projHeight;
1121
+ }
1122
+ }
1123
+
1124
+ // Click elsewhere to clear selection
1125
+ svg.addEventListener('click', () => {
1126
+ if (selectedBarSegment) {
1127
+ selectedBarSegment.classList.remove('selected');
1128
+ selectedBarSegment = null;
1129
+ detailPanel.className = 'detail-panel empty';
1130
+ detailPanel.textContent = 'Click a bar segment to see details';
1131
+ }
1132
+ });
1133
+
1134
+ // Update stats for bar chart view
1135
+ let totalTokens = 0;
1136
+ for (const day of data.days) {
1137
+ for (const proj of day.projects) {
1138
+ totalTokens += proj.totalTokens;
1139
+ }
1140
+ }
1141
+ stats.textContent = 'Total: ' + formatTokens(totalTokens) + ' tokens';
1142
+
1143
+ // Update legend with project colors
1144
+ const legendItems = [...projectColorMap.entries()].map(([project, color]) => ({
1145
+ color,
1146
+ label: project
1147
+ }));
1148
+ renderLegend(legendItems);
1149
+ }
1150
+
1151
+ /**
1152
+ * Show tooltip for bar chart segment.
1153
+ */
1154
+ function showBarTooltip(e, data) {
1155
+ tooltip.replaceChildren();
1156
+
1157
+ const title = document.createElement('div');
1158
+ title.className = 'tooltip-title';
1159
+ title.textContent = data.project;
1160
+ tooltip.appendChild(title);
1161
+
1162
+ function addRow(label, value) {
1163
+ const row = document.createElement('div');
1164
+ row.className = 'tooltip-row';
1165
+ const labelEl = document.createElement('span');
1166
+ labelEl.className = 'tooltip-label';
1167
+ labelEl.textContent = label;
1168
+ const valueEl = document.createElement('span');
1169
+ valueEl.className = 'tooltip-value';
1170
+ valueEl.textContent = value;
1171
+ row.appendChild(labelEl);
1172
+ row.appendChild(valueEl);
1173
+ tooltip.appendChild(row);
1174
+ }
1175
+
1176
+ addRow('Date:', data.date);
1177
+ addRow('Tokens:', formatTokens(data.totalTokens));
1178
+
1179
+ tooltip.style.display = 'block';
1180
+ moveTooltip(e);
1181
+ }
1182
+
1183
+ /**
1184
+ * Show detail panel for bar chart segment.
1185
+ */
1186
+ function showBarDetail(data) {
1187
+ detailPanel.className = 'detail-panel';
1188
+ detailPanel.replaceChildren();
1189
+
1190
+ const title = document.createElement('div');
1191
+ title.className = 'detail-title';
1192
+ title.textContent = data.project;
1193
+ detailPanel.appendChild(title);
1194
+
1195
+ function addDetailRow(label, value) {
1196
+ const row = document.createElement('div');
1197
+ row.className = 'detail-row';
1198
+ const labelEl = document.createElement('span');
1199
+ labelEl.className = 'detail-label';
1200
+ labelEl.textContent = label;
1201
+ const valueEl = document.createElement('span');
1202
+ valueEl.className = 'detail-value';
1203
+ valueEl.textContent = value;
1204
+ row.appendChild(labelEl);
1205
+ row.appendChild(valueEl);
1206
+ detailPanel.appendChild(row);
1207
+ }
1208
+
1209
+ addDetailRow('Date:', data.date);
1210
+ addDetailRow('Total tokens:', formatTokens(data.totalTokens));
1211
+ }
1212
+ </script>
1213
+ </body>
1214
+ </html>