claude-code-templates 1.8.0 → 1.8.2

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.
@@ -5,6 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Claude Code Analytics - Terminal</title>
7
7
  <script src="https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js"></script>
8
+ <script src="components/SessionTimer.js"></script>
8
9
  <style>
9
10
  * {
10
11
  margin: 0;
@@ -121,6 +122,65 @@
121
122
  margin-top: 2px;
122
123
  }
123
124
 
125
+ .token-popover, .claude-sessions-popover {
126
+ position: absolute;
127
+ top: 100%;
128
+ left: 50%;
129
+ transform: translateX(-50%);
130
+ margin-top: 8px;
131
+ padding: 12px;
132
+ background: #21262d;
133
+ border: 1px solid #30363d;
134
+ border-radius: 6px;
135
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
136
+ display: none;
137
+ z-index: 1000;
138
+ min-width: 200px;
139
+ white-space: nowrap;
140
+ pointer-events: auto;
141
+ }
142
+
143
+ .token-popover::before, .claude-sessions-popover::before {
144
+ content: '';
145
+ position: absolute;
146
+ bottom: 100%;
147
+ left: 50%;
148
+ transform: translateX(-50%);
149
+ border: 6px solid transparent;
150
+ border-bottom-color: #30363d;
151
+ }
152
+
153
+ .token-popover::after, .claude-sessions-popover::after {
154
+ content: '';
155
+ position: absolute;
156
+ bottom: 100%;
157
+ left: 50%;
158
+ transform: translateX(-50%);
159
+ border: 5px solid transparent;
160
+ border-bottom-color: #21262d;
161
+ margin-top: 1px;
162
+ }
163
+
164
+ .token-breakdown-item, .session-breakdown-item {
165
+ display: flex;
166
+ justify-content: space-between;
167
+ margin-bottom: 6px;
168
+ font-size: 0.85rem;
169
+ }
170
+
171
+ .token-breakdown-item:last-child, .session-breakdown-item:last-child {
172
+ margin-bottom: 0;
173
+ }
174
+
175
+ .token-type, .session-type {
176
+ color: #7d8590;
177
+ }
178
+
179
+ .token-value, .session-value {
180
+ color: #c9d1d9;
181
+ font-weight: 500;
182
+ }
183
+
124
184
  .chart-controls {
125
185
  display: flex;
126
186
  align-items: center;
@@ -196,6 +256,458 @@
196
256
  cursor: not-allowed;
197
257
  }
198
258
 
