@zhangferry-dev/tokendash 1.5.0 → 1.6.0

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,168 @@
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
+ background: rgba(255, 255, 255, 0.76);
557
+ border: 1px solid rgba(255, 255, 255, 0.72);
558
+ border-radius: var(--radius-shell);
310
559
  opacity: 0;
311
560
  pointer-events: none;
312
561
  transition: opacity 200ms ease;
313
562
  overflow: hidden;
314
- border-radius: var(--radius-shell);
563
+ backdrop-filter: blur(28px);
315
564
  }
316
565
 
317
566
  .drawer {
318
567
  height: 100%;
319
- display: grid;
320
- grid-template-rows: auto 1fr auto;
321
- gap: 14px;
322
- padding: 14px;
568
+ display: flex;
569
+ flex-direction: column;
570
+ padding: 0 24px;
323
571
  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);
572
+ background: linear-gradient(180deg, rgba(250, 252, 251, 0.85), rgba(244, 247, 245, 0.7));
573
+ border: none;
574
+ box-shadow: none;
328
575
  transform: translateY(10px);
329
576
  opacity: 0;
330
577
  transition: transform 220ms ease, opacity 220ms ease;
@@ -343,36 +590,67 @@
343
590
  .drawer-header {
344
591
  display: flex;
345
592
  align-items: center;
346
- justify-content: flex-start;
593
+ gap: 8px;
594
+ min-height: 48px;
595
+ border-bottom: 1px solid var(--border);
347
596
  }
348
597
 
349
598
  .drawer-back {
350
599
  appearance: none;
351
- border: 1px solid var(--border);
352
- background: rgba(255, 255, 255, 0.94);
353
- color: var(--fg);
600
+ border: 0;
601
+ background: transparent;
602
+ color: var(--muted);
354
603
  display: inline-flex;
355
604
  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;
605
+ justify-content: center;
606
+ width: 22px;
607
+ height: 22px;
608
+ padding: 0;
609
+ margin-left: -4px;
610
+ border-radius: 6px;
611
+ font-size: 0;
363
612
  font-family: var(--font-body);
364
613
  cursor: pointer;
614
+ transition: background-color 160ms ease, color 160ms ease;
615
+ }
616
+
617
+ .drawer-back:hover {
618
+ background: rgba(233, 238, 235, 0.72);
619
+ color: var(--fg);
365
620
  }
366
621
 
367
622
  .drawer-back-arrow {
368
- font-size: 17px;
369
- line-height: 1;
623
+ display: inline-flex;
624
+ align-items: center;
625
+ justify-content: center;
626
+ }
627
+
628
+ .drawer-back-arrow svg {
629
+ width: 15px;
630
+ height: 15px;
631
+ stroke: currentColor;
632
+ fill: none;
633
+ stroke-width: 2;
634
+ stroke-linecap: round;
635
+ stroke-linejoin: round;
636
+ }
637
+
638
+ .drawer-heading {
639
+ min-width: 0;
640
+ }
641
+
642
+ .drawer-title {
643
+ margin: 0;
644
+ font-size: 15px;
645
+ line-height: 1.1;
646
+ font-weight: 700;
370
647
  }
371
648
 
372
649
  .settings-list {
373
650
  display: grid;
374
651
  align-content: start;
375
- gap: 8px;
652
+ gap: 0;
653
+ padding: 7px 0 0;
376
654
  }
377
655
 
378
656
  .settings-row {
@@ -380,10 +658,14 @@
380
658
  align-items: center;
381
659
  justify-content: space-between;
382
660
  gap: 12px;
383
- padding: 12px 14px;
384
- border-radius: 14px;
385
- border: 1px solid var(--border);
386
- background: rgba(250, 252, 251, 0.94);
661
+ min-height: 48px;
662
+ padding: 6px 0;
663
+ border-bottom: 1px solid var(--border);
664
+ background: transparent;
665
+ }
666
+
667
+ .settings-row.has-update {
668
+ border-color: color-mix(in oklch, var(--notice) 30%, var(--border));
387
669
  }
388
670
 
