@zhangferry-dev/tokendash 1.5.0 → 1.6.1

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.
@@ -14,6 +14,7 @@
14
14
  --border: oklch(88% 0.008 240);
15
15
  --accent: oklch(69% 0.11 162);
16
16
  --accent-strong: oklch(61% 0.13 162);
17
+ --notice: oklch(67% 0.16 72);
17
18
  --error-bg: oklch(95% 0.02 80);
18
19
  --error-fg: oklch(45% 0.08 25);
19
20
  --shadow: 0 26px 70px rgba(20, 28, 38, 0.14), 0 8px 18px rgba(20, 28, 38, 0.08);
@@ -44,6 +45,8 @@
44
45
 
45
46
  .shell {
46
47
  position: relative;
48
+ display: flex;
49
+ flex-direction: column;
47
50
  width: 100%;
48
51
  height: 100%;
49
52
  background: rgba(255, 255, 255, 0.76);
@@ -57,16 +60,37 @@
57
60
  .toolbar {
58
61
  display: flex;
59
62
  align-items: center;
60
- justify-content: flex-start;
61
- padding: 14px 18px 12px;
63
+ justify-content: space-between;
64
+ padding: 10px 24px 9px;
62
65
  border-bottom: 1px solid rgba(225, 230, 228, 0.94);
63
66
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.84), rgba(248, 250, 249, 0.78));
64
67
  }
65
68
 
66
69
  .toolbar-label {
67
- font-size: 12px;
68
- color: var(--muted);
70
+ font-size: 15px;
71
+ line-height: 1.1;
72
+ color: var(--fg);
69
73
  margin: 0;
74
+ letter-spacing: 0;
75
+ font-weight: 500;
76
+ }
77
+
78
+ .toolbar-label strong {
79
+ font-weight: 700;
80
+ }
81
+
82
+ .cache-pill {
83
+ display: inline-flex;
84
+ align-items: center;
85
+ justify-content: center;
86
+ min-height: 22px;
87
+ padding: 0 9px;
88
+ border-radius: 8px;
89
+ border: 1px solid color-mix(in oklch, var(--accent-strong) 36%, var(--border));
90
+ background: color-mix(in oklch, var(--accent) 12%, white);
91
+ color: color-mix(in oklch, var(--accent-strong) 76%, black);
92
+ font-size: 11px;
93
+ font-weight: 600;
70
94
  }
71
95
 
72
96
  /* Error banner — shown when API is unreachable */
@@ -82,56 +106,105 @@
82
106
  .error-banner.visible { display: block; }
83
107
 
84
108
  .content {
85
- height: calc(100% - 48px);
86
- padding: 18px;
87
- display: grid;
88
- grid-template-rows: auto auto auto;
89
- gap: 14px;
109
+ flex: 1 1 auto;
110
+ min-height: 0;
111
+ padding: 0 24px 0;
112
+ display: flex;
113
+ flex-direction: column;
90
114
  background: linear-gradient(180deg, rgba(250, 252, 251, 0.85), rgba(244, 247, 245, 0.7));
91
115
  }
92
116
 
93
117
  .content.has-error {
94
- height: calc(100% - 48px - 28px);
118
+ min-height: 0;
95
119
  }
96
120
 
97
121
  .summary {
98
- display: grid;
99
- grid-template-columns: repeat(2, minmax(0, 1fr));
100
- gap: 8px;
122
+ flex: 0 0 auto;
123
+ padding: 7px 0 7px;
124
+ border-bottom: 1px solid var(--border);
101
125
  }
102
126
 
103
- .card {
104
- padding: 12px;
105
- border-radius: var(--radius-card);
106
- border: 1px solid var(--border);
107
- background: var(--surface-strong);
127
+ .summary-top {
128
+ display: grid;
129
+ grid-template-columns: minmax(0, 1fr) auto;
130
+ align-items: end;
131
+ gap: 12px;
132
+ margin-bottom: 8px;
108
133
  }
109
134
 
110
135
  .card-label {
111
136
  margin: 0 0 6px;
112
- font-size: 11px;
137
+ font-size: 12px;
113
138
  color: var(--muted);
114
- text-transform: uppercase;
115
- letter-spacing: 0.08em;
139
+ letter-spacing: 0;
116
140
  }
117
141
 
118
142
  .card-value {
119
143
  margin: 0;
120
144
  font-family: var(--font-display);
121
- font-size: 22px;
122
- line-height: 1;
145
+ font-size: 32px;
146
+ line-height: 0.95;
123
147
  letter-spacing: -0.05em;
124
148
  font-weight: 700;
125
149
  }
126
150
 
127
151
  .card-value.empty { color: var(--muted); }
128
152
 
129
- .card-meta {
130
- margin: 6px 0 0;
153
+ .cost-value {
154
+ margin: 0 0 2px;
155
+ font-family: var(--font-display);
156
+ font-size: 25px;
157
+ line-height: 1;
158
+ color: color-mix(in oklch, var(--accent-strong) 76%, black);
159
+ letter-spacing: -0.04em;
160
+ font-weight: 600;
161
+ }
162
+
163
+ .mini-metrics {
164
+ display: grid;
165
+ grid-template-columns: repeat(3, minmax(0, 1fr));
166
+ border-top: 1px solid var(--border);
167
+ padding-top: 7px;
168
+ }
169
+
170
+ .mini-metric {
171
+ min-width: 0;
172
+ padding-inline: 14px;
173
+ border-left: 1px solid var(--border);
174
+ text-align: center;
175
+ }
176
+
177
+ .mini-metric:first-child {
178
+ padding-left: 0;
179
+ border-left: 0;
180
+ text-align: left;
181
+ }
182
+
183
+ .mini-metric:last-child {
184
+ padding-right: 0;
185
+ text-align: right;
186
+ }
187
+
188
+ .mini-label {
189
+ margin: 0 0 5px;
131
190
  font-size: 11px;
132
191
  color: var(--muted);
133
192
  }
134
193
 
194
+ .mini-value {
195
+ margin: 0;
196
+ font-family: var(--font-display);
197
+ font-size: 15px;
198
+ line-height: 1;
199
+ color: var(--fg);
200
+ letter-spacing: -0.02em;
201
+ font-weight: 600;
202
+ }
203
+
204
+ .mini-value.input { color: color-mix(in oklch, var(--accent-strong) 76%, black); }
205
+ .mini-value.output { color: oklch(45% 0.13 255); }
206
+ .mini-value.cached { color: oklch(62% 0.15 75); }
207
+
135
208
  /* Shimmer skeleton for loading state */
136
209
  .skeleton {
137
210
  display: inline-block;
@@ -149,56 +222,92 @@
149
222
  }
150
223
 
151
224
  .chart-panel {
152
- padding: 16px;
153
- border-radius: var(--radius-panel);
154
- border: 1px solid var(--border);
155
- background: linear-gradient(180deg, rgba(255, 255, 255, 0.97), rgba(247, 250, 248, 0.9));
225
+ flex: 0 0 auto;
226
+ padding: 9px 0 7px;
227
+ border-radius: 0;
228
+ border: 0;
229
+ border-bottom: 1px solid var(--border);
230
+ background: transparent;
156
231
  }
157
232
 
158
233
  .chart-header {
159
- margin-bottom: 14px;
234
+ display: flex;
235
+ justify-content: flex-start;
236
+ align-items: baseline;
237
+ gap: 10px;
238
+ margin-bottom: 10px;
160
239
  }
161
240
 
162
241
  .eyebrow {
163
242
  margin: 0;
164
- font-family: var(--font-mono);
165
- font-size: 11px;
166
- color: var(--muted);
167
- text-transform: uppercase;
168
- letter-spacing: 0.12em;
243
+ font-family: var(--font-body);
244
+ font-size: 13px;
245
+ color: var(--fg);
246
+ text-transform: none;
247
+ letter-spacing: 0;
248
+ font-weight: 700;
169
249
  }
170
250
 
171
251
  .chart-frame {
172
252
  display: grid;
173
- grid-template-columns: max-content 1fr;
174
- gap: 6px;
253
+ grid-template-columns: 34px 1fr;
254
+ grid-template-rows: 88px 18px;
255
+ gap: 5px;
175
256
  align-items: stretch;
176
- min-height: 190px;
257
+ row-gap: 5px;
177
258
  }
178
259
 
179
260
  .y-axis {
180
- display: flex;
181
- flex-direction: column;
182
- justify-content: space-between;
183
- align-items: flex-end;
184
- width: max-content;
185
- padding-block: 6px 24px;
261
+ position: relative;
262
+ grid-column: 1;
263
+ grid-row: 1;
186
264
  color: var(--muted);
187
265
  font-family: var(--font-mono);
188
- font-size: 10px;
266
+ font-size: 9px;
267
+ }
268
+
269
+ .y-axis span {
270
+ position: absolute;
271
+ inset-inline-end: 0;
272
+ line-height: 1;
273
+ transform: translateY(-50%);
189
274
  }
190
275
 
191
276
  .plot {
192
277
  position: relative;
193
- border-inline-start: 1px solid var(--border);
194
- border-block-end: 1px solid var(--border);
195
- padding: 6px 12px 24px 14px;
278
+ grid-column: 2;
279
+ grid-row: 1;
280
+ border-inline-start: 0;
281
+ border-block-end: 0;
282
+ padding: 0 10px 0 12px;
196
283
  display: grid;
197
284
  align-items: end;
198
- gap: 8px;
199
- background:
200
- linear-gradient(to top, rgba(212, 218, 215, 0.6) 1px, transparent 1px) 0 0 / 100% calc((100% - 24px) / 3),
201
- transparent;
285
+ gap: 3px;
286
+ background: transparent;
287
+ }
288
+
289
+ .plot::before {
290
+ content: "";
291
+ position: absolute;
292
+ inset-block: 0;
293
+ inset-inline-start: 0;
294
+ width: 1px;
295
+ background: rgba(180, 190, 187, 0.72);
296
+ pointer-events: none;
297
+ }
298
+
299
+ .plot-grid {
300
+ position: absolute;
301
+ inset: 0;
302
+ pointer-events: none;
303
+ z-index: 0;
304
+ }
305
+
306
+ .plot-grid-line {
307
+ position: absolute;
308
+ inset-inline: 0;
309
+ height: 1px;
310
+ border-top: 1px dashed rgba(174, 186, 181, 0.58);
202
311
  }
203
312
 
204
313
  /* Empty state for chart */
@@ -210,7 +319,7 @@
210
319
  height: 100%;
211
320
  gap: 6px;
212
321
  color: var(--muted);
213
- font-size: 12px;
322
+ font-size: 10px;
214
323
  text-align: center;
215
324
  }
216
325
 
@@ -232,30 +341,62 @@
232
341
  display: flex;
233
342
  align-items: end;
234
343
  justify-content: center;
344
+ z-index: 1;
235
345
  }