259
+ /* Session Timer Accordion Styles */
260
+ .session-timer-section {
261
+ margin: 15px 0;
262
+ }
263
+
264
+ .session-timer-accordion {
265
+ background: #21262d;
266
+ border: 1px solid #30363d;
267
+ border-radius: 6px;
268
+ margin-bottom: 15px;
269
+ position: relative;
270
+ }
271
+
272
+ .session-timer-header {
273
+ display: flex;
274
+ justify-content: space-between;
275
+ align-items: center;
276
+ padding: 12px 16px;
277
+ cursor: pointer;
278
+ border-bottom: 1px solid #30363d;
279
+ transition: background-color 0.2s ease;
280
+ }
281
+
282
+ .session-timer-header:hover {
283
+ background: #30363d;
284
+ }
285
+
286
+ .session-timer-title-section {
287
+ display: flex;
288
+ align-items: center;
289
+ gap: 8px;
290
+ }
291
+
292
+ .session-timer-chevron {
293
+ font-size: 0.75rem;
294
+ color: #7d8590;
295
+ transition: transform 0.2s ease;
296
+ }
297
+
298
+ .session-timer-title {
299
+ color: #d57455;
300
+ font-size: 0.9rem;
301
+ font-weight: 500;
302
+ margin: 0;
303
+ }
304
+
305
+ .session-timer-status-inline {
306
+ display: flex;
307
+ align-items: center;
308
+ gap: 6px;
309
+ font-size: 0.75rem;
310
+ }
311
+
312
+ .session-timer-status-dot {
313
+ width: 6px;
314
+ height: 6px;
315
+ border-radius: 50%;
316
+ animation: pulse 2s infinite;
317
+ }
318
+
319
+ .session-timer-status-dot.active {
320
+ background: #3fb950;
321
+ }
322
+
323
+ .session-timer-status-dot.warning {
324
+ background: #f97316;
325
+ }
326
+
327
+ .session-timer-status-dot.inactive {
328
+ background: #7d8590;
329
+ }
330
+
331
+ .session-timer-status-text {
332
+ color: #c9d1d9;
333
+ }
334
+
335
+ .session-timer-content {
336
+ padding: 12px 16px;
337
+ border-top: none;
338
+ }
339
+
340
+ .session-loading-state {
341
+ display: flex;
342
+ align-items: center;
343
+ justify-content: center;
344
+ gap: 8px;
345
+ padding: 20px 0;
346
+ color: #7d8590;
347
+ }
348
+
349
+ .session-spinner {
350
+ width: 16px;
351
+ height: 16px;
352
+ border: 2px solid #30363d;
353
+ border-top: 2px solid #d57455;
354
+ border-radius: 50%;
355
+ animation: spin 1s linear infinite;
356
+ }
357
+
358
+ @keyframes spin {
359
+ 0% { transform: rotate(0deg); }
360
+ 100% { transform: rotate(360deg); }
361
+ }
362
+
363
+ .session-timer-empty {
364
+ text-align: center;
365
+ padding: 20px;
366
+ }
367
+
368
+ .session-timer-empty-text {
369
+ color: #c9d1d9;
370
+ font-size: 0.9rem;
371
+ margin-bottom: 4px;
372
+ }
373
+
374
+ .session-timer-empty-subtext {
375
+ color: #7d8590;
376
+ font-size: 0.8rem;
377
+ }
378
+
379
+ .session-timer-compact {
380
+ display: flex;
381
+ flex-direction: column;
382
+ gap: 12px;
383
+ }
384
+
385
+ .session-timer-row {
386
+ display: flex;
387
+ gap: 20px;
388
+ align-items: center;
389
+ }
390
+
391
+ .session-timer-time-compact {
392
+ flex: 0 0 auto;
393
+ text-align: center;
394
+ }
395
+
396
+ .session-timer-time-value {
397
+ font-size: 1.5rem;
398
+ font-weight: 700;
399
+ color: #d57455;
400
+ line-height: 1;
401
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
402
+ }
403
+
404
+ .session-timer-time-label {
405
+ color: #7d8590;
406
+ font-size: 0.75rem;
407
+ margin-top: 2px;
408
+ }
409
+
410
+ .session-timer-progress-compact {
411
+ flex: 1;
412
+ display: flex;
413
+ flex-direction: column;
414
+ gap: 8px;
415
+ }
416
+
417
+ .session-timer-progress-item {
418
+ display: flex;
419
+ flex-direction: column;
420
+ gap: 4px;
421
+ }
422
+
423
+ .session-timer-progress-header {
424
+ display: flex;
425
+ justify-content: space-between;
426
+ align-items: center;
427
+ }
428
+
429
+ .session-timer-progress-label {
430
+ color: #c9d1d9;
431
+ font-size: 0.75rem;
432
+ font-weight: 500;
433
+ }
434
+
435
+ .session-timer-progress-value {
436
+ color: #7d8590;
437
+ font-size: 0.75rem;
438
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
439
+ display: inline-flex;
440
+ align-items: center;
441
+ }
442
+
443
+ .session-timer-progress-bar {
444
+ width: 100%;
445
+ height: 4px;
446
+ background: #30363d;
447
+ border-radius: 2px;
448
+ overflow: hidden;
449
+ position: relative;
450
+ }
451
+
452
+ .session-timer-progress-fill {
453
+ height: 100%;
454
+ background: #3fb950;
455
+ transition: width 0.3s ease, background-color 0.3s ease;
456
+ border-radius: 2px;
457
+ }
458
+
459
+ .session-timer-stats-row {
460
+ display: flex;
461
+ gap: 12px;
462
+ padding-top: 8px;
463
+ border-top: 1px solid #30363d;
464
+ flex-wrap: wrap;
465
+ }
466
+
467
+ .session-timer-stat-compact {
468
+ display: flex;
469
+ align-items: center;
470
+ gap: 4px;
471
+ flex: 1;
472
+ min-width: 0;
473
+ }
474
+
475
+ .session-timer-stat-label {
476
+ color: #7d8590;
477
+ font-size: 0.75rem;
478
+ white-space: nowrap;
479
+ }
480
+
481
+ .session-timer-stat-value {
482
+ color: #c9d1d9;
483
+ font-weight: 500;
484
+ font-size: 0.75rem;
485
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
486
+ white-space: nowrap;
487
+ overflow: hidden;
488
+ text-overflow: ellipsis;
489
+ }
490
+
491
+ .session-timer-error {
492
+ text-align: center;
493
+ padding: 20px;
494
+ color: #ffa198;
495
+ }
496
+
497
+ .session-timer-error-text {
498
+ margin-bottom: 12px;
499
+ font-size: 0.85rem;
500
+ }
501
+
502
+ .session-timer-retry-btn {
503
+ background: #f85149;
504
+ border: 1px solid #f85149;
505
+ color: #ffffff;
506
+ padding: 6px 12px;
507
+ border-radius: 4px;
508
+ cursor: pointer;
509
+ font-size: 0.75rem;
510
+ transition: all 0.2s ease;
511
+ }
512
+
513
+ .session-timer-retry-btn:hover {
514
+ background: #da3633;
515
+ border-color: #da3633;
516
+ }
517
+
518
+ .session-warnings {
519
+ margin-top: 8px;
520
+ }
521
+
522
+ .warnings-list {
523
+ display: flex;
524
+ flex-direction: column;
525
+ gap: 4px;
526
+ }
527
+
528
+ .session-warning {
529
+ background: rgba(248, 81, 73, 0.1);
530
+ border: 1px solid rgba(248, 81, 73, 0.3);
531
+ border-radius: 4px;
532
+ padding: 8px;
533
+ color: #ffa198;
534
+ font-size: 0.75rem;
535
+ display: flex;
536
+ align-items: center;
537
+ gap: 6px;
538
+ }
539
+
540
+ .session-warning.error {
541
+ background: rgba(248, 81, 73, 0.15);
542
+ border-color: rgba(248, 81, 73, 0.5);
543
+ }
544
+
545
+ .session-warning.warning {
546
+ background: rgba(249, 115, 22, 0.1);
547
+ border-color: rgba(249, 115, 22, 0.3);
548
+ color: #fbbf24;
549
+ }
550
+
551
+ .session-warning.info {
552
+ background: rgba(59, 130, 246, 0.1);
553
+ border-color: rgba(59, 130, 246, 0.3);
554
+ color: #93c5fd;
555
+ }
556
+
557
+ /* Session Timer Info Icon and Popover */
558
+ .session-timer-label-with-info {
559
+ display: inline-flex;
560
+ align-items: center;
561
+ gap: 4px;
562
+ }
563
+
564
+ .session-timer-info-icon {
565
+ color: #7d8590;
566
+ font-size: 0.7rem;
567
+ cursor: pointer;
568
+ transition: all 0.2s ease;
569
+ display: inline-flex;
570
+ align-items: center;
571
+ margin-left: 4px;
572
+ opacity: 0.6;
573
+ line-height: 1;
574
+ vertical-align: middle;
575
+ padding: 2px;
576
+ border-radius: 2px;
577
+ }
578
+
579
+ .session-timer-info-icon:hover {
580
+ color: #58a6ff;
581
+ opacity: 1;
582
+ background-color: rgba(88, 166, 255, 0.1);
583
+ }
584
+
585
+ .session-timer-info-icon:active {
586
+ background-color: rgba(88, 166, 255, 0.2);
587
+ }
588
+
589
+ .session-timer-tooltip {
590
+ position: absolute;
591
+ z-index: 1000;
592
+ background: #161b22;
593
+ border: 1px solid #30363d;
594
+ border-radius: 6px;
595
+ padding: 0;
596
+ box-shadow: 0 16px 32px rgba(1, 4, 9, 0.85);
597
+ max-width: 300px;
598
+ font-size: 0.75rem;
599
+ line-height: 1.4;
600
+ }
601
+
602
+ .session-timer-tooltip-content {
603
+ padding: 12px;
604
+ }
605
+
606
+ .session-timer-tooltip-content h4 {
607
+ margin: 0 0 8px 0;
608
+ color: #f0f6fc;
609
+ font-size: 0.8rem;
610
+ font-weight: 600;
611
+ }
612
+
613
+ .session-timer-tooltip-content p {
614
+ margin: 0 0 8px 0;
615
+ color: #c9d1d9;
616
+ line-height: 1.5;
617
+ }
618
+
619
+ .session-timer-tooltip-content p:last-of-type {
620
+ margin-bottom: 12px;
621
+ }
622
+
623
+ .session-timer-tooltip-link {
624
+ border-top: 1px solid #30363d;
625
+ padding-top: 8px;
626
+ }
627
+
628
+ .session-timer-tooltip-link a {
629
+ color: #58a6ff;
630
+ text-decoration: none;
631
+ display: inline-flex;
632
+ align-items: center;
633
+ gap: 4px;
634
+ transition: color 0.2s ease;
635
+ }
636
+
637
+ .session-timer-tooltip-link a:hover {
638
+ color: #79c0ff;
639
+ }
640
+
641
+ .session-timer-tooltip-link i {
642
+ font-size: 0.65rem;
643
+ }
644
+
645
+ /* Claude Session Info Styles */
646
+ .claude-session-info {
647
+ background: #161b22;
648
+ border: 1px solid #30363d;
649
+ border-radius: 6px;
650
+ padding: 12px;
651
+ margin: 12px 0;
652
+ }
653
+
654
+ .claude-session-info-row {
655
+ margin-bottom: 12px;
656
+ }
657
+
658
+ .claude-session-info-compact {
659
+ padding: 8px 12px;
660
+ background: #0d1117;
661
+ border: 1px solid #21262d;
662
+ border-radius: 4px;
663
+ }
664
+
665
+ .claude-session-header {
666
+ color: #d57455;
667
+ font-size: 0.8rem;
668
+ margin-bottom: 8px;
669
+ text-transform: uppercase;
670
+ letter-spacing: 0.5px;
671
+ }
672
+
673
+ .claude-session-details {
674
+ display: flex;
675
+ flex-direction: column;
676
+ gap: 4px;
677
+ }
678
+
679
+ .claude-session-stats {
680
+ display: flex;
681
+ gap: 12px;
682
+ flex-wrap: wrap;
683
+ }
684
+
685
+ .claude-session-stat {
686
+ display: flex;
687
+ align-items: center;
688
+ gap: 4px;
689
+ }
690
+
691
+ .claude-session-label {
692
+ color: #7d8590;
693
+ font-size: 0.75rem;
694
+ }
695
+
696
+ .claude-session-value {
697
+ color: #c9d1d9;
698
+ font-size: 0.75rem;
699
+ font-weight: 600;
700
+ }
701
+
702
+ .claude-session-value.expired {
703
+ color: #f85149;
704
+ }
705
+
706
+ .session-timer-status-dot.expired {
707
+ background: #f85149;
708
+ animation: pulse 2s infinite;
709
+ }
710
+
199
711
  .charts-container {
200
712
  display: grid;
201
713
  grid-template-columns: 2fr 1fr;
@@ -229,6 +741,7 @@
229
741
  .filter-bar {
230
742
  display: flex;
231
743
  align-items: center;
744
+ justify-content: space-between;
232
745
  gap: 16px;
233
746
  margin: 20px 0;
234
747
  padding: 12px 0;
@@ -236,6 +749,12 @@
236
749
  border-bottom: 1px solid #21262d;
237
750
  }
238
751
 
752
+ .filter-section {
753
+ display: flex;
754
+ align-items: center;
755
+ gap: 16px;
756
+ }
757
+
239
758
  .filter-label {
240
759
  color: #7d8590;
241
760
  font-size: 0.875rem;
@@ -246,6 +765,11 @@
246
765
  gap: 8px;
247
766
  }
248
767
 
768
+ .filter-actions {
769
+ display: flex;
770
+ align-items: center;
771
+ }
772
+
249
773
  .filter-btn {
250
774
  background: none;
251
775
  border: 1px solid #30363d;
@@ -272,6 +796,7 @@
272
796
  .sessions-table {
273
797
  width: 100%;
274
798
  border-collapse: collapse;
799
+ table-layout: fixed;
275
800
  }
276
801
 
277
802
  .sessions-table th {
@@ -283,10 +808,34 @@
283
808
  border-bottom: 1px solid #30363d;
284
809
  }
285
810
 
811
+ .sessions-table th:nth-child(1) { width: 20%; } /* conversation id */
812
+ .sessions-table th:nth-child(2) { width: 15%; } /* project */
813
+ .sessions-table th:nth-child(3) { width: 10%; } /* model */
814
+ .sessions-table th:nth-child(4) { width: 8%; } /* messages */
815
+ .sessions-table th:nth-child(5) { width: 10%; } /* tokens */
816
+ .sessions-table th:nth-child(6) { width: 12%; } /* last activity */
817
+ .sessions-table th:nth-child(7) { width: 15%; } /* conversation state - FIXED WIDTH */
818
+ .sessions-table th:nth-child(8) { width: 10%; } /* status */
819
+
286
820
  .sessions-table td {
287
821
  padding: 8px 12px;
288
822
  font-size: 0.875rem;
289
823
  border-bottom: 1px solid #21262d;
824
+ overflow: hidden;
825
+ text-overflow: ellipsis;
826
+ white-space: nowrap;
827
+ }
828
+
829
+ /* Allow status squares to wrap and flex */
830
+ .sessions-table td:nth-child(8) {
831
+ white-space: normal;
832
+ overflow: visible;
833
+ }
834
+
835
+ /* Allow conversation id to wrap for long IDs */
836
+ .sessions-table td:nth-child(1) {
837
+ white-space: normal;
838
+ overflow: visible;
290
839
  }
291
840
 
292
841
  .sessions-table tr:hover {
@@ -621,6 +1170,20 @@
621
1170
  position: relative;
622
1171
  }
623
1172
 
1173
+ .message-metadata {
1174
+ margin: 8px 0;
1175
+ padding: 4px 8px;
1176
+ background: #161b22;
1177
+ border-radius: 4px;
1178
+ border-left: 2px solid #30363d;
1179
+ }
1180
+
1181
+ .message-id {
1182
+ margin-top: 8px;
1183
+ text-align: right;
1184
+ opacity: 0.7;
1185
+ }
1186
+
624
1187
  .message:last-child {
625
1188
  border-bottom: none;
626
1189
  }
@@ -772,7 +1335,7 @@
772
1335
  <div class="terminal-header">
773
1336
  <div class="terminal-title">
774
1337
  <span class="status-dot"></span>
775
- claude-code-analytics
1338
+ claude-code-analytics <span id="app-version">1.8.0</span>
776
1339
  </div>
777
1340
  <div class="terminal-subtitle">real-time monitoring dashboard</div>
778
1341
  <div class="terminal-subtitle" id="lastUpdate"></div>
@@ -797,14 +1360,49 @@
797
1360
  <span class="stat-label">conversations:</span>
798
1361
  <span class="stat-value" id="totalConversations">0</span>
799
1362
  </div>
800
- <div class="stat">
801
- <span class="stat-label">claude sessions:</span>
802
- <span class="stat-value" id="claudeSessions">0</span>
803
- <span class="stat-sublabel" id="claudeSessionsDetail"></span>
1363
+ <div class="stat" style="position: relative;">
1364
+ <span class="stat-label">sessions:</span>
1365
+ <span class="stat-value" id="claudeSessions"
1366
+ onmouseenter="showClaudeSessionsPopover(event)"
1367
+ onmouseleave="hideClaudeSessionsPopover()"
1368
+ onclick="showClaudeSessionsPopover(event)"
1369
+ style="cursor: help;">0</span>
1370
+ <div class="claude-sessions-popover" id="claudeSessionsPopover">
1371
+ <div class="session-breakdown-item">
1372
+ <span class="session-type">This Month:</span>
1373
+ <span class="session-value" id="claudeSessionsMonth">0</span>
1374
+ </div>
1375
+ <div class="session-breakdown-item">
1376
+ <span class="session-type">This Week:</span>
1377
+ <span class="session-value" id="claudeSessionsWeek">0</span>
1378
+ </div>
1379
+ </div>
804
1380
  </div>
805
- <div class="stat">
1381
+ <div class="stat" style="position: relative;">
806
1382
  <span class="stat-label">tokens:</span>
807
- <span class="stat-value" id="totalTokens">0</span>
1383
+ <span class="stat-value" id="totalTokens"
1384
+ onmouseenter="showTokenPopover(event)"
1385
+ onmouseleave="hideTokenPopover()"
1386
+ onclick="showTokenPopover(event)"
1387
+ style="cursor: help;">0</span>
1388
+ <div class="token-popover" id="tokenPopover">
1389
+ <div class="token-breakdown-item">
1390
+ <span class="token-type">Input:</span>
1391
+ <span class="token-value" id="inputTokens">0</span>
1392
+ </div>
1393
+ <div class="token-breakdown-item">
1394
+ <span class="token-type">Output:</span>
1395
+ <span class="token-value" id="outputTokens">0</span>
1396
+ </div>
1397
+ <div class="token-breakdown-item">
1398
+ <span class="token-type">Cache Creation:</span>
1399
+ <span class="token-value" id="cacheCreationTokens">0</span>
1400
+ </div>
1401
+ <div class="token-breakdown-item">
1402
+ <span class="token-type">Cache Read:</span>
1403
+ <span class="token-value" id="cacheReadTokens">0</span>
1404
+ </div>
1405
+ </div>
808
1406
  </div>
809
1407
  <div class="stat">
810
1408
  <span class="stat-label">projects:</span>
@@ -816,6 +1414,13 @@
816
1414
  </div>
817
1415
  </div>
818
1416
 
1417
+ <!-- Session Timer Section -->
1418
+ <div class="session-timer-section">
1419
+ <div id="session-timer-container">
1420
+ <!-- SessionTimer component will be mounted here -->
1421
+ </div>
1422
+ </div>
1423
+
819
1424
  <div class="chart-controls">
820
1425
  <div class="chart-controls-left">
821
1426
  <div class="date-control">
@@ -828,9 +1433,6 @@
828
1433
  </div>
829
1434
  </div>
830
1435
  <div class="chart-controls-right">
831
- <button class="refresh-btn" onclick="toggleNotifications()" id="notificationBtn">
832
- enable notifications
833
- </button>
834
1436
  <button class="refresh-btn" onclick="refreshCharts()" id="refreshBtn">
835
1437
  refresh charts
836
1438
  </button>
@@ -854,12 +1456,19 @@
854
1456
  </div>
855
1457
 
856
1458
  <div class="filter-bar">
857
- <span class="filter-label">filter conversations:</span>
858
- <div class="filter-buttons">
859
- <button class="filter-btn active" data-filter="active">active</button>
860
- <button class="filter-btn" data-filter="recent">recent</button>
861
- <button class="filter-btn" data-filter="inactive">inactive</button>
862
- <button class="filter-btn" data-filter="all">all</button>
1459
+ <div class="filter-section">
1460
+ <span class="filter-label">filter conversations:</span>
1461
+ <div class="filter-buttons">
1462
+ <button class="filter-btn active" data-filter="active">active</button>
1463
+ <button class="filter-btn" data-filter="recent">recent</button>
1464
+ <button class="filter-btn" data-filter="inactive">inactive</button>
1465
+ <button class="filter-btn" data-filter="all">all</button>
1466
+ </div>
1467
+ </div>
1468
+ <div class="filter-actions">
1469
+ <button class="refresh-btn" onclick="toggleNotifications()" id="notificationBtn">
1470
+ enable notifications
1471
+ </button>
863
1472
  </div>
864
1473
  </div>
865
1474
 
@@ -924,6 +1533,22 @@
924
1533
  let allData = null;
925
1534
  let notificationsEnabled = false;
926
1535
  let previousConversationStates = new Map();
1536
+ let sessionTimerComponent = null;
1537
+
1538
+ async function loadVersion() {
1539
+ try {
1540
+ const response = await fetch('/api/version');
1541
+ const data = await response.json();
1542
+
1543
+ const versionElement = document.getElementById('app-version');
1544
+ if (versionElement) {
1545
+ versionElement.textContent = data.version;
1546
+ }
1547
+ } catch (error) {
1548
+ console.error('Error loading version:', error);
1549
+ // Keep fallback version if API fails
1550
+ }
1551
+ }
927
1552
 
928
1553
  async function loadData() {
929
1554
  try {
@@ -938,7 +1563,7 @@
938
1563
  // Update timestamp
939
1564
  document.getElementById('lastUpdate').textContent = `last update: ${data.lastUpdate}`;
940
1565
 
941
- updateStats(data.summary);
1566
+ updateStats(data.summary, data.detailedTokenUsage, data);
942
1567
  allConversations = data.conversations;
943
1568
  allData = data; // Store data globally for access
944
1569
  window.allData = data; // Keep for backward compatibility
@@ -951,6 +1576,9 @@
951
1576
  updateCharts(data);
952
1577
  updateSessionsTable();
953
1578
 
1579
+ // Initialize or update SessionTimer
1580
+ await initializeSessionTimer();
1581
+
954
1582
  // Check for conversation state changes and send notifications
955
1583
  checkForNotifications(data.conversations);
956
1584
 
@@ -964,15 +1592,32 @@
964
1592
  // Function to only update conversation data without refreshing charts
965
1593
  async function loadConversationData() {
966
1594
  try {
967
- const response = await fetch('/api/data');
1595
+ const response = await fetch('/api/fast-update');
968
1596
  const data = await response.json();
969
1597
 
970
- console.log('Conversation data refreshed:', data.timestamp);
1598
+ // Only log state changes, not every refresh
1599
+ let hasStateChanges = false;
1600
+
1601
+ // Log conversation state changes
1602
+ if (data.conversations && allConversations) {
1603
+ data.conversations.forEach(conv => {
1604
+ const prevConv = allConversations.find(c => c.id === conv.id);
1605
+ if (prevConv && prevConv.conversationState !== conv.conversationState) {
1606
+ console.log('🔄 State change: ' + conv.project + ' from "' + prevConv.conversationState + '" to "' + conv.conversationState + '"');
1607
+ hasStateChanges = true;
1608
+ }
1609
+ });
1610
+ }
1611
+
1612
+ // Only log refresh timestamp if there were actual changes
1613
+ if (hasStateChanges) {
1614
+ console.log('⚡ Update completed at:', new Date().toLocaleTimeString());
1615
+ }
971
1616
 
972
1617
  // Update timestamp
973
1618
  document.getElementById('lastUpdate').textContent = `last update: ${data.lastUpdate}`;
974
1619
 
975
- updateStats(data.summary);
1620
+ updateStats(data.summary, data.detailedTokenUsage, data);
976
1621
  allConversations = data.conversations;
977
1622
  allData = data; // Store data globally for access
978
1623
  window.allData = data; // Keep for backward compatibility
@@ -988,6 +1633,40 @@
988
1633
  }
989
1634
  }
990
1635
 
1636
+ // NEW: Function to update ONLY conversation states (ultra-fast)
1637
+ async function updateConversationStatesOnly() {
1638
+ try {
1639
+ const response = await fetch('/api/conversation-state');
1640
+ const data = await response.json();
1641
+
1642
+ // Update only the conversation state fields in the UI
1643
+ data.activeStates.forEach(stateInfo => {
1644
+ // Update in sessions table
1645
+ const sessionRow = document.querySelector('tr[data-session-id="' + stateInfo.id + '"]');
1646
+ if (sessionRow) {
1647
+ const stateCell = sessionRow.querySelector('.conversation-state');
1648
+ if (stateCell && stateCell.textContent !== stateInfo.state) {
1649
+ console.log('⚡ INSTANT State Update: ' + stateInfo.project + ' → "' + stateInfo.state + '"');
1650
+ stateCell.textContent = stateInfo.state;
1651
+ stateCell.className = 'conversation-state ' + getStateClass(stateInfo.state);
1652
+ }
1653
+ }
1654
+
1655
+ // Update in session detail if visible
1656
+ if (currentSession && currentSession.id === stateInfo.id) {
1657
+ const detailStateElement = document.querySelector('#sessionDetail .conversation-state');
1658
+ if (detailStateElement && detailStateElement.textContent !== stateInfo.state) {
1659
+ detailStateElement.textContent = stateInfo.state;
1660
+ detailStateElement.className = 'conversation-state ' + getStateClass(stateInfo.state);
1661
+ }
1662
+ }
1663
+ });
1664
+
1665
+ } catch (error) {
1666
+ // Silently fail - don't interfere with main data flow
1667
+ }
1668
+ }
1669
+
991
1670
  // Notification functions
992
1671
  async function requestNotificationPermission() {
993
1672
  if (!('Notification' in window)) {
@@ -1044,16 +1723,13 @@
1044
1723
 
1045
1724
  // Check if conversation state changed to "Awaiting user input..."
1046
1725
  if (prevState && prevState !== currentState) {
1047
- console.log(`State change detected: ${conv.project} from "${prevState}" to "${currentState}"`);
1048
-
1049
1726
  if (currentState === 'Awaiting user input...' ||
1050
1727
  currentState === 'User may be typing...' ||
1051
1728
  currentState === 'Awaiting response...') {
1052
1729
 
1053
- const title = '🤖 Claude is waiting for you!';
1730
+ const title = 'Claude is waiting for you!';
1054
1731
  const body = `Project: ${conv.project} - Claude needs your input`;
1055
1732
 
1056
- console.log('Sending notification for state:', currentState);
1057
1733
  sendNotification(title, body, conv.id);
1058
1734
  }
1059
1735
  }
@@ -1093,20 +1769,94 @@
1093
1769
  }
1094
1770
  }
1095
1771
 
1096
- function updateStats(summary) {
1772
+ function updateStats(summary, detailedTokenUsage, data = null) {
1097
1773
  document.getElementById('totalConversations').textContent = summary.totalConversations.toLocaleString();
1098
- document.getElementById('totalTokens').textContent = summary.totalTokens.toLocaleString();
1774
+
1775
+ // Use detailed token usage for accurate totals
1776
+ const totalTokens = detailedTokenUsage ? detailedTokenUsage.total : summary.totalTokens;
1777
+ document.getElementById('totalTokens').textContent = totalTokens.toLocaleString();
1778
+
1779
+ // Update detailed token breakdown
1780
+ if (detailedTokenUsage) {
1781
+ document.getElementById('inputTokens').textContent = detailedTokenUsage.inputTokens.toLocaleString();
1782
+ document.getElementById('outputTokens').textContent = detailedTokenUsage.outputTokens.toLocaleString();
1783
+ document.getElementById('cacheCreationTokens').textContent = detailedTokenUsage.cacheCreationTokens.toLocaleString();
1784
+ document.getElementById('cacheReadTokens').textContent = detailedTokenUsage.cacheReadTokens.toLocaleString();
1785
+ }
1786
+
1099
1787
  document.getElementById('activeProjects').textContent = summary.activeProjects;
1100
1788
  document.getElementById('dataSize').textContent = summary.totalFileSize;
1101
1789
 
1102
1790
  // Update Claude sessions
1103
1791
  if (summary.claudeSessions) {
1104
1792
  document.getElementById('claudeSessions').textContent = summary.claudeSessions.total.toLocaleString();
1105
- document.getElementById('claudeSessionsDetail').textContent =
1106
- `this month: ${summary.claudeSessions.currentMonth} this week: ${summary.claudeSessions.thisWeek}`;
1793
+
1794
+ // Update Claude sessions popover
1795
+ document.getElementById('claudeSessionsMonth').textContent = summary.claudeSessions.currentMonth;
1796
+ document.getElementById('claudeSessionsWeek').textContent = summary.claudeSessions.thisWeek;
1107
1797
  }
1108
1798
  }
1109
1799
 
1800
+ let popoverTimeout;
1801
+
1802
+ function showTokenPopover(event) {
1803
+ clearTimeout(popoverTimeout);
1804
+ const popover = document.getElementById('tokenPopover');
1805
+ if (popover) {
1806
+ popover.style.display = 'block';
1807
+ popover.style.visibility = 'visible';
1808
+ popover.style.opacity = '1';
1809
+
1810
+ // Add event listeners to popover to keep it open when hovering over it
1811
+ popover.addEventListener('mouseenter', () => {
1812
+ clearTimeout(popoverTimeout);
1813
+ });
1814
+
1815
+ popover.addEventListener('mouseleave', () => {
1816
+ hideTokenPopover();
1817
+ });
1818
+ }
1819
+ }
1820
+
1821
+ function hideTokenPopover() {
1822
+ popoverTimeout = setTimeout(() => {
1823
+ const popover = document.getElementById('tokenPopover');
1824
+ if (popover) {
1825
+ popover.style.display = 'none';
1826
+ }
1827
+ }, 100); // Small delay to allow moving to popover
1828
+ }
1829
+
1830
+ let claudeSessionsPopoverTimeout;
1831
+
1832
+ function showClaudeSessionsPopover(event) {
1833
+ clearTimeout(claudeSessionsPopoverTimeout);
1834
+ const popover = document.getElementById('claudeSessionsPopover');
1835
+ if (popover) {
1836
+ popover.style.display = 'block';
1837
+ popover.style.visibility = 'visible';
1838
+ popover.style.opacity = '1';
1839
+
1840
+ // Add event listeners to popover to keep it open when hovering over it
1841
+ popover.addEventListener('mouseenter', () => {
1842
+ clearTimeout(claudeSessionsPopoverTimeout);
1843
+ });
1844
+
1845
+ popover.addEventListener('mouseleave', () => {
1846
+ hideClaudeSessionsPopover();
1847
+ });
1848
+ }
1849
+ }
1850
+
1851
+ function hideClaudeSessionsPopover() {
1852
+ claudeSessionsPopoverTimeout = setTimeout(() => {
1853
+ const popover = document.getElementById('claudeSessionsPopover');
1854
+ if (popover) {
1855
+ popover.style.display = 'none';
1856
+ }
1857
+ }, 100); // Small delay to allow moving to popover
1858
+ }
1859
+
1110
1860
  function initializeDateInputs() {
1111
1861
  const today = new Date();
1112
1862
  const sevenDaysAgo = new Date(today);
@@ -1165,7 +1915,7 @@
1165
1915
  console.error('Error refreshing charts:', error);
1166
1916
  } finally {
1167
1917
  refreshBtn.classList.remove('loading');
1168
- refreshBtn.textContent = '🔄 refresh charts';
1918
+ refreshBtn.textContent = 'refresh charts';
1169
1919
  }
1170
1920
  }
1171
1921
 
@@ -1416,7 +2166,7 @@
1416
2166
  noSessionsDiv.style.display = 'none';
1417
2167
 
1418
2168
  tableBody.innerHTML = filteredConversations.map(conv => `
1419
- <tr onclick="showSessionDetail('${conv.id}')" style="cursor: pointer;">
2169
+ <tr data-session-id="${conv.id}" onclick="showSessionDetail('${conv.id}')" style="cursor: pointer;">
1420
2170
  <td>
1421
2171
  <div class="session-id-container">
1422
2172
  <div class="session-id">
@@ -1516,26 +2266,38 @@
1516
2266
  tooltip: 'User input'
1517
2267
  };
1518
2268
  } else if (message.role === 'assistant') {
1519
- const content = message.content || '';
2269
+ // Use the new metadata fields to determine type more accurately
1520
2270
 
1521
- if (typeof content === 'string') {
1522
- if (content.includes('[Tool:') || content.includes('tool_use')) {
1523
- return {
1524
- type: 'tool',
1525
- tooltip: 'Tool execution'
1526
- };
1527
- } else if (content.includes('error') || content.includes('Error') || content.includes('failed')) {
1528
- return {
1529
- type: 'error',
1530
- tooltip: 'Error in response'
1531
- };
1532
- } else {
1533
- return {
1534
- type: 'success',
1535
- tooltip: 'Successful response'
1536
- };
1537
- }
2271
+ // Check if message has tool use based on stop_reason or content blocks
2272
+ if (message.stop_reason === 'tool_use' || message.hasToolUse) {
2273
+ return {
2274
+ type: 'tool',
2275
+ tooltip: 'Tool execution'
2276
+ };
2277
+ }
2278
+
2279
+ // Check if message has tool results
2280
+ if (message.hasToolResult) {
2281
+ return {
2282
+ type: 'tool',
2283
+ tooltip: 'Tool result'
2284
+ };
2285
+ }
2286
+
2287
+ // Check for actual errors based on stop_reason or specific error patterns
2288
+ if (message.stop_reason === 'error' ||
2289
+ (message.content && message.content.includes('I apologize') && message.content.includes('error'))) {
2290
+ return {
2291
+ type: 'error',
2292
+ tooltip: 'Error in response'
2293
+ };
1538
2294
  }
2295
+
2296
+ // Normal successful response
2297
+ return {
2298
+ type: 'success',
2299
+ tooltip: 'Successful response'
2300
+ };
1539
2301
  }
1540
2302
 
1541
2303
  return {
@@ -1686,6 +2448,24 @@
1686
2448
  container.innerHTML = reversedMessages.map((message, index) => {
1687
2449
  const messageType = getMessageType(message);
1688
2450
  const messageNum = filteredMessages.length - index;
2451
+
2452
+ // Build additional info display
2453
+ const additionalInfo = [];
2454
+ if (message.model) additionalInfo.push(`Model: ${message.model}`);
2455
+ if (message.stop_reason) additionalInfo.push(`Stop: ${message.stop_reason}`);
2456
+ if (message.usage) {
2457
+ const usage = message.usage;
2458
+ if (usage.input_tokens) additionalInfo.push(`In: ${usage.input_tokens}t`);
2459
+ if (usage.output_tokens) additionalInfo.push(`Out: ${usage.output_tokens}t`);
2460
+ if (usage.cache_creation_input_tokens) additionalInfo.push(`Cache: ${usage.cache_creation_input_tokens}t`);
2461
+ if (usage.service_tier) additionalInfo.push(`Tier: ${usage.service_tier}`);
2462
+ }
2463
+ if (message.contentBlocks && message.contentBlocks.length > 0) {
2464
+ const blockTypes = message.contentBlocks.map(b => b.type).join(', ');
2465
+ additionalInfo.push(`Blocks: ${blockTypes}`);
2466
+ }
2467
+ if (message.version) additionalInfo.push(`v${message.version}`);
2468
+
1689
2469
  return `
1690
2470
  <div class="message">
1691
2471
  <div class="message-type-indicator ${messageType.type}" data-tooltip="${messageType.tooltip}"></div>
@@ -1695,7 +2475,13 @@
1695
2475
  #${messageNum} • ${message.timestamp ? formatMessageTime(message.timestamp) : 'unknown time'}
1696
2476
  </div>
1697
2477
  </div>
2478
+ ${additionalInfo.length > 0 ? `
2479
+ <div class="message-metadata">
2480
+ <small style="color: #7d8590; font-size: 0.8rem;">${additionalInfo.join(' • ')}</small>
2481
+ </div>
2482
+ ` : ''}
1698
2483
  <div class="message-content">${truncateContent(message.content || 'no content')}</div>
2484
+ ${message.message_id ? `<div class="message-id"><small style="color: #7d8590; font-size: 0.7rem;">ID: ${message.message_id}</small></div>` : ''}
1699
2485
  </div>
1700
2486
  `;
1701
2487
  }).join('');
@@ -1836,12 +2622,18 @@
1836
2622
  function initWhenReady() {
1837
2623
  if (typeof Chart !== 'undefined') {
1838
2624
  console.log('Chart.js loaded successfully');
2625
+ loadVersion();
1839
2626
  loadData();
1840
2627
 
1841
- // Automatic refresh for conversation data every 1 second for real-time updates
2628
+ // Regular refresh for conversation data every 1 second (slower)
1842
2629
  setInterval(() => {
1843
2630
  loadConversationData();
1844
2631
  }, 1000);
2632
+
2633
+ // NEW: Ultra-fast refresh ONLY for conversation states (every 100ms)
2634
+ setInterval(() => {
2635
+ updateConversationStatesOnly();
2636
+ }, 100);
1845
2637
  } else {
1846
2638
  console.log('Waiting for Chart.js to load...');
1847
2639
  setTimeout(initWhenReady, 100);
@@ -1858,6 +2650,47 @@
1858
2650
  updateNotificationButtonState();
1859
2651
  });
1860
2652
 
2653
+ // Initialize SessionTimer component
2654
+ async function initializeSessionTimer() {
2655
+ if (!window.SessionTimer) {
2656
+ console.warn('SessionTimer component not available');
2657
+ return;
2658
+ }
2659
+
2660
+ const container = document.getElementById('session-timer-container');
2661
+ if (!container) {
2662
+ console.warn('Session timer container not found');
2663
+ return;
2664
+ }
2665
+
2666
+ // Create mock services for the SessionTimer
2667
+ const mockDataService = {
2668
+ getSessionData: async () => {
2669
+ try {
2670
+ const response = await fetch('/api/session/data');
2671
+ return await response.json();
2672
+ } catch (error) {
2673
+ console.error('Error fetching session data:', error);
2674
+ return null;
2675
+ }
2676
+ }
2677
+ };
2678
+
2679
+ const mockStateService = {
2680
+ subscribe: (callback) => {
2681
+ // Mock state service subscription
2682
+ return () => {}; // Unsubscribe function
2683
+ }
2684
+ };
2685
+
2686
+ // Initialize SessionTimer if not already initialized
2687
+ if (!sessionTimerComponent) {
2688
+ sessionTimerComponent = new SessionTimer(container, mockDataService, mockStateService);
2689
+ await sessionTimerComponent.initialize();
2690
+ console.log('✅ SessionTimer initialized successfully');
2691
+ }
2692
+ }
2693
+
1861
2694
  function updateNotificationButtonState() {
1862
2695
  const btn = document.getElementById('notificationBtn');
1863
2696
  if (!btn) return;