389
671
  .settings-copy {
@@ -392,24 +674,48 @@
392
674
 
393
675
  .settings-label {
394
676
  margin: 0;
395
- font-size: 13px;
396
- font-weight: 600;
677
+ font-size: 12px;
678
+ font-weight: 700;
679
+ }
680
+
681
+ .settings-title-line {
682
+ display: flex;
683
+ align-items: center;
684
+ gap: 6px;
685
+ }
686
+
687
+ .update-pill {
688
+ display: none;
689
+ padding: 2px 6px;
690
+ border-radius: 7px;
691
+ background: color-mix(in oklch, var(--notice) 20%, white);
692
+ color: color-mix(in oklch, var(--notice) 82%, black);
693
+ font-family: var(--font-mono);
694
+ font-size: 9px;
695
+ font-weight: 700;
696
+ letter-spacing: 0;
697
+ text-transform: uppercase;
698
+ }
699
+
700
+ .settings-row.has-update .update-pill {
701
+ display: inline-flex;
397
702
  }
398
703
 
399
704
  .settings-meta {
400
705
  margin: 3px 0 0;
401
- font-size: 11px;
706
+ font-size: 10px;
402
707
  color: var(--muted);
708
+ line-height: 1.25;
403
709
  }
404
710
 
405
711
  .settings-value {
406
712
  display: inline-flex;
407
713
  align-items: center;
408
714
  justify-content: center;
409
- min-height: 28px;
410
- min-width: 58px;
715
+ min-height: 26px;
716
+ min-width: 54px;
411
717
  padding-inline: 10px;
412
- border-radius: 999px;
718
+ border-radius: 9px;
413
719
  border: 1px solid var(--border);
414
720
  background: rgba(255, 255, 255, 0.96);
415
721
  color: var(--muted);
@@ -421,92 +727,26 @@
421
727
  .settings-value.button {
422
728
  cursor: pointer;
423
729
  color: var(--fg);
730
+ transition: background-color 160ms ease, border-color 160ms ease;
424
731
  }
425
732
 
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;
733
+ .settings-value.button:hover {
734
+ background: rgba(247, 249, 248, 0.96);
488
735
  }
489
736
 
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;
737
+ .settings-value.button.primary-update {
738
+ border-color: color-mix(in oklch, var(--notice) 45%, var(--border));
739
+ background: color-mix(in oklch, var(--notice) 18%, white);
740
+ color: color-mix(in oklch, var(--notice) 74%, black);
498
741
  font-weight: 700;
499
- font-size: 10px;
500
742
  }
501
743
 
502
744
  .settings-divider {
503
- height: 1px;
504
- background: var(--border);
505
- margin: 4px 0;
745
+ height: 8px;
506
746
  }
507
747
 
508
748
  .settings-quit-row {
509
- border-color: rgba(192, 57, 43, 0.15);
749
+ border-bottom: 0;
510
750
  }
511
751
 
512
752
  .settings-quit-btn {
@@ -514,19 +754,21 @@
514
754
  }
515
755
 
516
756
  .settings-version {
517
- text-align: center;
757
+ margin-top: auto;
758
+ border-top: 1px solid var(--border);
759
+ text-align: left;
518
760
  font-family: var(--font-mono);
519
- font-size: 11px;
761
+ font-size: 10px;
520
762
  color: var(--muted);
521
- letter-spacing: 0.06em;
522
- text-transform: uppercase;
523
- padding-block: 2px 6px;
763
+ letter-spacing: 0;
764
+ text-transform: none;
765
+ padding-block: 10px 13px;
524
766
  }
525
767
 
526
768
  .switch {
527
769
  position: relative;
528
- width: 44px;
529
- height: 26px;
770
+ width: 42px;
771
+ height: 24px;
530
772
  border-radius: 999px;
531
773
  border: 1px solid color-mix(in srgb, #000 7%, transparent);
532
774
  background: rgba(228, 231, 229, 0.9);
@@ -539,8 +781,8 @@
539
781
  position: absolute;
540
782
  inset-block-start: 2px;
541
783
  inset-inline-start: 2px;
542
- width: 20px;
543
- height: 20px;
784
+ width: 18px;
785
+ height: 18px;
544
786
  border-radius: 50%;
545
787
  background: white;
546
788
  box-shadow: 0 1px 3px rgba(20, 28, 38, 0.18);
@@ -559,7 +801,8 @@
559
801
  <body>
560
802
  <main class="shell" aria-label="Token dashboard menu" data-view="main">
561
803
  <header class="toolbar">
562
- <p class="toolbar-label">Today <span id="date"></span></p>
804
+ <p class="toolbar-label"><strong>Today</strong> · <span id="date"></span></p>
805
+ <div class="cache-pill" id="cache-pill">cache --</div>
563
806
  </header>
564
807
 
565
808
  <div class="error-banner" id="error-banner">
@@ -568,22 +811,27 @@
568
811
 
569
812
  <section class="content" id="content">
570
813
  <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>
814
+ <div class="summary-top">
815
+ <div>
816
+ <p class="card-label">Total</p>
817
+ <p class="card-value" id="total-tokens"><span class="skeleton"></span></p>
818
+ </div>
819
+ <p class="cost-value" id="total-cost">$--</p>
820
+ </div>
821
+ <div class="mini-metrics" aria-label="Token breakdown">
822
+ <div class="mini-metric">
823
+ <p class="mini-label">In</p>
824
+ <p class="mini-value input" id="input"><span class="skeleton"></span></p>
825
+ </div>
826
+ <div class="mini-metric">
827
+ <p class="mini-label">Out</p>
828
+ <p class="mini-value output" id="output"><span class="skeleton"></span></p>
829
+ </div>
830
+ <div class="mini-metric">
831
+ <p class="mini-label">Cached</p>
832
+ <p class="mini-value cached" id="cached"><span class="skeleton"></span></p>
833
+ </div>
834
+ </div>
587
835
  </section>
588
836
 
589
837
  <section class="chart-panel" aria-label="Hourly usage">
@@ -594,17 +842,41 @@
594
842
  <div class="y-axis" id="y-axis" aria-hidden="true"></div>
595
843
  <div class="plot" id="plot">
596
844
  <div class="chart-empty">
597
- <div class="chart-empty-icon">&#x1F4CA;</div>
598
845
  <div class="chart-empty-title">No usage yet</div>
599
846
  <div>Start a session to see your hourly breakdown.</div>
600
847
  </div>
601
848
  </div>
849
+ <div class="x-axis" id="x-axis" aria-hidden="true"></div>
850
+ </div>
851
+ </section>
852
+
853
+ <section class="usage-panel" aria-label="Projects">
854
+ <div class="usage-header">
855
+ <span class="usage-title">Projects</span>
856
+ <span class="usage-header-stat">in</span>
857
+ <span class="usage-header-stat">out</span>
858
+ <span class="usage-header-stat">cached</span>
859
+ </div>
860
+ <div class="usage-list" id="projects-list">
861
+ <div class="usage-empty">Loading projects...</div>
862
+ </div>
863
+ </section>
864
+
865
+ <section class="usage-panel" aria-label="Agents">
866
+ <div class="usage-header">
867
+ <span class="usage-title">Agents</span>
868
+ <span class="usage-header-stat">in</span>
869
+ <span class="usage-header-stat">out</span>
870
+ <span class="usage-header-stat">cached</span>
871
+ </div>
872
+ <div class="usage-list" id="agents-list">
873
+ <div class="usage-empty">Loading agents...</div>
602
874
  </div>
603
875
  </section>
604
876
 
605
877
  <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>
878
+ <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>
879
+ <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
880
  </section>
609
881
  </section>
610
882
 
@@ -612,14 +884,18 @@
612
884
  <section class="drawer" aria-label="Settings drawer">
613
885
  <div class="drawer-header">
614
886
  <button class="drawer-back" type="button" aria-label="Back" id="close-settings">
615
- <span class="drawer-back-arrow">&lsaquo;</span>
887
+ <span class="drawer-back-arrow"><svg viewBox="0 0 24 24"><path d="m15 18-6-6 6-6"/></svg></span>
616
888
  </button>
889
+ <p class="drawer-title">Settings</p>
617
890
  </div>
618
891
 
619
892
  <div class="settings-list">
620
- <article class="settings-row">
893
+ <article class="settings-row" id="update-row">
621
894
  <div class="settings-copy">
622
- <p class="settings-label">Check for updates</p>
895
+ <div class="settings-title-line">
896
+ <p class="settings-label">Check for updates</p>
897
+ <span class="update-pill" id="update-pill">New</span>
898
+ </div>
623
899
  <p class="settings-meta" id="update-status">Check whether a newer build is available.</p>
624
900
  </div>
625
901
  <button class="settings-value button" type="button" id="check-updates">Check</button>
@@ -633,17 +909,6 @@
633
909
  <button class="switch" type="button" id="launch-at-login" role="switch" aria-checked="false" aria-label="Launch at login"></button>
634
910
  </article>
635
911
 
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
912
  <div class="settings-divider"></div>
648
913
 
649
914
  <article class="settings-row settings-quit-row">
@@ -665,16 +930,14 @@
665
930
  var port = window.location.port || '3456';
666
931
  var apiBase = 'http://localhost:' + port;
667
932
  var appInfo = { version: '--', launchAtLogin: false };
668
- var availableAgents = [];
669
- var selectedAgents = [];
670
- var isFirstLoad = true;
933
+ var updateInfo = null;
671
934
 
672
935
  // --- Helpers ---
673
936
 
674
937
  function formatNumber(n) {
675
938
  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';
939
+ if (n >= 1e6) return trimTrailingZero((n / 1e6).toFixed(n >= 10e6 ? 1 : 2)) + 'M';
940
+ if (n >= 1e3) return trimTrailingZero((n / 1e3).toFixed(n >= 100e3 ? 0 : 1)) + 'K';
678
941
  return String(Math.round(n));
679
942
  }
680
943
 
@@ -682,6 +945,32 @@
682
945
  return (Number.isFinite(n) ? n : 0).toFixed(1) + '%';
683
946
  }
684
947
 
948
+ function formatCost(n) {
949
+ var value = Number(n) || 0;
950
+ if (value >= 100) return '$' + Math.round(value);
951
+ if (value >= 10) return '$' + value.toFixed(1);
952
+ return '$' + value.toFixed(2);
953
+ }
954
+
955
+ function trimTrailingZero(value) {
956
+ return String(value).replace(/\.0+$/, '').replace(/(\.\d*[1-9])0+$/, '$1');
957
+ }
958
+
959
+ function formatProjectName(path) {
960
+ var value = String(path || 'Unknown');
961
+ var parts = value.split('/').filter(Boolean);
962
+ return parts[parts.length - 1] || value;
963
+ }
964
+
965
+ function escapeHtml(value) {
966
+ return String(value)
967
+ .replace(/&/g, '&amp;')
968
+ .replace(/</g, '&lt;')
969
+ .replace(/>/g, '&gt;')
970
+ .replace(/"/g, '&quot;')
971
+ .replace(/'/g, '&#39;');
972
+ }
973
+
685
974
  function getTodayString() {
686
975
  var d = new Date();
687
976
  return d.getFullYear() + '-' + String(d.getMonth() + 1).padStart(2, '0') + '-' + String(d.getDate()).padStart(2, '0');
@@ -700,25 +989,17 @@
700
989
  // --- Dynamic chart hours based on current time ---
701
990
 
702
991
  function getChartHours() {
703
- var now = new Date();
704
- var currentHour = now.getHours();
705
992
  var hours = [];
706
- for (var h = 0; h <= currentHour; h += 2) {
993
+ for (var h = 0; h < 24; h += 1) {
707
994
  hours.push(h);
708
995
  }
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
996
  return hours;
716
997
  }
717
998
 
718
999
  // --- Loading & Error states ---
719
1000
 
720
1001
  function showSkeletons() {
721
- ['total-tokens', 'input', 'output', 'cache-rate'].forEach(function(id) {
1002
+ ['total-tokens', 'input', 'output', 'cached'].forEach(function(id) {
722
1003
  var el = document.getElementById(id);
723
1004
  if (el && !el.querySelector('.skeleton')) return; // already has data
724
1005
  if (el) el.innerHTML = '<span class="skeleton"></span>';
@@ -762,12 +1043,15 @@
762
1043
  setCardValue('total-tokens', formatNumber(totalTokens), totalTokens === 0);
763
1044
  setCardValue('input', formatNumber(totalInput), totalInput === 0);
764
1045
  setCardValue('output', formatNumber(totalOutput), totalOutput === 0);
765
- setCardValue('cache-rate', formatPercent(cacheRate), cacheRate === 0);
1046
+ setCardValue('cached', formatNumber(totalCacheRead), totalCacheRead === 0);
1047
+ document.getElementById('total-cost').textContent = formatCost(totalCost);
1048
+ applyMetricFit(document.getElementById('total-cost'), formatCost(totalCost));
1049
+ document.getElementById('cache-pill').textContent = 'cache ' + formatPercent(cacheRate);
766
1050
 
767
1051
  return {
768
1052
  today: todayStr,
769
- agentKey: selectedAgents.slice().sort().join(','),
770
1053
  totalTokens: totalTokens,
1054
+ totalInput: totalInput,
771
1055
  totalCost: totalCost,
772
1056
  totalCacheRead: totalCacheRead
773
1057
  };
@@ -777,6 +1061,24 @@
777
1061
  var el = document.getElementById(id);
778
1062
  el.textContent = text;
779
1063
  el.classList.toggle('empty', isEmpty);
1064
+ applyMetricFit(el, text);
1065
+ }
1066
+
1067
+ function applyMetricFit(el, text) {
1068
+ if (!el) return;
1069
+ var length = String(text || '').length;
1070
+
1071
+ if (el.id === 'total-tokens') {
1072
+ el.style.fontSize = length >= 8 ? '27px' : length >= 6 ? '29px' : '32px';
1073
+ return;
1074
+ }
1075
+
1076
+ if (el.id === 'total-cost') {
1077
+ el.style.fontSize = length >= 8 ? '21px' : length >= 6 ? '23px' : '25px';
1078
+ return;
1079
+ }
1080
+
1081
+ el.style.fontSize = length >= 8 ? '13px' : '15px';
780
1082
  }
781
1083
 
782
1084
  // --- Chart rendering ---
@@ -807,10 +1109,12 @@
807
1109
 
808
1110
  var plot = document.getElementById('plot');
809
1111
  var axis = document.getElementById('y-axis');
1112
+ var xAxis = document.getElementById('x-axis');
810
1113
 
811
1114
  // Empty state
812
1115
  if (maxValue === 0) {
813
1116
  axis.innerHTML = '';
1117
+ xAxis.innerHTML = '';
814
1118
  plot.innerHTML = '';
815
1119
  plot.style.gridTemplateColumns = '';
816
1120
  plot.innerHTML = '<div class="chart-empty">' +
@@ -820,55 +1124,77 @@
820
1124
  return;
821
1125
  }
822
1126
 
823
- var axisInfo = niceStep(maxValue);
824
- renderAxis(maxValue);
1127
+ var axisInfo = buildAxis(maxValue);
1128
+ renderAxis(axisInfo);
825
1129
  var maxEntry = selected.reduce(function(max, entry) {
826
1130
  return !max || entry.value > max.value ? entry : max;
827
1131
  }, null);
828
- renderBars(selected, axisInfo.top, maxEntry);
1132
+ renderBars(selected, axisInfo, maxEntry);
829
1133
  }
830
1134
 
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;
1135
+ function niceIncrement(rawStep) {
1136
+ if (rawStep <= 0) return 1;
835
1137
  var exp = Math.floor(Math.log10(rawStep));
836
1138
  var base = Math.pow(10, exp);
837
1139
  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 };
1140
+ if (frac <= 1) return base;
1141
+ if (frac <= 2) return 2 * base;
1142
+ if (frac <= 2.5) return 2.5 * base;
1143
+ if (frac <= 5) return 5 * base;
1144
+ return 10 * base;
1145
+ }
1146
+
1147
+ function buildAxis(value) {
1148
+ if (value <= 0) return { top: 4, ticks: [4, 3, 2, 1, 0] };
1149
+ var step = niceIncrement(value / 4);
1150
+ var top = step * Math.ceil(value / step);
1151
+ if (top / value > 1.35 && top > step) top -= step;
1152
+ if (top < value) top += step;
1153
+ var segments = Math.max(3, Math.min(5, Math.round(top / step)));
1154
+ top = step * segments;
1155
+
1156
+ var ticks = [];
1157
+ for (var i = segments; i >= 0; i--) {
1158
+ ticks.push(step * i);
1159
+ }
1160
+ return { top: top, ticks: ticks };
842
1161
  }
843
1162
 
844
- function renderAxis(axisMax) {
1163
+ function renderAxis(info) {
845
1164
  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('');
1165
+ axis.innerHTML = info.ticks.map(function(tick) {
1166
+ var top = ((info.top - tick) / info.top) * 100;
1167
+ return '<span style="top: ' + top + '%">' + formatNumber(tick) + '</span>';
1168
+ }).join('');
853
1169
  }
854
1170
 
855
- function renderBars(selected, maxValue, maxEntry) {
1171
+ function renderBars(selected, axisInfo, maxEntry) {
856
1172
  var plot = document.getElementById('plot');
1173
+ var xAxis = document.getElementById('x-axis');
857
1174
  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';
1175
+ plot.style.gridTemplateColumns = 'repeat(24, minmax(0, 1fr))';
1176
+ xAxis.innerHTML = '';
1177
+ xAxis.style.gridTemplateColumns = 'repeat(24, minmax(0, 1fr))';
1178
+ renderGrid(plot, axisInfo);
1179
+
1180
+ // Dynamic gap and bar width based on bar count
1181
+ var count = selected.filter(function(entry) { return entry.value > 0; }).length;
1182
+ var gap = '2px';
1183
+ var barMaxWidth = count > 10 ? '7px' : count > 6 ? '9px' : '12px';
862
1184
  plot.style.gap = gap;
1185
+ xAxis.style.gap = gap;
863
1186
 
864
1187
  selected.forEach(function(entry) {
865
- var height = entry.value > 0 ? Math.max(8, (entry.value / maxValue) * 100) : 3;
1188
+ if (entry.value <= 0) return;
1189
+ var height = entry.value > 0 ? Math.max(8, (entry.value / axisInfo.top) * 100) : 0;
866
1190
  var wrap = document.createElement('div');
867
1191
  wrap.className = 'bar-wrap';
1192
+ wrap.style.gridColumn = String(entry.hour + 1);
868
1193
  wrap.style.setProperty('--height', height + '%');
869
1194
 
870
1195
  var bar = document.createElement('div');
871
1196
  bar.className = 'bar';
1197
+ bar.style.maxWidth = barMaxWidth + 'px';
872
1198
  if (entry.value === 0) {
873
1199
  bar.classList.add('zero');
874
1200
  } else if (maxEntry && entry.hour === maxEntry.hour && entry.value === maxEntry.value) {
@@ -884,12 +1210,148 @@
884
1210
  wrap.appendChild(tip);
885
1211
  }
886
1212
 
1213
+ plot.appendChild(wrap);
1214
+ });
1215
+
1216
+ renderActiveHourLabels(xAxis, selected, maxEntry);
1217
+ }
1218
+
1219
+ function renderGrid(plot, info) {
1220
+ var grid = document.createElement('div');
1221
+ grid.className = 'plot-grid';
1222
+
1223
+ info.ticks.forEach(function(tick) {
1224
+ var line = document.createElement('span');
1225
+ line.className = 'plot-grid-line';
1226
+ line.style.top = (((info.top - tick) / info.top) * 100) + '%';
1227
+ grid.appendChild(line);
1228
+ });
1229
+
1230
+ plot.appendChild(grid);
1231
+ }
1232
+
1233
+ function renderActiveHourLabels(xAxis, selected, maxEntry) {
1234
+ var active = selected.filter(function(entry) { return entry.value > 0; });
1235
+ var labeledHours = [];
1236
+
1237
+ active.forEach(function(entry) {
1238
+ var isPeak = maxEntry && entry.hour === maxEntry.hour;
1239
+ var hasRoom = labeledHours.every(function(hour) {
1240
+ return Math.abs(entry.hour - hour) >= 3;
1241
+ });
1242
+ var shouldLabel = isPeak || hasRoom;
1243
+
1244
+ if (shouldLabel) labeledHours.push(entry.hour);
1245
+
887
1246
  var label = document.createElement('span');
888
- label.className = 'bar-label';
889
- label.textContent = String(entry.hour).padStart(2, '0');
890
- wrap.appendChild(label);
1247
+ label.className = 'x-axis-label' + (shouldLabel ? '' : ' is-unlabeled');
1248
+ label.style.gridColumn = String(entry.hour + 1);
1249
+ label.textContent = String(entry.hour).padStart(2, '0') + ':00';
1250
+ xAxis.appendChild(label);
1251
+ });
1252
+ }
891
1253
 
892
- plot.appendChild(wrap);
1254
+ function renderProjects(allProjects) {
1255
+ var todayStr = getTodayString();
1256
+ var totals = {};
1257
+
1258
+ (allProjects || []).forEach(function(data) {
1259
+ var projects = data && data.projects;
1260
+ if (!projects) return;
1261
+
1262
+ Object.keys(projects).forEach(function(projectPath) {
1263
+ var todayEntries = (projects[projectPath] || []).filter(function(entry) {
1264
+ return entry && entry.date === todayStr;
1265
+ });
1266
+ if (todayEntries.length === 0) return;
1267
+
1268
+ if (!totals[projectPath]) {
1269
+ totals[projectPath] = { path: projectPath, input: 0, output: 0, cached: 0, total: 0 };
1270
+ }
1271
+ todayEntries.forEach(function(entry) {
1272
+ totals[projectPath].input += entry.inputTokens || 0;
1273
+ totals[projectPath].output += entry.outputTokens || 0;
1274
+ totals[projectPath].cached += entry.cacheReadTokens || 0;
1275
+ totals[projectPath].total += entry.totalTokens || 0;
1276
+ });
1277
+ });
1278
+ });
1279
+
1280
+ var rows = Object.keys(totals).map(function(key) { return totals[key]; })
1281
+ .sort(function(a, b) { return b.total - a.total; })
1282
+ .slice(0, 4);
1283
+ var list = document.getElementById('projects-list');
1284
+
1285
+ if (rows.length === 0) {
1286
+ list.innerHTML = '<div class="usage-empty">No project usage today.</div>';
1287
+ return;
1288
+ }
1289
+
1290
+ list.innerHTML = '';
1291
+ rows.forEach(function(row) {
1292
+ var item = document.createElement('div');
1293
+ item.className = 'usage-row';
1294
+ item.innerHTML =
1295
+ '<span class="usage-name" title="' + escapeHtml(row.path) + '">' + escapeHtml(formatProjectName(row.path)) + '</span>' +
1296
+ '<span class="usage-stat input">' + formatNumber(row.input) + '</span>' +
1297
+ '<span class="usage-stat output">' + formatNumber(row.output) + '</span>' +
1298
+ '<span class="usage-stat cached">' + formatNumber(row.cached) + '</span>';
1299
+ list.appendChild(item);
1300
+ });
1301
+ }
1302
+
1303
+ function formatAgentName(agent) {
1304
+ var names = {
1305
+ claude: 'Claude Code',
1306
+ codex: 'Codex',
1307
+ openclaw: 'OpenClaw',
1308
+ opencode: 'OpenCode'
1309
+ };
1310
+ return names[agent] || agent;
1311
+ }
1312
+
1313
+ function renderAgents(allDaily, agents) {
1314
+ var todayStr = getTodayString();
1315
+ var rows = (agents || []).map(function(agent, index) {
1316
+ var data = (allDaily || [])[index];
1317
+ var entry = null;
1318
+ if (data && Array.isArray(data.daily)) {
1319
+ entry = data.daily.find(function(day) { return day && day.date === todayStr; });
1320
+ }
1321
+ var input = entry ? (entry.inputTokens || 0) : 0;
1322
+ var output = entry ? (entry.outputTokens || 0) : 0;
1323
+ var cached = entry ? (entry.cacheReadTokens || 0) : 0;
1324
+ return {
1325
+ name: formatAgentName(agent),
1326
+ input: input,
1327
+ output: output,
1328
+ cached: cached,
1329
+ total: entry ? (entry.totalTokens || input + output + cached) : 0
1330
+ };
1331
+ }).filter(function(row) {
1332
+ return row.total > 0 || row.input > 0 || row.output > 0;
1333
+ }).sort(function(a, b) {
1334
+ return b.total - a.total;
1335
+ });
1336
+
1337
+ var list = document.getElementById('agents-list');
1338
+ if (!list) return;
1339
+
1340
+ if (rows.length === 0) {
1341
+ list.innerHTML = '<div class="usage-empty">No agent usage today.</div>';
1342
+ return;
1343
+ }
1344
+
1345
+ list.innerHTML = '';
1346
+ rows.forEach(function(row) {
1347
+ var item = document.createElement('div');
1348
+ item.className = 'usage-row';
1349
+ item.innerHTML =
1350
+ '<span class="usage-name" title="' + escapeHtml(row.name) + '">' + escapeHtml(row.name) + '</span>' +
1351
+ '<span class="usage-stat input">' + formatNumber(row.input) + '</span>' +
1352
+ '<span class="usage-stat output">' + formatNumber(row.output) + '</span>' +
1353
+ '<span class="usage-stat cached">' + formatNumber(row.cached) + '</span>';
1354
+ list.appendChild(item);
893
1355
  });
894
1356
  }
895
1357
 
@@ -914,85 +1376,74 @@
914
1376
  return Promise.resolve();
915
1377
  }
916
1378
 
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';
1379
+ function setUpdateUi(state, message, nextInfo) {
1380
+ var row = document.getElementById('update-row');
1381
+ var button = document.getElementById('check-updates');
1382
+ var status = document.getElementById('update-status');
1383
+ updateInfo = nextInfo || updateInfo;
1384
+
1385
+ row.classList.toggle('has-update', state === 'available' || state === 'ready');
1386
+ button.classList.toggle('primary-update', state === 'available');
1387
+ button.disabled = state === 'checking' || state === 'downloading';
1388
+ status.textContent = message;
1389
+
1390
+ if (state === 'available') {
1391
+ button.textContent = 'Download';
1392
+ button.dataset.action = 'download';
1393
+ } else if (state === 'ready') {
1394
+ button.textContent = 'Done';
1395
+ button.dataset.action = 'check';
941
1396
  } else {
942
- btn.textContent = selectedAgents.length + ' of ' + availableAgents.length;
1397
+ button.textContent = state === 'checking' ? 'Checking' : state === 'downloading' ? 'Downloading' : 'Check';
1398
+ button.dataset.action = 'check';
943
1399
  }
944
1400
  }
945
1401
 
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
- });
1402
+ function checkForUpdates() {
1403
+ setUpdateUi('checking', 'Checking GitHub Releases...', null);
976
1404
 
977
- updateAgentDropdownLabel();
1405
+ var promise = window.electronAPI && window.electronAPI.checkForUpdates
1406
+ ? window.electronAPI.checkForUpdates()
1407
+ : Promise.resolve({ currentVersion: appInfo.version, latestVersion: appInfo.version, upToDate: true });
978
1408
 
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' : '';
1409
+ return promise.then(function(result) {
1410
+ if (result.error) {
1411
+ setUpdateUi('idle', 'Unable to reach update service right now.', result);
1412
+ return;
1413
+ }
1414
+ if (result.upToDate) {
1415
+ setUpdateUi('idle', 'You are up to date on version ' + result.currentVersion + '.', result);
1416
+ return;
1417
+ }
1418
+ if (!result.asset || !result.asset.url) {
1419
+ setUpdateUi('idle', 'Version ' + result.latestVersion + ' is available, but no macOS DMG was attached.', result);
1420
+ return;
1421
+ }
1422
+ setUpdateUi('available', 'Version ' + result.latestVersion + ' is available.', result);
1423
+ }).catch(function() {
1424
+ setUpdateUi('idle', 'Unable to reach update service right now.', null);
1425
+ });
982
1426
  }
983
1427
 
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
1428
+ function downloadUpdate() {
1429
+ if (!updateInfo || !updateInfo.asset) {
1430
+ return checkForUpdates();
993
1431
  }
994
- renderAgentDropdown();
995
- saveSelectedAgents();
1432
+ if (!window.electronAPI || !window.electronAPI.downloadUpdate) {
1433
+ setUpdateUi('idle', 'Download is only available in the macOS app.', updateInfo);
1434
+ return Promise.resolve();
1435
+ }
1436
+
1437
+ setUpdateUi('downloading', 'Preparing download...', updateInfo);
1438
+ return window.electronAPI.downloadUpdate(updateInfo).then(function(result) {
1439
+ if (!result || !result.ok) {
1440
+ setUpdateUi('available', result && result.error ? result.error : 'Download failed.', updateInfo);
1441
+ return;
1442
+ }
1443
+ setUpdateUi('ready', 'Downloaded. The installer disk image has been opened.', updateInfo);
1444
+ }).catch(function() {
1445
+ setUpdateUi('available', 'Download failed. Please try again.', updateInfo);
1446
+ });
996
1447
  }
997
1448
 
998
1449
  // --- Data fetching ---
@@ -1005,30 +1456,30 @@
1005
1456
  .then(function(agentData) {
1006
1457
  var agents = (agentData && agentData.available) ? agentData.available : ['claude'];
1007
1458
 
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) {
1459
+ return Promise.all(agents.map(function(agent) {
1018
1460
  return fetchJson(apiBase + '/api/daily?agent=' + agent).catch(function() { return null; });
1019
1461
  })).then(function(dailyResults) {
1020
- // Also fetch blocks for all selected agents
1021
- var blockPromises = fetchAgents.map(function(agent) {
1462
+ var blockPromises = agents.map(function(agent) {
1022
1463
  return fetchJson(apiBase + '/api/blocks?agent=' + agent).catch(function() { return null; });
1023
1464
  });
1024
- return Promise.all(blockPromises).then(function(blockResults) {
1025
- return { dailyResults: dailyResults, blockResults: blockResults };
1465
+ var projectPromises = agents.map(function(agent) {
1466
+ return fetchJson(apiBase + '/api/projects?agent=' + agent).catch(function() { return null; });
1467
+ });
1468
+ return Promise.all([Promise.all(blockPromises), Promise.all(projectPromises)]).then(function(results) {
1469
+ return {
1470
+ agents: agents,
1471
+ dailyResults: dailyResults,
1472
+ blockResults: results[0],
1473
+ projectResults: results[1]
1474
+ };
1026
1475
  });
1027
1476
  });
1028
1477
  })
1029
1478
  .then(function(data) {
1030
1479
  var traySnapshot = renderMetrics(data.dailyResults);
1031
1480
  renderChart(data.blockResults);
1481
+ renderProjects(data.projectResults);
1482
+ renderAgents(data.dailyResults, data.agents);
1032
1483
  if (window.electronAPI && window.electronAPI.updateTraySnapshot && traySnapshot.totalTokens > 0) {
1033
1484
  window.electronAPI.updateTraySnapshot(traySnapshot).catch(function() {});
1034
1485
  }
@@ -1074,28 +1525,11 @@
1074
1525
  });
1075
1526
 
1076
1527
  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
- });
1528
+ if (this.dataset.action === 'download') {
1529
+ downloadUpdate();
1530
+ } else {
1531
+ checkForUpdates();
1532
+ }
1099
1533
  });
1100
1534
 
1101
1535
  document.getElementById('quit-app').addEventListener('click', function() {
@@ -1104,23 +1538,15 @@
1104
1538
  }
1105
1539
  });
1106
1540
 
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
- });
1541
+ if (window.electronAPI && window.electronAPI.onUpdateDownloadProgress) {
1542
+ window.electronAPI.onUpdateDownloadProgress(function(progress) {
1543
+ if (!progress) return;
1544
+ var label = progress.percent === null
1545
+ ? 'Downloading...'
1546
+ : 'Downloading ' + progress.percent + '%...';
1547
+ setUpdateUi('downloading', label, updateInfo);
1548
+ });
1549
+ }
1124
1550
 
1125
1551
  // --- Init ---
1126
1552