236
346
 
237
347
  .bar {
238
348
  width: 100%;
239
- max-width: 22px;
240
- border-radius: 8px 8px 3px 3px;
349
+ max-width: 7px;
350
+ border-radius: 4px 4px 2px 2px;
241
351
  background: linear-gradient(180deg, color-mix(in oklch, var(--accent) 60%, white), var(--accent));
242
352
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.45);
243
353
  }
244
354
 
245
- .bar.zero { opacity: 0.35; }
355
+ .bar.zero { opacity: 0; }
246
356
 
247
357
  .bar.peak {
248
358
  background: linear-gradient(180deg, color-mix(in oklch, var(--accent) 85%, white), var(--accent-strong));
249
359
  }
250
360
 
251
361
  .bar-label {
252
- position: absolute;
253
- inset-block-end: -20px;
254
362
  font-family: var(--font-mono);
255
- font-size: 10px;
363
+ font-size: 9px;
256
364
  color: var(--muted);
257
365
  }
258
366
 
367
+ .x-axis {
368
+ grid-column: 2;
369
+ grid-row: 2;
370
+ display: grid;
371
+ align-items: start;
372
+ padding: 0 10px 0 12px;
373
+ font-family: var(--font-mono);
374
+ font-size: 9px;
375
+ color: var(--muted);
376
+ }
377
+
378
+ .x-axis-label {
379
+ justify-self: center;
380
+ position: relative;
381
+ line-height: 1;
382
+ white-space: nowrap;
383
+ }
384
+
385
+ .x-axis-label::before {
386
+ content: "";
387
+ position: absolute;
388
+ inset-block-start: -7px;
389
+ inset-inline-start: 50%;
390
+ width: 1px;
391
+ height: 4px;
392
+ background: rgba(150, 162, 158, 0.46);
393
+ transform: translateX(-50%);
394
+ }
395
+
396
+ .x-axis-label.is-unlabeled {
397
+ color: transparent;
398
+ }
399
+
259
400
  .bar-tip {
260
401
  position: absolute;
261
402
  inset-block-end: calc(var(--height) + 4px);
@@ -269,62 +410,169 @@
269
410
  white-space: nowrap;
270
411
  }
271
412
 
413
+ .usage-panel {
414
+ flex: 0 0 auto;
415
+ padding: 6px 0 3px;
416
+ overflow: visible;
417
+ border-bottom: 1px solid rgba(211, 218, 215, 0.82);
418
+ }
419
+
420
+ .usage-panel:last-of-type {
421
+ flex: 1 1 auto;
422
+ border-bottom: 0;
423
+ }
424
+
425
+ .usage-panel + .usage-panel {
426
+ border-bottom: 0;
427
+ }
428
+
429
+ .usage-title {
430
+ margin: 0;
431
+ font-size: 13px;
432
+ font-weight: 700;
433
+ }
434
+
435
+ .usage-header {
436
+ display: grid;
437
+ grid-template-columns: minmax(0, 1fr) 44px 44px 50px;
438
+ align-items: center;
439
+ gap: 8px;
440
+ margin-bottom: 4px;
441
+ }
442
+
443
+ .usage-header-stat {
444
+ font-family: var(--font-mono);
445
+ font-size: 9px;
446
+ color: var(--muted);
447
+ text-align: right;
448
+ text-transform: uppercase;
449
+ letter-spacing: 0.5px;
450
+ }
451
+
452
+ .usage-list {
453
+ display: grid;
454
+ gap: 1px;
455
+ }
456
+
457
+ .usage-row {
458
+ display: grid;
459
+ grid-template-columns: minmax(0, 1fr) 44px 44px 50px;
460
+ align-items: center;
461
+ gap: 8px;
462
+ min-height: 21px;
463
+ border-bottom: 0;
464
+ font-size: 11px;
465
+ }
466
+
467
+ .usage-row + .usage-row {
468
+ box-shadow: none;
469
+ }
470
+
471
+ .usage-name {
472
+ min-width: 0;
473
+ overflow: hidden;
474
+ text-overflow: ellipsis;
475
+ white-space: nowrap;
476
+ color: var(--fg);
477
+ font-weight: 500;
478
+ }
479
+
480
+ .usage-stat {
481
+ font-family: var(--font-mono);
482
+ font-size: 10px;
483
+ white-space: nowrap;
484
+ text-align: right;
485
+ }
486
+
487
+ .usage-stat.input { color: color-mix(in oklch, var(--accent-strong) 76%, black); }
488
+ .usage-stat.output { color: oklch(45% 0.13 255); }
489
+ .usage-stat.cached { color: oklch(62% 0.15 75); }
490
+
491
+ .usage-empty {
492
+ padding: 12px 0 11px;
493
+ color: var(--muted);
494
+ font-size: 12px;
495
+ text-align: center;
496
+ }
497
+
272
498
  .actions {
273
499
  display: grid;
274
500
  grid-template-columns: repeat(2, minmax(0, 1fr));
275
- gap: 8px;
276
- align-self: end;
501
+ gap: 0;
502
+ margin-top: auto;
503
+ margin-inline: -24px;
504
+ padding: 3px 18px 4px;
505
+ border-top: 1px solid var(--border);
506
+ background: rgba(248, 250, 249, 0.86);
277
507
  }
278
508
 
279
509
  .action {
280
510
  appearance: none;
281
- border: 1px solid var(--border);
282
- border-radius: 14px;
283
- min-height: 42px;
284
- padding: 0 14px;
511
+ border: 0;
512
+ border-radius: 10px;
513
+ min-height: 25px;
514
+ padding: 0 6px;
285
515
  font-family: var(--font-body);
286
- font-size: 13px;
287
- font-weight: 600;
516
+ font-size: 11px;
517
+ font-weight: 500;
288
518
  cursor: pointer;
289
519
  transition: transform 160ms ease, background-color 160ms ease, border-color 160ms ease, opacity 160ms ease;
520
+ background: transparent;
521
+ color: var(--muted);
522
+ display: inline-flex;
523
+ align-items: center;
524
+ justify-content: center;
290
525
  }
291
526
 
292
- .action:hover { transform: translateY(-1px); }
527
+ .action:hover { background: rgba(233, 238, 235, 0.72); }
293
528
  .action:disabled { opacity: 0.6; cursor: default; transform: none; }
294
529
 
295
- .action.primary {
296
- background: #1f2423;
297
- border-color: #1f2423;
298
- color: white;
530
+ .action-icon {
531
+ display: inline-flex;
532
+ align-items: center;
533
+ justify-content: center;
534
+ width: 15px;
535
+ height: 15px;
536
+ margin-right: 6px;
537
+ vertical-align: -1px;
299
538
  }
300
539
 
301
- .action.secondary {
302
- background: rgba(255, 255, 255, 0.94);
303
- color: var(--fg);
540
+ .action-icon svg {
541
+ width: 15px;
542
+ height: 15px;
543
+ stroke: currentColor;
544
+ fill: none;
545
+ stroke-width: 2;
546
+ stroke-linecap: round;
547
+ stroke-linejoin: round;
304
548
  }
305
549
 
550
+ .action.primary,
551
+ .action.secondary { color: var(--muted); }
552
+
306
553
  .drawer-overlay {
307
554
  position: absolute;
308
555
  inset: 0;
309
- background: transparent;
556
+ z-index: 20;
557
+ background: rgba(250, 252, 251, 0.96);
558
+ border: 1px solid rgba(255, 255, 255, 0.72);
559
+ border-radius: var(--radius-shell);
310
560
  opacity: 0;
311
561
  pointer-events: none;
312
562
  transition: opacity 200ms ease;
313
563
  overflow: hidden;
314
- border-radius: var(--radius-shell);
564
+ backdrop-filter: blur(28px);
315
565
  }
316
566
 
317
567
  .drawer {
318
568
  height: 100%;
319
- display: grid;
320
- grid-template-rows: auto 1fr auto;
321
- gap: 14px;
322
- padding: 14px;
569
+ display: flex;
570
+ flex-direction: column;
571
+ padding: 0 24px;
323
572
  border-radius: var(--radius-shell);
324
- background: rgba(255, 255, 255, 0.96);
325
- backdrop-filter: blur(20px);
326
- border: 1px solid rgba(255, 255, 255, 0.84);
327
- box-shadow: 0 18px 48px rgba(20, 28, 38, 0.14);
573
+ background: linear-gradient(180deg, rgba(250, 252, 251, 0.98), rgba(244, 247, 245, 0.96));
574
+ border: none;
575
+ box-shadow: none;
328
576
  transform: translateY(10px);
329
577
  opacity: 0;
330
578
  transition: transform 220ms ease, opacity 220ms ease;
@@ -343,36 +591,67 @@
343
591
  .drawer-header {
344
592
  display: flex;
345
593
  align-items: center;
346
- justify-content: flex-start;
594
+ gap: 8px;
595
+ min-height: 48px;
596
+ border-bottom: 1px solid var(--border);
347
597
  }
348
598
 
349
599
  .drawer-back {
350
600
  appearance: none;
351
- border: 1px solid var(--border);
352
- background: rgba(255, 255, 255, 0.94);
353
- color: var(--fg);
601
+ border: 0;
602
+ background: transparent;
603
+ color: var(--muted);
354
604
  display: inline-flex;
355
605
  align-items: center;
356
- gap: 4px;
357
- min-width: auto;
358
- height: 32px;
359
- padding: 0 12px;
360
- border-radius: 999px;
361
- font-size: 13px;
362
- font-weight: 600;
606
+ justify-content: center;
607
+ width: 22px;
608
+ height: 22px;
609
+ padding: 0;
610
+ margin-left: -4px;
611
+ border-radius: 6px;
612
+ font-size: 0;
363
613
  font-family: var(--font-body);
364
614
  cursor: pointer;
615
+ transition: background-color 160ms ease, color 160ms ease;
616
+ }
617
+
618
+ .drawer-back:hover {
619
+ background: rgba(233, 238, 235, 0.72);
620
+ color: var(--fg);
365
621
  }
366
622
 
367
623
  .drawer-back-arrow {
368
- font-size: 17px;
369
- line-height: 1;
624
+ display: inline-flex;
625
+ align-items: center;
626
+ justify-content: center;
627
+ }
628
+
629
+ .drawer-back-arrow svg {
630
+ width: 15px;
631
+ height: 15px;
632
+ stroke: currentColor;
633
+ fill: none;
634
+ stroke-width: 2;
635
+ stroke-linecap: round;
636
+ stroke-linejoin: round;
637
+ }
638
+
639
+ .drawer-heading {
640
+ min-width: 0;
641
+ }
642
+
643
+ .drawer-title {
644
+ margin: 0;
645
+ font-size: 15px;
646
+ line-height: 1.1;
647
+ font-weight: 700;
370
648
  }
371
649
 
372
650
  .settings-list {
373
651
  display: grid;
374
652
  align-content: start;
375
- gap: 8px;
653
+ gap: 0;
654
+ padding: 7px 0 0;
376
655
  }
377
656
 
378
657
  .settings-row {
@@ -380,10 +659,14 @@
380
659
  align-items: center;
381
660
  justify-content: space-between;
382
661
  gap: 12px;
383
- padding: 12px 14px;
384
- border-radius: 14px;
385
- border: 1px solid var(--border);
386
- background: rgba(250, 252, 251, 0.94);
662
+ min-height: 48px;
663
+ padding: 6px 0;
664
+ border-bottom: 1px solid var(--border);
665
+ background: transparent;
666
+ }
667
+
668
+ .settings-row.has-update {
669
+ border-color: color-mix(in oklch, var(--notice) 30%, var(--border));
387
670
  }
388
671
 
389
672
  .settings-copy {
@@ -392,24 +675,48 @@
392
675
 
393
676
  .settings-label {
394
677
  margin: 0;
395
- font-size: 13px;
396
- font-weight: 600;
678
+ font-size: 12px;
679
+ font-weight: 700;
680
+ }
681
+
682
+ .settings-title-line {
683
+ display: flex;
684
+ align-items: center;
685
+ gap: 6px;
686
+ }
687
+
688
+ .update-pill {
689
+ display: none;
690
+ padding: 2px 6px;
691
+ border-radius: 7px;
692
+ background: color-mix(in oklch, var(--notice) 20%, white);
693
+ color: color-mix(in oklch, var(--notice) 82%, black);
694
+ font-family: var(--font-mono);
695
+ font-size: 9px;
696
+ font-weight: 700;
697
+ letter-spacing: 0;
698
+ text-transform: uppercase;
699
+ }
700
+
701
+ .settings-row.has-update .update-pill {
702
+ display: inline-flex;
397
703
  }
398
704
 
399
705
  .settings-meta {
400
706
  margin: 3px 0 0;
401
- font-size: 11px;
707
+ font-size: 10px;
402
708
  color: var(--muted);
709
+ line-height: 1.25;
403
710
  }
404
711
 
405
712
  .settings-value {
406
713
  display: inline-flex;
407
714
  align-items: center;
408
715
  justify-content: center;
409
- min-height: 28px;
410
- min-width: 58px;
716
+ min-height: 26px;
717
+ min-width: 54px;
411
718
  padding-inline: 10px;
412
- border-radius: 999px;
719
+ border-radius: 9px;
413
720
  border: 1px solid var(--border);
414
721
  background: rgba(255, 255, 255, 0.96);
415
722
  color: var(--muted);
@@ -421,92 +728,26 @@
421
728
  .settings-value.button {
422
729
  cursor: pointer;
423
730
  color: var(--fg);
731
+ transition: background-color 160ms ease, border-color 160ms ease;
424
732
  }
425
733
 
426
- /* Agent filter dropdown */
427
- .agent-dropdown-wrap {
428
- position: relative;
429
- }
430
-
431
- .agent-dropdown-trigger {
432
- cursor: pointer;
433
- color: var(--fg);
434
- padding-right: 24px;
435
- position: relative;
436
- }
437
-
438
- .agent-dropdown-trigger::after {
439
- content: "▾";
440
- position: absolute;
441
- right: 10px;
442
- font-size: 10px;
443
- top: 50%;
444
- transform: translateY(-50%);
445
- }
446
-
447
- .agent-dropdown-menu {
448
- display: none;
449
- position: absolute;
450
- right: 0;
451
- top: calc(100% + 6px);
452
- min-width: 160px;
453
- padding: 6px;
454
- border-radius: 14px;
455
- border: 1px solid var(--border);
456
- background: white;
457
- box-shadow: 0 8px 24px rgba(20, 28, 38, 0.12);
458
- z-index: 10;
459
- }
460
-
461
- .agent-dropdown-menu.open { display: block; }
462
-
463
- .agent-option {
464
- display: flex;
465
- align-items: center;
466
- gap: 8px;
467
- padding: 8px 10px;
468
- border-radius: 10px;
469
- cursor: pointer;
470
- font-size: 13px;
471
- font-weight: 500;
472
- transition: background 120ms ease;
473
- }
474
-
475
- .agent-option:hover { background: rgba(240, 244, 242, 0.9); }
476
-
477
- .agent-checkbox {
478
- width: 16px;
479
- height: 16px;
480
- border-radius: 4px;
481
- border: 1.5px solid var(--border);
482
- display: flex;
483
- align-items: center;
484
- justify-content: center;
485
- font-size: 10px;
486
- flex-shrink: 0;
487
- transition: background 120ms ease, border-color 120ms ease;
734
+ .settings-value.button:hover {
735
+ background: rgba(247, 249, 248, 0.96);
488
736
  }
489
737
 
490
- .agent-checkbox.checked {
491
- background: color-mix(in oklch, var(--accent) 72%, white);
492
- border-color: var(--accent);
493
- }
494
-
495
- .agent-checkbox.checked::after {
496
- content: "✓";
497
- color: white;
738
+ .settings-value.button.primary-update {
739
+ border-color: color-mix(in oklch, var(--notice) 45%, var(--border));
740
+ background: color-mix(in oklch, var(--notice) 18%, white);
741
+ color: color-mix(in oklch, var(--notice) 74%, black);
498
742
  font-weight: 700;
499
- font-size: 10px;
500
743
  }
501
744
 
502
745
  .settings-divider {
503
- height: 1px;
504
- background: var(--border);
505
- margin: 4px 0;
746
+ height: 8px;
506
747
  }
507
748
 
508
749
  .settings-quit-row {
509
- border-color: rgba(192, 57, 43, 0.15);
750
+ border-bottom: 0;
510
751
  }
511
752
 
512
753
  .settings-quit-btn {
@@ -514,19 +755,21 @@
514
755
  }
515
756
 
516
757
  .settings-version {
517
- text-align: center;
758
+ margin-top: auto;
759
+ border-top: 1px solid var(--border);
760
+ text-align: left;
518
761
  font-family: var(--font-mono);
519
- font-size: 11px;
762
+ font-size: 10px;
520
763
  color: var(--muted);
521
- letter-spacing: 0.06em;
522
- text-transform: uppercase;
523
- padding-block: 2px 6px;
764
+ letter-spacing: 0;
765
+ text-transform: none;
766
+ padding-block: 10px 13px;
524
767
  }
525
768
 
526
769
  .switch {
527
770
  position: relative;
528
- width: 44px;
529
- height: 26px;
771
+ width: 42px;
772
+ height: 24px;
530
773
  border-radius: 999px;
531
774
  border: 1px solid color-mix(in srgb, #000 7%, transparent);
532
775
  background: rgba(228, 231, 229, 0.9);
@@ -539,8 +782,8 @@
539
782
  position: absolute;
540
783
  inset-block-start: 2px;
541
784
  inset-inline-start: 2px;
542
- width: 20px;
543
- height: 20px;
785
+ width: 18px;
786
+ height: 18px;
544
787
  border-radius: 50%;
545
788
  background: white;
546
789
  box-shadow: 0 1px 3px rgba(20, 28, 38, 0.18);
@@ -559,7 +802,8 @@
559
802
  <body>
560
803
  <main class="shell" aria-label="Token dashboard menu" data-view="main">
561
804
  <header class="toolbar">
562
- <p class="toolbar-label">Today <span id="date"></span></p>
805
+ <p class="toolbar-label"><strong>Today</strong> · <span id="date"></span></p>
806
+ <div class="cache-pill" id="cache-pill">cache --</div>
563
807
  </header>
564
808
 
565
809
  <div class="error-banner" id="error-banner">
@@ -568,22 +812,27 @@
568
812
 
569
813
  <section class="content" id="content">
570
814
  <section class="summary" aria-label="Summary metrics">
571
- <article class="card">
572
- <p class="card-label">Total</p>
573
- <p class="card-value" id="total-tokens"><span class="skeleton"></span></p>
574
- </article>
575
- <article class="card">
576
- <p class="card-label">Input</p>
577
- <p class="card-value" id="input"><span class="skeleton"></span></p>
578
- </article>
579
- <article class="card">
580
- <p class="card-label">Output</p>
581
- <p class="card-value" id="output"><span class="skeleton"></span></p>
582
- </article>
583
- <article class="card">
584
- <p class="card-label">Hit rate</p>
585
- <p class="card-value" id="cache-rate"><span class="skeleton"></span></p>
586
- </article>
815
+ <div class="summary-top">
816
+ <div>
817
+ <p class="card-label">Total</p>
818
+ <p class="card-value" id="total-tokens"><span class="skeleton"></span></p>
819
+ </div>
820
+ <p class="cost-value" id="total-cost">$--</p>
821
+ </div>
822
+ <div class="mini-metrics" aria-label="Token breakdown">
823
+ <div class="mini-metric">
824
+ <p class="mini-label">In</p>
825
+ <p class="mini-value input" id="input"><span class="skeleton"></span></p>
826
+ </div>
827
+ <div class="mini-metric">
828
+ <p class="mini-label">Out</p>
829
+ <p class="mini-value output" id="output"><span class="skeleton"></span></p>
830
+ </div>
831
+ <div class="mini-metric">
832
+ <p class="mini-label">Cached</p>
833
+ <p class="mini-value cached" id="cached"><span class="skeleton"></span></p>
834
+ </div>
835
+ </div>
587
836
  </section>
588
837
 
589
838
  <section class="chart-panel" aria-label="Hourly usage">
@@ -594,17 +843,41 @@
594
843
  <div class="y-axis" id="y-axis" aria-hidden="true"></div>
595
844
  <div class="plot" id="plot">
596
845
  <div class="chart-empty">
597
- <div class="chart-empty-icon">&#x1F4CA;</div>
598
846
  <div class="chart-empty-title">No usage yet</div>
599
847
  <div>Start a session to see your hourly breakdown.</div>
600
848
  </div>
601
849
  </div>
850
+ <div class="x-axis" id="x-axis" aria-hidden="true"></div>
851
+ </div>
852
+ </section>
853
+
854
+ <section class="usage-panel" aria-label="Projects">
855
+ <div class="usage-header">
856
+ <span class="usage-title">Projects</span>
857
+ <span class="usage-header-stat">in</span>
858
+ <span class="usage-header-stat">out</span>
859
+ <span class="usage-header-stat">cached</span>
860
+ </div>
861
+ <div class="usage-list" id="projects-list">
862
+ <div class="usage-empty">Loading projects...</div>
863
+ </div>
864
+ </section>
865
+
866
+ <section class="usage-panel" aria-label="Agents">
867
+ <div class="usage-header">
868
+ <span class="usage-title">Agents</span>
869
+ <span class="usage-header-stat">in</span>
870
+ <span class="usage-header-stat">out</span>
871
+ <span class="usage-header-stat">cached</span>
872
+ </div>
873
+ <div class="usage-list" id="agents-list">
874
+ <div class="usage-empty">Loading agents...</div>
602
875
  </div>
603
876
  </section>
604
877
 
605
878
  <section class="actions" aria-label="Primary actions">
606
- <button class="action primary" type="button" id="open-dashboard">Open Dashboard</button>
607
- <button class="action secondary" type="button" id="open-settings">Settings</button>
879
+ <button class="action primary" type="button" id="open-dashboard"><span class="action-icon" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M3 3v18h18"/><path d="M18 17V9"/><path d="M13 17V5"/><path d="M8 17v-3"/></svg></span>Dashboard</button>
880
+ <button class="action secondary" type="button" id="open-settings"><span class="action-icon" aria-hidden="true"><svg viewBox="0 0 24 24"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg></span>Settings</button>
608
881
  </section>
609
882
  </section>
610
883
 
@@ -612,14 +885,18 @@
612
885
  <section class="drawer" aria-label="Settings drawer">
613
886
  <div class="drawer-header">
614
887
  <button class="drawer-back" type="button" aria-label="Back" id="close-settings">
615
- <span class="drawer-back-arrow">&lsaquo;</span>
888
+ <span class="drawer-back-arrow"><svg viewBox="0 0 24 24"><path d="m15 18-6-6 6-6"/></svg></span>
616
889
  </button>
890
+ <p class="drawer-title">Settings</p>
617
891
  </div>
618
892
 
619
893
  <div class="settings-list">
620
- <article class="settings-row">
894
+ <article class="settings-row" id="update-row">
621
895
  <div class="settings-copy">
622
- <p class="settings-label">Check for updates</p>
896
+ <div class="settings-title-line">
897
+ <p class="settings-label">Check for updates</p>
898
+ <span class="update-pill" id="update-pill">New</span>
899
+ </div>
623
900
  <p class="settings-meta" id="update-status">Check whether a newer build is available.</p>
624
901
  </div>
625
902
  <button class="settings-value button" type="button" id="check-updates">Check</button>
@@ -633,17 +910,6 @@
633
910
  <button class="switch" type="button" id="launch-at-login" role="switch" aria-checked="false" aria-label="Launch at login"></button>
634
911
  </article>
635
912
 
636
- <article class="settings-row" id="agent-filter-row">
637
- <div class="settings-copy">
638
- <p class="settings-label">Data sources</p>
639
- <p class="settings-meta">Filter which agents contribute to metrics.</p>
640
- </div>
641
- <div class="agent-dropdown-wrap">
642
- <button class="settings-value button agent-dropdown-trigger" type="button" id="agent-dropdown-btn">All</button>
643
- <div class="agent-dropdown-menu" id="agent-dropdown-menu"></div>
644
- </div>
645
- </article>
646
-
647
913
  <div class="settings-divider"></div>
648
914
 
649
915
  <article class="settings-row settings-quit-row">
@@ -665,16 +931,14 @@
665
931
  var port = window.location.port || '3456';
666
932
  var apiBase = 'http://localhost:' + port;
667
933
  var appInfo = { version: '--', launchAtLogin: false };
668
- var availableAgents = [];
669
- var selectedAgents = [];
670
- var isFirstLoad = true;
934
+ var updateInfo = null;
671
935
 
672
936
  // --- Helpers ---
673
937
 
674
938
  function formatNumber(n) {
675
939
  if (!Number.isFinite(n) || n <= 0) return '0';
676
- if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
677
- if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K';
940
+ if (n >= 1e6) return trimTrailingZero((n / 1e6).toFixed(n >= 10e6 ? 1 : 2)) + 'M';
941
+ if (n >= 1e3) return trimTrailingZero((n / 1e3).toFixed(n >= 100e3 ? 0 : 1)) + 'K';
678
942
  return String(Math.round(n));
679
943
  }
680
944
 
@@ -682,6 +946,32 @@
682
946
  return (Number.isFinite(n) ? n : 0).toFixed(1) + '%';
683
947
  }
684
948
 
949
+ function formatCost(n) {
950
+ var value = Number(n) || 0;
951
+ if (value >= 100) return '$' + Math.round(value);
952
+ if (value >= 10) return '$' + value.toFixed(1);
953
+ return '$' + value.toFixed(2);
954
+ }
955
+
956
+ function trimTrailingZero(value) {
957
+ return String(value).replace(/\.0+$/, '').replace(/(\.\d*[1-9])0+$/, '$1');
958
+ }
959
+
960
+ function formatProjectName(path) {
961
+ var value = String(path || 'Unknown');
962
+ var parts = value.split('/').filter(Boolean);
963
+ return parts[parts.length - 1] || value;
964
+ }
965
+
966
+ function escapeHtml(value) {
967
+ return String(value)
968
+ .replace(/&/g, '&amp;')
969
+ .replace(/</g, '&lt;')
970
+ .replace(/>/g, '&gt;')
971
+ .replace(/"/g, '&quot;')
972
+ .replace(/'/g, '&#39;');
973
+ }
974
+
685
975
  function getTodayString() {
686
976
  var d = new Date();
687
977
  return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
@@ -700,25 +990,17 @@
700
990
  // --- Dynamic chart hours based on current time ---
701
991
 
702
992
  function getChartHours() {
703
- var now = new Date();
704
- var currentHour = now.getHours();
705
993
  var hours = [];
706
- for (var h = 0; h <= currentHour; h += 2) {
994
+ for (var h = 0; h < 24; h += 1) {
707
995
  hours.push(h);
708
996
  }
709
- // At least 4 bars
710
- while (hours.length < 4) {
711
- var next = hours[hours.length - 1] + 2;
712
- if (next > 23) break;
713
- hours.push(next);
714
- }
715
997
  return hours;
716
998
  }
717
999
 
718
1000
  // --- Loading & Error states ---
719
1001
 
720
1002
  function showSkeletons() {
721
- ['total-tokens', 'input', 'output', 'cache-rate'].forEach(function(id) {
1003
+ ['total-tokens', 'input', 'output', 'cached'].forEach(function(id) {
722
1004
  var el = document.getElementById(id);
723
1005
  if (el && !el.querySelector('.skeleton')) return; // already has data
724
1006
  if (el) el.innerHTML = '<span class="skeleton"></span>';
@@ -762,12 +1044,15 @@
762
1044
  setCardValue('total-tokens', formatNumber(totalTokens), totalTokens === 0);
763
1045
  setCardValue('input', formatNumber(totalInput), totalInput === 0);
764
1046
  setCardValue('output', formatNumber(totalOutput), totalOutput === 0);
765
- setCardValue('cache-rate', formatPercent(cacheRate), cacheRate === 0);
1047
+ setCardValue('cached', formatNumber(totalCacheRead), totalCacheRead === 0);
1048
+ document.getElementById('total-cost').textContent = formatCost(totalCost);
1049
+ applyMetricFit(document.getElementById('total-cost'), formatCost(totalCost));
1050
+ document.getElementById('cache-pill').textContent = 'cache ' + formatPercent(cacheRate);
766
1051
 
767
1052
  return {
768
1053
  today: todayStr,
769
- agentKey: selectedAgents.slice().sort().join(','),
770
1054
  totalTokens: totalTokens,
1055
+ totalInput: totalInput,
771
1056
  totalCost: totalCost,
772
1057
  totalCacheRead: totalCacheRead
773
1058
  };
@@ -777,6 +1062,24 @@
777
1062
  var el = document.getElementById(id);
778
1063
  el.textContent = text;
779
1064
  el.classList.toggle('empty', isEmpty);
1065
+ applyMetricFit(el, text);
1066
+ }
1067
+
1068
+ function applyMetricFit(el, text) {
1069
+ if (!el) return;
1070
+ var length = String(text || '').length;
1071
+
1072
+ if (el.id === 'total-tokens') {
1073
+ el.style.fontSize = length >= 8 ? '27px' : length >= 6 ? '29px' : '32px';
1074
+ return;
1075
+ }
1076
+
1077
+ if (el.id === 'total-cost') {
1078
+ el.style.fontSize = length >= 8 ? '21px' : length >= 6 ? '23px' : '25px';
1079
+ return;
1080
+ }
1081
+
1082
+ el.style.fontSize = length >= 8 ? '13px' : '15px';
780
1083
  }
781
1084
 
782
1085
  // --- Chart rendering ---
@@ -807,10 +1110,12 @@
807
1110
 
808
1111
  var plot = document.getElementById('plot');
809
1112
  var axis = document.getElementById('y-axis');
1113
+ var xAxis = document.getElementById('x-axis');
810
1114
 
811
1115
  // Empty state
812
1116
  if (maxValue === 0) {
813
1117
  axis.innerHTML = '';
1118
+ xAxis.innerHTML = '';
814
1119
  plot.innerHTML = '';
815
1120
  plot.style.gridTemplateColumns = '';
816
1121
  plot.innerHTML = '<div class="chart-empty">' +
@@ -820,55 +1125,77 @@
820
1125
  return;
821
1126
  }
822
1127
 
823
- var axisInfo = niceStep(maxValue);
824
- renderAxis(maxValue);
1128
+ var axisInfo = buildAxis(maxValue);
1129
+ renderAxis(axisInfo);
825
1130
  var maxEntry = selected.reduce(function(max, entry) {
826
1131
  return !max || entry.value > max.value ? entry : max;
827
1132
  }, null);
828
- renderBars(selected, axisInfo.top, maxEntry);
1133
+ renderBars(selected, axisInfo, maxEntry);
829
1134
  }
830
1135
 
831
- function niceStep(value) {
832
- // Always divide into 3 intervals, pick a step that gives round numbers
833
- if (value <= 0) return { top: 3, step: 1 };
834
- var rawStep = value / 3;
1136
+ function niceIncrement(rawStep) {
1137
+ if (rawStep <= 0) return 1;
835
1138
  var exp = Math.floor(Math.log10(rawStep));
836
1139
  var base = Math.pow(10, exp);
837
1140
  var frac = rawStep / base;
838
- // Round step up to nearest 1, 2, or 5 × 10^n
839
- var nice = frac <= 1 ? base : frac <= 2 ? 2 * base : frac <= 5 ? 5 * base : 10 * base;
840
- var top = nice * 3;
841
- return { top: top, step: nice };
1141
+ if (frac <= 1) return base;
1142
+ if (frac <= 2) return 2 * base;
1143
+ if (frac <= 2.5) return 2.5 * base;
1144
+ if (frac <= 5) return 5 * base;
1145
+ return 10 * base;
1146
+ }
1147
+
1148
+ function buildAxis(value) {
1149
+ if (value <= 0) return { top: 4, ticks: [4, 3, 2, 1, 0] };
1150
+ var step = niceIncrement(value / 4);
1151
+ var top = step * Math.ceil(value / step);
1152
+ if (top / value > 1.35 && top > step) top -= step;
1153
+ if (top < value) top += step;
1154
+ var segments = Math.max(3, Math.min(5, Math.round(top / step)));
1155
+ top = step * segments;
1156
+
1157
+ var ticks = [];
1158
+ for (var i = segments; i >= 0; i--) {
1159
+ ticks.push(step * i);
1160
+ }
1161
+ return { top: top, ticks: ticks };
842
1162
  }
843
1163
 
844
- function renderAxis(axisMax) {
1164
+ function renderAxis(info) {
845
1165
  var axis = document.getElementById('y-axis');
846
- var info = niceStep(axisMax);
847
- axis.innerHTML = [
848
- '<span>' + formatNumber(info.top) + '</span>',
849
- '<span>' + formatNumber(info.top - info.step) + '</span>',
850
- '<span>' + formatNumber(info.top - 2 * info.step) + '</span>',
851
- '<span>0</span>'
852
- ].join('');
1166
+ axis.innerHTML = info.ticks.map(function(tick) {
1167
+ var top = ((info.top - tick) / info.top) * 100;
1168
+ return '<span style="top: ' + top + '%">' + formatNumber(tick) + '</span>';
1169
+ }).join('');
853
1170
  }
854
1171
 
855
- function renderBars(selected, maxValue, maxEntry) {
1172
+ function renderBars(selected, axisInfo, maxEntry) {
856
1173
  var plot = document.getElementById('plot');
1174
+ var xAxis = document.getElementById('x-axis');
857
1175
  plot.innerHTML = '';
858
- plot.style.gridTemplateColumns = 'repeat(' + selected.length + ', minmax(0, 1fr))';
859
-
860
- // Adjust gap based on number of bars
861
- var gap = selected.length > 10 ? '4px' : selected.length > 6 ? '6px' : '8px';
1176
+ plot.style.gridTemplateColumns = 'repeat(24, minmax(0, 1fr))';
1177
+ xAxis.innerHTML = '';
1178
+ xAxis.style.gridTemplateColumns = 'repeat(24, minmax(0, 1fr))';
1179
+ renderGrid(plot, axisInfo);
1180
+
1181
+ // Dynamic gap and bar width based on bar count
1182
+ var count = selected.filter(function(entry) { return entry.value > 0; }).length;
1183
+ var gap = '2px';
1184
+ var barMaxWidth = count > 10 ? '7px' : count > 6 ? '9px' : '12px';
862
1185
  plot.style.gap = gap;
1186
+ xAxis.style.gap = gap;
863
1187
 
864
1188
  selected.forEach(function(entry) {
865
- var height = entry.value > 0 ? Math.max(8, (entry.value / maxValue) * 100) : 3;
1189
+ if (entry.value <= 0) return;
1190
+ var height = entry.value > 0 ? Math.max(8, (entry.value / axisInfo.top) * 100) : 0;
866
1191
  var wrap = document.createElement('div');
867
1192
  wrap.className = 'bar-wrap';
1193
+ wrap.style.gridColumn = String(entry.hour + 1);
868
1194
  wrap.style.setProperty('--height', height + '%');
869
1195
 
870
1196
  var bar = document.createElement('div');
871
1197
  bar.className = 'bar';
1198
+ bar.style.maxWidth = barMaxWidth + 'px';
872
1199
  if (entry.value === 0) {
873
1200
  bar.classList.add('zero');
874
1201
  } else if (maxEntry && entry.hour === maxEntry.hour && entry.value === maxEntry.value) {
@@ -884,12 +1211,148 @@
884
1211
  wrap.appendChild(tip);
885
1212
  }
886
1213
 
1214
+ plot.appendChild(wrap);
1215
+ });
1216
+
1217
+ renderActiveHourLabels(xAxis, selected, maxEntry);
1218
+ }
1219
+
1220
+ function renderGrid(plot, info) {
1221
+ var grid = document.createElement('div');
1222
+ grid.className = 'plot-grid';
1223
+
1224
+ info.ticks.forEach(function(tick) {
1225
+ var line = document.createElement('span');
1226
+ line.className = 'plot-grid-line';
1227
+ line.style.top = (((info.top - tick) / info.top) * 100) + '%';
1228
+ grid.appendChild(line);
1229
+ });
1230
+
1231
+ plot.appendChild(grid);
1232
+ }
1233
+
1234
+ function renderActiveHourLabels(xAxis, selected, maxEntry) {
1235
+ var active = selected.filter(function(entry) { return entry.value > 0; });
1236
+ var labeledHours = [];
1237
+
1238
+ active.forEach(function(entry) {
1239
+ var isPeak = maxEntry && entry.hour === maxEntry.hour;
1240
+ var hasRoom = labeledHours.every(function(hour) {
1241
+ return Math.abs(entry.hour - hour) >= 3;
1242
+ });
1243
+ var shouldLabel = isPeak || hasRoom;
1244
+
1245
+ if (shouldLabel) labeledHours.push(entry.hour);
1246
+
887
1247
  var label = document.createElement('span');
888
- label.className = 'bar-label';
1248
+ label.className = 'x-axis-label' + (shouldLabel ? '' : ' is-unlabeled');
1249
+ label.style.gridColumn = String(entry.hour + 1);
889
1250
  label.textContent = String(entry.hour).padStart(2, '0');
890
- wrap.appendChild(label);
1251
+ xAxis.appendChild(label);
1252
+ });
1253
+ }
891
1254
 
892
- plot.appendChild(wrap);
1255
+ function renderProjects(allProjects) {
1256
+ var todayStr = getTodayString();
1257
+ var totals = {};
1258
+
1259
+ (allProjects || []).forEach(function(data) {
1260
+ var projects = data && data.projects;
1261
+ if (!projects) return;
1262
+
1263
+ Object.keys(projects).forEach(function(projectPath) {
1264
+ var todayEntries = (projects[projectPath] || []).filter(function(entry) {
1265
+ return entry && entry.date === todayStr;
1266
+ });
1267
+ if (todayEntries.length === 0) return;
1268
+
1269
+ if (!totals[projectPath]) {
1270
+ totals[projectPath] = { path: projectPath, input: 0, output: 0, cached: 0, total: 0 };
1271
+ }
1272
+ todayEntries.forEach(function(entry) {
1273
+ totals[projectPath].input += entry.inputTokens || 0;
1274
+ totals[projectPath].output += entry.outputTokens || 0;
1275
+ totals[projectPath].cached += entry.cacheReadTokens || 0;
1276
+ totals[projectPath].total += entry.totalTokens || 0;
1277
+ });
1278
+ });
1279
+ });
1280
+
1281
+ var rows = Object.keys(totals).map(function(key) { return totals[key]; })
1282
+ .sort(function(a, b) { return b.total - a.total; })
1283
+ .slice(0, 4);
1284
+ var list = document.getElementById('projects-list');
1285
+
1286
+ if (rows.length === 0) {
1287
+ list.innerHTML = '<div class="usage-empty">No project usage today.</div>';
1288
+ return;
1289
+ }
1290
+
1291
+ list.innerHTML = '';
1292
+ rows.forEach(function(row) {
1293
+ var item = document.createElement('div');
1294
+ item.className = 'usage-row';
1295
+ item.innerHTML =
1296
+ '<span class="usage-name" title="' + escapeHtml(row.path) + '">' + escapeHtml(formatProjectName(row.path)) + '</span>' +
1297
+ '<span class="usage-stat input">' + formatNumber(row.input) + '</span>' +
1298
+ '<span class="usage-stat output">' + formatNumber(row.output) + '</span>' +
1299
+ '<span class="usage-stat cached">' + formatNumber(row.cached) + '</span>';
1300
+ list.appendChild(item);
1301
+ });
1302
+ }
1303
+
1304
+ function formatAgentName(agent) {
1305
+ var names = {
1306
+ claude: 'Claude Code',
1307
+ codex: 'Codex',
1308
+ openclaw: 'OpenClaw',
1309
+ opencode: 'OpenCode'
1310
+ };
1311
+ return names[agent] || agent;
1312
+ }
1313
+
1314
+ function renderAgents(allDaily, agents) {
1315
+ var todayStr = getTodayString();
1316
+ var rows = (agents || []).map(function(agent, index) {
1317
+ var data = (allDaily || [])[index];
1318
+ var entry = null;
1319
+ if (data && Array.isArray(data.daily)) {
1320
+ entry = data.daily.find(function(day) { return day && day.date === todayStr; });
1321
+ }
1322
+ var input = entry ? (entry.inputTokens || 0) : 0;
1323
+ var output = entry ? (entry.outputTokens || 0) : 0;
1324
+ var cached = entry ? (entry.cacheReadTokens || 0) : 0;
1325
+ return {
1326
+ name: formatAgentName(agent),
1327
+ input: input,
1328
+ output: output,
1329
+ cached: cached,
1330
+ total: entry ? (entry.totalTokens || input + output + cached) : 0
1331
+ };
1332
+ }).filter(function(row) {
1333
+ return row.total > 0 || row.input > 0 || row.output > 0;
1334
+ }).sort(function(a, b) {
1335
+ return b.total - a.total;
1336
+ });
1337
+
1338
+ var list = document.getElementById('agents-list');
1339
+ if (!list) return;
1340
+
1341
+ if (rows.length === 0) {
1342
+ list.innerHTML = '<div class="usage-empty">No agent usage today.</div>';
1343
+ return;
1344
+ }
1345
+
1346
+ list.innerHTML = '';
1347
+ rows.forEach(function(row) {
1348
+ var item = document.createElement('div');
1349
+ item.className = 'usage-row';
1350
+ item.innerHTML =
1351
+ '<span class="usage-name" title="' + escapeHtml(row.name) + '">' + escapeHtml(row.name) + '</span>' +
1352
+ '<span class="usage-stat input">' + formatNumber(row.input) + '</span>' +
1353
+ '<span class="usage-stat output">' + formatNumber(row.output) + '</span>' +
1354
+ '<span class="usage-stat cached">' + formatNumber(row.cached) + '</span>';
1355
+ list.appendChild(item);
893
1356
  });
894
1357
  }
895
1358
 
@@ -914,85 +1377,74 @@
914
1377
  return Promise.resolve();
915
1378
  }
916
1379
 
917
- // --- Agent filter ---
918
-
919
- function loadSelectedAgents() {
920
- try {
921
- var saved = localStorage.getItem('tokendash:selected-agents');
922
- if (saved) return JSON.parse(saved);
923
- } catch(e) {}
924
- return null;
925
- }
926
-
927
- function saveSelectedAgents() {
928
- try {
929
- localStorage.setItem('tokendash:selected-agents', JSON.stringify(selectedAgents));
930
- } catch(e) {}
931
- // Sync to main process for tray badge updates
932
- if (window.electronAPI && window.electronAPI.setSelectedAgents) {
933
- window.electronAPI.setSelectedAgents(selectedAgents);
934
- }
935
- }
936
-
937
- function updateAgentDropdownLabel() {
938
- var btn = document.getElementById('agent-dropdown-btn');
939
- if (selectedAgents.length === availableAgents.length) {
940
- btn.textContent = 'All';
1380
+ function setUpdateUi(state, message, nextInfo) {
1381
+ var row = document.getElementById('update-row');
1382
+ var button = document.getElementById('check-updates');
1383
+ var status = document.getElementById('update-status');
1384
+ updateInfo = nextInfo || updateInfo;
1385
+
1386
+ row.classList.toggle('has-update', state === 'available' || state === 'ready');
1387
+ button.classList.toggle('primary-update', state === 'available');
1388
+ button.disabled = state === 'checking' || state === 'downloading';
1389
+ status.textContent = message;
1390
+
1391
+ if (state === 'available') {
1392
+ button.textContent = 'Download';
1393
+ button.dataset.action = 'download';
1394
+ } else if (state === 'ready') {
1395
+ button.textContent = 'Done';
1396
+ button.dataset.action = 'check';
941
1397
  } else {
942
- btn.textContent = selectedAgents.length + ' of ' + availableAgents.length;
1398
+ button.textContent = state === 'checking' ? 'Checking' : state === 'downloading' ? 'Downloading' : 'Check';
1399
+ button.dataset.action = 'check';
943
1400
  }
944
1401
  }
945
1402
 
946
- function renderAgentDropdown() {
947
- var menu = document.getElementById('agent-dropdown-menu');
948
- menu.innerHTML = '';
949
-
950
- availableAgents.forEach(function(agent) {
951
- var isChecked = selectedAgents.indexOf(agent) !== -1;
952
- var option = document.createElement('div');
953
- option.className = 'agent-option';
954
- option.innerHTML =
955
- '<div class="agent-checkbox' + (isChecked ? ' checked' : '') + '"></div>' +
956
- '<span>' + agent.charAt(0).toUpperCase() + agent.slice(1) + '</span>';
957
-
958
- option.addEventListener('click', function(e) {
959
- e.stopPropagation();
960
- var idx = selectedAgents.indexOf(agent);
961
- if (idx !== -1) {
962
- // Don't allow deselecting the last agent
963
- if (selectedAgents.length <= 1) return;
964
- selectedAgents.splice(idx, 1);
965
- } else {
966
- selectedAgents.push(agent);
967
- }
968
- saveSelectedAgents();
969
- renderAgentDropdown();
970
- updateAgentDropdownLabel();
971
- fetchData();
972
- });
973
-
974
- menu.appendChild(option);
975
- });
1403
+ function checkForUpdates() {
1404
+ setUpdateUi('checking', 'Checking GitHub Releases...', null);
976
1405
 
977
- updateAgentDropdownLabel();
1406
+ var promise = window.electronAPI && window.electronAPI.checkForUpdates
1407
+ ? window.electronAPI.checkForUpdates()
1408
+ : Promise.resolve({ currentVersion: appInfo.version, latestVersion: appInfo.version, upToDate: true });
978
1409
 
979
- // Show/hide the row if only 1 agent available
980
- var row = document.getElementById('agent-filter-row');
981
- row.style.display = availableAgents.length <= 1 ? 'none' : '';
1410
+ return promise.then(function(result) {
1411
+ if (result.error) {
1412
+ setUpdateUi('idle', 'Unable to reach update service right now.', result);
1413
+ return;
1414
+ }
1415
+ if (result.upToDate) {
1416
+ setUpdateUi('idle', 'You are up to date on version ' + result.currentVersion + '.', result);
1417
+ return;
1418
+ }
1419
+ if (!result.asset || !result.asset.url) {
1420
+ setUpdateUi('idle', 'Version ' + result.latestVersion + ' is available, but no macOS DMG was attached.', result);
1421
+ return;
1422
+ }
1423
+ setUpdateUi('available', 'Version ' + result.latestVersion + ' is available.', result);
1424
+ }).catch(function() {
1425
+ setUpdateUi('idle', 'Unable to reach update service right now.', null);
1426
+ });
982
1427
  }
983
1428
 
984
- function initAgentFilter(agents) {
985
- availableAgents = agents;
986
- var saved = loadSelectedAgents();
987
- if (saved) {
988
- // Filter saved to only include agents that are actually available
989
- selectedAgents = saved.filter(function(a) { return agents.indexOf(a) !== -1; });
990
- if (selectedAgents.length === 0) selectedAgents = agents.slice(); // fallback
991
- } else {
992
- selectedAgents = agents.slice(); // default: all selected
1429
+ function downloadUpdate() {
1430
+ if (!updateInfo || !updateInfo.asset) {
1431
+ return checkForUpdates();
1432
+ }
1433
+ if (!window.electronAPI || !window.electronAPI.downloadUpdate) {
1434
+ setUpdateUi('idle', 'Download is only available in the macOS app.', updateInfo);
1435
+ return Promise.resolve();
993
1436
  }
994
- renderAgentDropdown();
995
- saveSelectedAgents();
1437
+
1438
+ setUpdateUi('downloading', 'Preparing download...', updateInfo);
1439
+ return window.electronAPI.downloadUpdate(updateInfo).then(function(result) {
1440
+ if (!result || !result.ok) {
1441
+ setUpdateUi('available', result && result.error ? result.error : 'Download failed.', updateInfo);
1442
+ return;
1443
+ }
1444
+ setUpdateUi('ready', 'Downloaded. The installer disk image has been opened.', updateInfo);
1445
+ }).catch(function() {
1446
+ setUpdateUi('available', 'Download failed. Please try again.', updateInfo);
1447
+ });
996
1448
  }
997
1449
 
998
1450
  // --- Data fetching ---
@@ -1005,30 +1457,30 @@
1005
1457
  .then(function(agentData) {
1006
1458
  var agents = (agentData && agentData.available) ? agentData.available : ['claude'];
1007
1459
 
1008
- // Init agent filter on first load or if agents changed
1009
- if (isFirstLoad || agents.length !== availableAgents.length) {
1010
- initAgentFilter(agents);
1011
- isFirstLoad = false;
1012
- }
1013
-
1014
- // Use filtered agents
1015
- var fetchAgents = selectedAgents.length > 0 ? selectedAgents : agents;
1016
-
1017
- return Promise.all(fetchAgents.map(function(agent) {
1460
+ return Promise.all(agents.map(function(agent) {
1018
1461
  return fetchJson(apiBase + '/api/daily?agent=' + agent).catch(function() { return null; });
1019
1462
  })).then(function(dailyResults) {
1020
- // Also fetch blocks for all selected agents
1021
- var blockPromises = fetchAgents.map(function(agent) {
1463
+ var blockPromises = agents.map(function(agent) {
1022
1464
  return fetchJson(apiBase + '/api/blocks?agent=' + agent).catch(function() { return null; });
1023
1465
  });
1024
- return Promise.all(blockPromises).then(function(blockResults) {
1025
- return { dailyResults: dailyResults, blockResults: blockResults };
1466
+ var projectPromises = agents.map(function(agent) {
1467
+ return fetchJson(apiBase + '/api/projects?agent=' + agent).catch(function() { return null; });
1468
+ });
1469
+ return Promise.all([Promise.all(blockPromises), Promise.all(projectPromises)]).then(function(results) {
1470
+ return {
1471
+ agents: agents,
1472
+ dailyResults: dailyResults,
1473
+ blockResults: results[0],
1474
+ projectResults: results[1]
1475
+ };
1026
1476
  });
1027
1477
  });
1028
1478
  })
1029
1479
  .then(function(data) {
1030
1480
  var traySnapshot = renderMetrics(data.dailyResults);
1031
1481
  renderChart(data.blockResults);
1482
+ renderProjects(data.projectResults);
1483
+ renderAgents(data.dailyResults, data.agents);
1032
1484
  if (window.electronAPI && window.electronAPI.updateTraySnapshot && traySnapshot.totalTokens > 0) {
1033
1485
  window.electronAPI.updateTraySnapshot(traySnapshot).catch(function() {});
1034
1486
  }
@@ -1074,28 +1526,11 @@
1074
1526
  });
1075
1527
 
1076
1528
  document.getElementById('check-updates').addEventListener('click', function() {
1077
- var status = document.getElementById('update-status');
1078
- var button = this;
1079
- status.textContent = 'Checking...';
1080
- button.disabled = true;
1081
-
1082
- var promise = window.electronAPI && window.electronAPI.checkForUpdates
1083
- ? window.electronAPI.checkForUpdates()
1084
- : Promise.resolve({ currentVersion: appInfo.version, latestVersion: appInfo.version, upToDate: true });
1085
-
1086
- promise.then(function(result) {
1087
- if (result.error) {
1088
- status.textContent = 'Unable to reach update service right now.';
1089
- return;
1090
- }
1091
- status.textContent = result.upToDate
1092
- ? 'You are up to date.'
1093
- : 'Version ' + result.latestVersion + ' is available.';
1094
- }).catch(function() {
1095
- status.textContent = 'Unable to reach update service right now.';
1096
- }).finally(function() {
1097
- button.disabled = false;
1098
- });
1529
+ if (this.dataset.action === 'download') {
1530
+ downloadUpdate();
1531
+ } else {
1532
+ checkForUpdates();
1533
+ }
1099
1534
  });
1100
1535
 
1101
1536
  document.getElementById('quit-app').addEventListener('click', function() {
@@ -1104,23 +1539,15 @@
1104
1539
  }
1105
1540
  });
1106
1541
 
1107
- // Agent dropdown toggle
1108
- document.getElementById('agent-dropdown-btn').addEventListener('click', function(e) {
1109
- e.stopPropagation();
1110
- var menu = document.getElementById('agent-dropdown-menu');
1111
- menu.classList.toggle('open');
1112
- });
1113
-
1114
- // Close dropdown on outside click
1115
- document.addEventListener('click', function() {
1116
- var menu = document.getElementById('agent-dropdown-menu');
1117
- if (menu) menu.classList.remove('open');
1118
- });
1119
-
1120
- // Prevent dropdown from closing when clicking inside it
1121
- document.getElementById('agent-dropdown-menu').addEventListener('click', function(e) {
1122
- e.stopPropagation();
1123
- });
1542
+ if (window.electronAPI && window.electronAPI.onUpdateDownloadProgress) {
1543
+ window.electronAPI.onUpdateDownloadProgress(function(progress) {
1544
+ if (!progress) return;
1545
+ var label = progress.percent === null
1546
+ ? 'Downloading...'
1547
+ : 'Downloading ' + progress.percent + '%...';
1548
+ setUpdateUi('downloading', label, updateInfo);
1549
+ });
1550
+ }
1124
1551
 
1125
1552
  // --- Init ---
1126
1553