@hk_net/pi-usage-bars 0.4.5 → 0.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.
- package/CHANGELOG.md +29 -0
- package/LICENSE +21 -21
- package/README.md +145 -129
- package/docs/provider-research.md +4 -4
- package/docs/releasing.md +104 -84
- package/extensions/usage-bars/core.ts +1341 -1274
- package/extensions/usage-bars/index.ts +781 -672
- package/package.json +75 -74
|
@@ -1,672 +1,781 @@
|
|
|
1
|
-
/** Quota, balance, and spend indicators for providers supported by current Pi releases. */
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
DynamicBorder,
|
|
5
|
-
type ExtensionAPI,
|
|
6
|
-
type ExtensionContext,
|
|
7
|
-
type KeybindingsManager,
|
|
8
|
-
type Theme,
|
|
9
|
-
} from "@earendil-works/pi-coding-agent";
|
|
10
|
-
import {
|
|
11
|
-
Container,
|
|
12
|
-
Input,
|
|
13
|
-
Spacer,
|
|
14
|
-
Text,
|
|
15
|
-
type Focusable,
|
|
16
|
-
type TUI,
|
|
17
|
-
} from "@earendil-works/pi-tui";
|
|
18
|
-
import {
|
|
19
|
-
clampPercent,
|
|
20
|
-
colorForPercent,
|
|
21
|
-
detectProvider,
|
|
22
|
-
fetchAllUsages,
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
type
|
|
35
|
-
type
|
|
36
|
-
type
|
|
37
|
-
type
|
|
38
|
-
type
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
"
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
private readonly
|
|
119
|
-
private readonly
|
|
120
|
-
private readonly
|
|
121
|
-
private readonly
|
|
122
|
-
private readonly
|
|
123
|
-
private
|
|
124
|
-
private
|
|
125
|
-
private
|
|
126
|
-
private
|
|
127
|
-
private
|
|
128
|
-
private
|
|
129
|
-
private
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
this.
|
|
157
|
-
this.
|
|
158
|
-
this.
|
|
159
|
-
this.
|
|
160
|
-
this.
|
|
161
|
-
|
|
162
|
-
this.addChild(
|
|
163
|
-
this.addChild(new Spacer(1));
|
|
164
|
-
this.
|
|
165
|
-
this.addChild(this.
|
|
166
|
-
this.addChild(new Spacer(1));
|
|
167
|
-
this.
|
|
168
|
-
|
|
169
|
-
this.
|
|
170
|
-
this.
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
this.
|
|
184
|
-
this.
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
this.filteredItems =
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
)
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
)
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
this.
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
this.
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
return;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
const
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
if (
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
const
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
if (
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
if (
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
|
|
1
|
+
/** Quota, balance, and spend indicators for providers supported by current Pi releases. */
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
DynamicBorder,
|
|
5
|
+
type ExtensionAPI,
|
|
6
|
+
type ExtensionContext,
|
|
7
|
+
type KeybindingsManager,
|
|
8
|
+
type Theme,
|
|
9
|
+
} from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import {
|
|
11
|
+
Container,
|
|
12
|
+
Input,
|
|
13
|
+
Spacer,
|
|
14
|
+
Text,
|
|
15
|
+
type Focusable,
|
|
16
|
+
type TUI,
|
|
17
|
+
} from "@earendil-works/pi-tui";
|
|
18
|
+
import {
|
|
19
|
+
clampPercent,
|
|
20
|
+
colorForPercent,
|
|
21
|
+
detectProvider,
|
|
22
|
+
fetchAllUsages,
|
|
23
|
+
fetchBasetenUsage,
|
|
24
|
+
fetchClaudeUsageWithFallback,
|
|
25
|
+
fetchCodexUsage,
|
|
26
|
+
fetchDeepSeekBalance,
|
|
27
|
+
fetchKimiUsage,
|
|
28
|
+
fetchMiniMaxUsage,
|
|
29
|
+
fetchMoonshotBalance,
|
|
30
|
+
fetchOpenRouterUsage,
|
|
31
|
+
fetchZaiUsage,
|
|
32
|
+
providerToPiProviderId,
|
|
33
|
+
resolveUsageEndpoints,
|
|
34
|
+
type AccountBalance,
|
|
35
|
+
type AccountSpend,
|
|
36
|
+
type ProviderKey,
|
|
37
|
+
type UsageByProvider,
|
|
38
|
+
type UsageData,
|
|
39
|
+
type UsageTokens,
|
|
40
|
+
} from "./core";
|
|
41
|
+
|
|
42
|
+
const POLL_INTERVAL_MS = 2 * 60 * 1000;
|
|
43
|
+
const EXTENSION_ID = "@hk_net/pi-usage-bars";
|
|
44
|
+
const STATUS_KEY = EXTENSION_ID;
|
|
45
|
+
const USAGE_UPDATE_EVENT = `${EXTENSION_ID}:update`;
|
|
46
|
+
const PROVIDERS: readonly ProviderKey[] = [
|
|
47
|
+
"codex",
|
|
48
|
+
"claude",
|
|
49
|
+
"zai",
|
|
50
|
+
"zai-cn",
|
|
51
|
+
"kimi",
|
|
52
|
+
"minimax",
|
|
53
|
+
"minimax-cn",
|
|
54
|
+
"openrouter",
|
|
55
|
+
"deepseek",
|
|
56
|
+
"moonshot",
|
|
57
|
+
"moonshot-cn",
|
|
58
|
+
"baseten",
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
const PROVIDER_LABELS: Record<ProviderKey, string> = {
|
|
62
|
+
codex: "Codex",
|
|
63
|
+
claude: "Claude",
|
|
64
|
+
zai: "ZAI Coding Plan (Global)",
|
|
65
|
+
"zai-cn": "ZAI Coding Plan (China)",
|
|
66
|
+
kimi: "Kimi For Coding",
|
|
67
|
+
minimax: "MiniMax Coding Plan (Global)",
|
|
68
|
+
"minimax-cn": "MiniMax Coding Plan (China)",
|
|
69
|
+
openrouter: "OpenRouter",
|
|
70
|
+
deepseek: "DeepSeek",
|
|
71
|
+
moonshot: "Moonshot/Kimi API (Global)",
|
|
72
|
+
"moonshot-cn": "Moonshot/Kimi API (China)",
|
|
73
|
+
baseten: "Baseten",
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
function formatFinancialAmount(amount: number, unit: string): string {
|
|
77
|
+
if (/^[A-Z]{3}$/.test(unit)) {
|
|
78
|
+
return new Intl.NumberFormat("en-US", {
|
|
79
|
+
style: "currency",
|
|
80
|
+
currency: unit,
|
|
81
|
+
minimumFractionDigits: 2,
|
|
82
|
+
maximumFractionDigits: 5,
|
|
83
|
+
}).format(amount);
|
|
84
|
+
}
|
|
85
|
+
const formatted = new Intl.NumberFormat("en-US", { maximumFractionDigits: 2 }).format(amount);
|
|
86
|
+
return `${formatted} ${unit}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function formatAccountBalance(balance: AccountBalance): string {
|
|
90
|
+
return `${balance.label} · ${formatFinancialAmount(balance.amount, balance.unit)}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function formatAccountSpend(spend: AccountSpend): string {
|
|
94
|
+
const values = [
|
|
95
|
+
spend.daily === undefined ? undefined : `today ${formatFinancialAmount(spend.daily, spend.unit)}`,
|
|
96
|
+
spend.weekly === undefined ? undefined : `week ${formatFinancialAmount(spend.weekly, spend.unit)}`,
|
|
97
|
+
spend.monthly === undefined ? undefined : `month ${formatFinancialAmount(spend.monthly, spend.unit)}`,
|
|
98
|
+
].filter((value): value is string => Boolean(value));
|
|
99
|
+
if (values.length === 0 && spend.lifetime !== undefined) {
|
|
100
|
+
values.push(`lifetime ${formatFinancialAmount(spend.lifetime, spend.unit)}`);
|
|
101
|
+
}
|
|
102
|
+
return `Spent · ${values.join(" · ")}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
interface SubscriptionItem {
|
|
106
|
+
name: string;
|
|
107
|
+
provider: ProviderKey;
|
|
108
|
+
data: UsageData;
|
|
109
|
+
isActive: boolean;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
interface CredentialResolution {
|
|
113
|
+
token?: string;
|
|
114
|
+
error?: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
class UsageSelectorComponent extends Container implements Focusable {
|
|
118
|
+
private readonly searchInput: Input;
|
|
119
|
+
private readonly listContainer: Container;
|
|
120
|
+
private readonly hintText: Text;
|
|
121
|
+
private readonly requestController = new AbortController();
|
|
122
|
+
private readonly tui: TUI;
|
|
123
|
+
private readonly theme: Theme;
|
|
124
|
+
private readonly keybindings: KeybindingsManager;
|
|
125
|
+
private readonly onCancelCallback: () => void;
|
|
126
|
+
private readonly activeProvider: ProviderKey | null;
|
|
127
|
+
private readonly fetchAllFn: (signal: AbortSignal) => Promise<UsageByProvider>;
|
|
128
|
+
private allItems: SubscriptionItem[] = [];
|
|
129
|
+
private filteredItems: SubscriptionItem[] = [];
|
|
130
|
+
private selectedIndex = 0;
|
|
131
|
+
private viewportStart = 0;
|
|
132
|
+
private loading = true;
|
|
133
|
+
private hint: "loading" | "ready" | "error" = "loading";
|
|
134
|
+
private disposed = false;
|
|
135
|
+
private _focused = false;
|
|
136
|
+
|
|
137
|
+
get focused(): boolean {
|
|
138
|
+
return this._focused;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
set focused(value: boolean) {
|
|
142
|
+
this._focused = value;
|
|
143
|
+
this.searchInput.focused = value;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
constructor(
|
|
147
|
+
tui: TUI,
|
|
148
|
+
theme: Theme,
|
|
149
|
+
keybindings: KeybindingsManager,
|
|
150
|
+
activeProvider: ProviderKey | null,
|
|
151
|
+
fetchAll: (signal: AbortSignal) => Promise<UsageByProvider>,
|
|
152
|
+
onCancel: () => void,
|
|
153
|
+
) {
|
|
154
|
+
super();
|
|
155
|
+
this.tui = tui;
|
|
156
|
+
this.theme = theme;
|
|
157
|
+
this.keybindings = keybindings;
|
|
158
|
+
this.activeProvider = activeProvider;
|
|
159
|
+
this.fetchAllFn = fetchAll;
|
|
160
|
+
this.onCancelCallback = onCancel;
|
|
161
|
+
|
|
162
|
+
this.addChild(new DynamicBorder((text: string) => theme.fg("accent", text)));
|
|
163
|
+
this.addChild(new Spacer(1));
|
|
164
|
+
this.hintText = new Text("", 0, 0);
|
|
165
|
+
this.addChild(this.hintText);
|
|
166
|
+
this.addChild(new Spacer(1));
|
|
167
|
+
this.searchInput = new Input();
|
|
168
|
+
this.addChild(this.searchInput);
|
|
169
|
+
this.addChild(new Spacer(1));
|
|
170
|
+
this.listContainer = new Container();
|
|
171
|
+
this.addChild(this.listContainer);
|
|
172
|
+
this.addChild(new Spacer(1));
|
|
173
|
+
this.addChild(new DynamicBorder((text: string) => theme.fg("accent", text)));
|
|
174
|
+
|
|
175
|
+
this.updateHint();
|
|
176
|
+
this.updateList();
|
|
177
|
+
void this.load();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private async load(): Promise<void> {
|
|
181
|
+
try {
|
|
182
|
+
const results = await this.fetchAllFn(this.requestController.signal);
|
|
183
|
+
if (this.disposed || this.requestController.signal.aborted) return;
|
|
184
|
+
this.loading = false;
|
|
185
|
+
this.hint = "ready";
|
|
186
|
+
this.buildItems(results);
|
|
187
|
+
} catch {
|
|
188
|
+
if (this.disposed || this.requestController.signal.aborted) return;
|
|
189
|
+
this.loading = false;
|
|
190
|
+
this.hint = "error";
|
|
191
|
+
}
|
|
192
|
+
this.updateHint();
|
|
193
|
+
this.updateList();
|
|
194
|
+
this.tui.requestRender();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private updateHint(): void {
|
|
198
|
+
if (this.hint === "loading") {
|
|
199
|
+
this.hintText.setText(this.theme.fg("dim", "Fetching quota, balance, and spend from configured providers…"));
|
|
200
|
+
} else if (this.hint === "error") {
|
|
201
|
+
this.hintText.setText(this.theme.fg("error", "Failed to fetch usage data"));
|
|
202
|
+
} else {
|
|
203
|
+
this.hintText.setText(
|
|
204
|
+
this.theme.fg("muted", "Only showing configured usage providers. ") +
|
|
205
|
+
this.theme.fg("dim", "✓ = active provider"),
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private buildItems(results: UsageByProvider): void {
|
|
211
|
+
this.allItems = PROVIDERS.flatMap((provider) => {
|
|
212
|
+
const data = results[provider];
|
|
213
|
+
return data
|
|
214
|
+
? [{
|
|
215
|
+
name: PROVIDER_LABELS[provider],
|
|
216
|
+
provider,
|
|
217
|
+
data,
|
|
218
|
+
isActive: this.activeProvider === provider,
|
|
219
|
+
}]
|
|
220
|
+
: [];
|
|
221
|
+
});
|
|
222
|
+
this.filteredItems = this.allItems;
|
|
223
|
+
this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredItems.length - 1));
|
|
224
|
+
this.ensureSelectedVisible();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private filterItems(query: string): void {
|
|
228
|
+
const normalized = query.trim().toLowerCase();
|
|
229
|
+
this.filteredItems = normalized
|
|
230
|
+
? this.allItems.filter((item) =>
|
|
231
|
+
item.name.toLowerCase().includes(normalized) || item.provider.includes(normalized))
|
|
232
|
+
: this.allItems;
|
|
233
|
+
this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredItems.length - 1));
|
|
234
|
+
this.viewportStart = 0;
|
|
235
|
+
this.ensureSelectedVisible();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private viewportSize(): number {
|
|
239
|
+
// Leave room for the frame, search input, hints, and expanded details for
|
|
240
|
+
// the selected provider. Keeping the provider list bounded avoids pushing
|
|
241
|
+
// the custom UI beyond short terminal viewports.
|
|
242
|
+
return Math.max(1, Math.min(8, this.tui.terminal.rows - 14));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private ensureSelectedVisible(): void {
|
|
246
|
+
const size = this.viewportSize();
|
|
247
|
+
if (this.selectedIndex < this.viewportStart) this.viewportStart = this.selectedIndex;
|
|
248
|
+
if (this.selectedIndex >= this.viewportStart + size) {
|
|
249
|
+
this.viewportStart = this.selectedIndex - size + 1;
|
|
250
|
+
}
|
|
251
|
+
this.viewportStart = Math.max(0, Math.min(
|
|
252
|
+
this.viewportStart,
|
|
253
|
+
Math.max(0, this.filteredItems.length - size),
|
|
254
|
+
));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private moveSelection(delta: number): void {
|
|
258
|
+
if (this.filteredItems.length === 0) return;
|
|
259
|
+
this.selectedIndex = Math.max(0, Math.min(this.filteredItems.length - 1, this.selectedIndex + delta));
|
|
260
|
+
this.ensureSelectedVisible();
|
|
261
|
+
this.refresh();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private renderBar(percent: number, width = 16): string {
|
|
265
|
+
const value = clampPercent(percent);
|
|
266
|
+
const filled = Math.round((value / 100) * width);
|
|
267
|
+
return this.theme.fg(colorForPercent(value), "█".repeat(filled)) +
|
|
268
|
+
this.theme.fg("dim", "░".repeat(width - filled));
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private renderItem(item: SubscriptionItem, selected: boolean): void {
|
|
272
|
+
const theme = this.theme;
|
|
273
|
+
const pointer = selected ? theme.fg("accent", "→ ") : " ";
|
|
274
|
+
const activeBadge = item.isActive ? theme.fg("success", " ✓") : "";
|
|
275
|
+
const name = selected ? theme.fg("accent", theme.bold(item.name)) : item.name;
|
|
276
|
+
this.listContainer.addChild(new Text(`${pointer}${name}${activeBadge}`, 0, 0));
|
|
277
|
+
if (!selected) return;
|
|
278
|
+
|
|
279
|
+
const indent = " ";
|
|
280
|
+
if (item.data.error) {
|
|
281
|
+
this.listContainer.addChild(new Text(indent + theme.fg("error", item.data.error), 0, 0));
|
|
282
|
+
} else {
|
|
283
|
+
const session = clampPercent(item.data.session);
|
|
284
|
+
const weekly = clampPercent(item.data.weekly);
|
|
285
|
+
const sessionReset = item.data.sessionResetsIn
|
|
286
|
+
? theme.fg("dim", ` resets in ${item.data.sessionResetsIn}`)
|
|
287
|
+
: "";
|
|
288
|
+
const weeklyReset = item.data.weeklyResetsIn
|
|
289
|
+
? theme.fg("dim", ` resets in ${item.data.weeklyResetsIn}`)
|
|
290
|
+
: "";
|
|
291
|
+
const sessionLabel = (item.data.sessionLabel ?? "Session").slice(0, 9).padEnd(10);
|
|
292
|
+
const weeklyLabel = (item.data.weeklyLabel ?? "Weekly").slice(0, 9).padEnd(10);
|
|
293
|
+
|
|
294
|
+
if (!item.data.quotaHidden) {
|
|
295
|
+
if (!item.data.sessionHidden) {
|
|
296
|
+
this.listContainer.addChild(new Text(
|
|
297
|
+
indent + theme.fg("muted", sessionLabel) + this.renderBar(session) + " " +
|
|
298
|
+
theme.fg(colorForPercent(session), `${session}%`.padStart(4)) + sessionReset,
|
|
299
|
+
0,
|
|
300
|
+
0,
|
|
301
|
+
));
|
|
302
|
+
}
|
|
303
|
+
if (!item.data.weeklyHidden) {
|
|
304
|
+
this.listContainer.addChild(new Text(
|
|
305
|
+
indent + theme.fg("muted", weeklyLabel) + this.renderBar(weekly) + " " +
|
|
306
|
+
theme.fg(colorForPercent(weekly), `${weekly}%`.padStart(4)) + weeklyReset,
|
|
307
|
+
0,
|
|
308
|
+
0,
|
|
309
|
+
));
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (item.data.accountBalance) {
|
|
313
|
+
this.listContainer.addChild(new Text(
|
|
314
|
+
indent + theme.fg("muted", formatAccountBalance(item.data.accountBalance)),
|
|
315
|
+
0,
|
|
316
|
+
0,
|
|
317
|
+
));
|
|
318
|
+
}
|
|
319
|
+
for (const balance of item.data.accountBalanceDetails ?? []) {
|
|
320
|
+
this.listContainer.addChild(new Text(
|
|
321
|
+
indent + theme.fg("dim", formatAccountBalance(balance)),
|
|
322
|
+
0,
|
|
323
|
+
0,
|
|
324
|
+
));
|
|
325
|
+
}
|
|
326
|
+
if (item.data.accountUsage) {
|
|
327
|
+
this.listContainer.addChild(new Text(
|
|
328
|
+
indent + theme.fg("muted", formatAccountBalance(item.data.accountUsage)),
|
|
329
|
+
0,
|
|
330
|
+
0,
|
|
331
|
+
));
|
|
332
|
+
}
|
|
333
|
+
if (item.data.accountSpend) {
|
|
334
|
+
this.listContainer.addChild(new Text(
|
|
335
|
+
indent + theme.fg("muted", formatAccountSpend(item.data.accountSpend)),
|
|
336
|
+
0,
|
|
337
|
+
0,
|
|
338
|
+
));
|
|
339
|
+
}
|
|
340
|
+
if (item.data.notice) {
|
|
341
|
+
this.listContainer.addChild(new Text(indent + theme.fg("muted", item.data.notice), 0, 0));
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (typeof item.data.extraSpend === "number" && typeof item.data.extraLimit === "number") {
|
|
345
|
+
this.listContainer.addChild(new Text(
|
|
346
|
+
indent + theme.fg("muted", "Extra ") +
|
|
347
|
+
theme.fg("dim", `$${item.data.extraSpend.toFixed(2)} / $${item.data.extraLimit}`),
|
|
348
|
+
0,
|
|
349
|
+
0,
|
|
350
|
+
));
|
|
351
|
+
}
|
|
352
|
+
if (item.data.warning) {
|
|
353
|
+
this.listContainer.addChild(new Text(indent + theme.fg("warning", `⚠ ${item.data.warning}`), 0, 0));
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
this.listContainer.addChild(new Spacer(1));
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
private updateList(): void {
|
|
360
|
+
this.listContainer.clear();
|
|
361
|
+
if (this.loading) {
|
|
362
|
+
this.listContainer.addChild(new Text(this.theme.fg("muted", " Loading…"), 0, 0));
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if (this.filteredItems.length === 0) {
|
|
366
|
+
this.listContainer.addChild(new Text(this.theme.fg("muted", " No matching configured providers"), 0, 0));
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
this.ensureSelectedVisible();
|
|
370
|
+
const size = this.viewportSize();
|
|
371
|
+
const end = Math.min(this.filteredItems.length, this.viewportStart + size);
|
|
372
|
+
if (this.viewportStart > 0) {
|
|
373
|
+
this.listContainer.addChild(new Text(this.theme.fg("dim", ` ↑ ${this.viewportStart} more`), 0, 0));
|
|
374
|
+
}
|
|
375
|
+
for (let index = this.viewportStart; index < end; index += 1) {
|
|
376
|
+
this.renderItem(this.filteredItems[index]!, index === this.selectedIndex);
|
|
377
|
+
}
|
|
378
|
+
if (end < this.filteredItems.length) {
|
|
379
|
+
this.listContainer.addChild(new Text(
|
|
380
|
+
this.theme.fg("dim", ` ↓ ${this.filteredItems.length - end} more`),
|
|
381
|
+
0,
|
|
382
|
+
0,
|
|
383
|
+
));
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
private refresh(): void {
|
|
388
|
+
this.updateList();
|
|
389
|
+
this.tui.requestRender();
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
handleInput(keyData: string): void {
|
|
393
|
+
if (this.keybindings.matches(keyData, "tui.select.up")) {
|
|
394
|
+
if (this.filteredItems.length > 0) {
|
|
395
|
+
this.selectedIndex = this.selectedIndex === 0
|
|
396
|
+
? this.filteredItems.length - 1
|
|
397
|
+
: this.selectedIndex - 1;
|
|
398
|
+
this.ensureSelectedVisible();
|
|
399
|
+
this.refresh();
|
|
400
|
+
}
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
if (this.keybindings.matches(keyData, "tui.select.down")) {
|
|
404
|
+
if (this.filteredItems.length > 0) {
|
|
405
|
+
this.selectedIndex = this.selectedIndex === this.filteredItems.length - 1
|
|
406
|
+
? 0
|
|
407
|
+
: this.selectedIndex + 1;
|
|
408
|
+
this.ensureSelectedVisible();
|
|
409
|
+
this.refresh();
|
|
410
|
+
}
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
if (this.keybindings.matches(keyData, "tui.select.pageUp")) {
|
|
414
|
+
this.moveSelection(-this.viewportSize());
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
if (this.keybindings.matches(keyData, "tui.select.pageDown")) {
|
|
418
|
+
this.moveSelection(this.viewportSize());
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
if (
|
|
422
|
+
this.keybindings.matches(keyData, "tui.select.cancel") ||
|
|
423
|
+
this.keybindings.matches(keyData, "tui.select.confirm")
|
|
424
|
+
) {
|
|
425
|
+
this.onCancelCallback();
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
this.searchInput.handleInput(keyData);
|
|
430
|
+
this.filterItems(this.searchInput.getValue());
|
|
431
|
+
this.refresh();
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
override invalidate(): void {
|
|
435
|
+
super.invalidate();
|
|
436
|
+
this.updateHint();
|
|
437
|
+
this.updateList();
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
dispose(): void {
|
|
441
|
+
this.disposed = true;
|
|
442
|
+
this.requestController.abort();
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
interface UsageState extends UsageByProvider {
|
|
447
|
+
activeProvider: ProviderKey | null;
|
|
448
|
+
available: Partial<Record<ProviderKey, boolean>>;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export default function (pi: ExtensionAPI): void {
|
|
452
|
+
pi.registerFlag("usage", {
|
|
453
|
+
description: "Print one-line usage for the active provider and exit",
|
|
454
|
+
type: "boolean",
|
|
455
|
+
default: false,
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
const endpoints = resolveUsageEndpoints();
|
|
459
|
+
const state: UsageState = {
|
|
460
|
+
codex: null,
|
|
461
|
+
claude: null,
|
|
462
|
+
zai: null,
|
|
463
|
+
"zai-cn": null,
|
|
464
|
+
kimi: null,
|
|
465
|
+
minimax: null,
|
|
466
|
+
"minimax-cn": null,
|
|
467
|
+
openrouter: null,
|
|
468
|
+
deepseek: null,
|
|
469
|
+
moonshot: null,
|
|
470
|
+
"moonshot-cn": null,
|
|
471
|
+
baseten: null,
|
|
472
|
+
activeProvider: null,
|
|
473
|
+
available: {},
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
let pollTimer: ReturnType<typeof setInterval> | undefined;
|
|
477
|
+
let pollInFlight: Promise<void> | undefined;
|
|
478
|
+
let pollQueued = false;
|
|
479
|
+
let currentContext: ExtensionContext | undefined;
|
|
480
|
+
let sessionController: AbortController | undefined;
|
|
481
|
+
let providerPollController: AbortController | undefined;
|
|
482
|
+
|
|
483
|
+
const renderPercent = (theme: Theme, value: number) => {
|
|
484
|
+
const percent = clampPercent(value);
|
|
485
|
+
return theme.fg(colorForPercent(percent), `${percent}%`);
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
const renderBar = (theme: Theme, value: number) => {
|
|
489
|
+
const percent = clampPercent(value);
|
|
490
|
+
const width = 8;
|
|
491
|
+
const filled = Math.round((percent / 100) * width);
|
|
492
|
+
return theme.fg(colorForPercent(percent), "█".repeat(filled)) +
|
|
493
|
+
theme.fg("dim", "░".repeat(width - filled));
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
function updateStatus(): void {
|
|
497
|
+
const ctx = currentContext;
|
|
498
|
+
if (!ctx || ctx.mode !== "tui") return;
|
|
499
|
+
const provider = state.activeProvider;
|
|
500
|
+
if (!provider || state.available[provider] === false) {
|
|
501
|
+
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const data = state[provider];
|
|
506
|
+
const theme = ctx.ui.theme;
|
|
507
|
+
const label = PROVIDER_LABELS[provider];
|
|
508
|
+
if (!data) {
|
|
509
|
+
ctx.ui.setStatus(STATUS_KEY, theme.fg("dim", `${label} usage: loading…`));
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
if (data.error) {
|
|
513
|
+
ctx.ui.setStatus(STATUS_KEY, theme.fg("warning", `${label} usage unavailable (${data.error})`));
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
if (data.quotaHidden) {
|
|
517
|
+
const financial = [
|
|
518
|
+
data.accountBalance ? formatAccountBalance(data.accountBalance) : undefined,
|
|
519
|
+
data.accountUsage ? formatAccountBalance(data.accountUsage) : undefined,
|
|
520
|
+
data.accountSpend?.monthly === undefined
|
|
521
|
+
? undefined
|
|
522
|
+
: `Month · ${formatFinancialAmount(data.accountSpend.monthly, data.accountSpend.unit)}`,
|
|
523
|
+
].filter((value): value is string => Boolean(value));
|
|
524
|
+
const summary = financial.length > 0 ? financial.join(" · ") : data.notice;
|
|
525
|
+
ctx.ui.setStatus(
|
|
526
|
+
STATUS_KEY,
|
|
527
|
+
summary ? theme.fg("dim", `${label} `) + theme.fg("muted", summary) : undefined,
|
|
528
|
+
);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const session = clampPercent(data.session);
|
|
533
|
+
const weekly = clampPercent(data.weekly);
|
|
534
|
+
const sessionPrefix = data.sessionLabel === "5-hour"
|
|
535
|
+
? "5h "
|
|
536
|
+
: data.sessionLabel === "Interval"
|
|
537
|
+
? "I "
|
|
538
|
+
: data.sessionLabel === "Key limit"
|
|
539
|
+
? "L "
|
|
540
|
+
: "S ";
|
|
541
|
+
const quotaLanes: string[] = [];
|
|
542
|
+
if (!data.sessionHidden) {
|
|
543
|
+
quotaLanes.push(
|
|
544
|
+
theme.fg("muted", sessionPrefix) + renderBar(theme, session) + " " + renderPercent(theme, session) +
|
|
545
|
+
(data.sessionResetsIn ? theme.fg("dim", ` ⟳ ${data.sessionResetsIn}`) : ""),
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
if (!data.weeklyHidden) {
|
|
549
|
+
quotaLanes.push(
|
|
550
|
+
theme.fg("muted", "W ") + renderBar(theme, weekly) + " " + renderPercent(theme, weekly) +
|
|
551
|
+
(data.weeklyResetsIn ? theme.fg("dim", ` ⟳ ${data.weeklyResetsIn}`) : ""),
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
const status =
|
|
555
|
+
theme.fg("dim", `${label} `) +
|
|
556
|
+
quotaLanes.join(" ") +
|
|
557
|
+
(data.accountBalance ? theme.fg("muted", ` · ${formatAccountBalance(data.accountBalance)}`) : "") +
|
|
558
|
+
(data.accountSpend?.monthly === undefined
|
|
559
|
+
? ""
|
|
560
|
+
: theme.fg("muted", ` · Month ${formatFinancialAmount(data.accountSpend.monthly, data.accountSpend.unit)}`)) +
|
|
561
|
+
(data.stale ? theme.fg("warning", " stale") : "") +
|
|
562
|
+
(data.warning && !data.stale ? theme.fg("warning", " ⚠") : "");
|
|
563
|
+
ctx.ui.setStatus(STATUS_KEY, status);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function updateProviderFrom(model: ExtensionContext["model"]): boolean {
|
|
567
|
+
const previous = state.activeProvider;
|
|
568
|
+
state.activeProvider = detectProvider(model);
|
|
569
|
+
if (previous !== state.activeProvider) {
|
|
570
|
+
providerPollController?.abort();
|
|
571
|
+
updateStatus();
|
|
572
|
+
return true;
|
|
573
|
+
}
|
|
574
|
+
return false;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function isClaudeSubscriptionAuth(source: string | undefined): boolean {
|
|
578
|
+
// Pi 0.84 exposes AuthResult.source as a human-readable label rather than a
|
|
579
|
+
// credential-type discriminator. Keep the compatibility assumption in one
|
|
580
|
+
// place until ModelRegistry exposes the resolved credential type directly.
|
|
581
|
+
return source === "OAuth";
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
async function resolveCredential(ctx: ExtensionContext, provider: ProviderKey): Promise<CredentialResolution> {
|
|
585
|
+
const providerId = providerToPiProviderId(provider);
|
|
586
|
+
if (!ctx.modelRegistry.getProvider(providerId)) return {};
|
|
587
|
+
const status = ctx.modelRegistry.getProviderAuthStatus(providerId);
|
|
588
|
+
if (!status.configured) return {};
|
|
589
|
+
|
|
590
|
+
try {
|
|
591
|
+
const resolved = await ctx.modelRegistry.getProviderAuth(providerId);
|
|
592
|
+
if (provider === "claude" && !isClaudeSubscriptionAuth(resolved?.source)) return {};
|
|
593
|
+
const token = resolved?.auth.apiKey;
|
|
594
|
+
if (token) return { token };
|
|
595
|
+
// Some OAuth flows (e.g. kimi-coding) expose the token only as a Bearer
|
|
596
|
+
// Authorization header rather than as apiKey.
|
|
597
|
+
const authorization = resolved?.auth.headers?.Authorization ?? resolved?.auth.headers?.authorization;
|
|
598
|
+
if (typeof authorization === "string" && authorization.startsWith("Bearer ")) {
|
|
599
|
+
return { token: authorization.slice("Bearer ".length) };
|
|
600
|
+
}
|
|
601
|
+
return { error: "configured authentication did not resolve a token" };
|
|
602
|
+
} catch (error) {
|
|
603
|
+
return { error: error instanceof Error ? error.message : String(error) };
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
async function fetchProvider(
|
|
608
|
+
ctx: ExtensionContext,
|
|
609
|
+
provider: ProviderKey,
|
|
610
|
+
signal: AbortSignal,
|
|
611
|
+
): Promise<void> {
|
|
612
|
+
const credential = await resolveCredential(ctx, provider);
|
|
613
|
+
if (signal.aborted) return;
|
|
614
|
+
state.available[provider] = Boolean(credential.token || credential.error);
|
|
615
|
+
if (credential.error) {
|
|
616
|
+
state[provider] = { session: 0, weekly: 0, error: `auth resolution failed (${credential.error})` };
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
if (!credential.token) {
|
|
620
|
+
state[provider] = null;
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
if (provider === "codex") state.codex = await fetchCodexUsage(credential.token, { signal });
|
|
625
|
+
if (provider === "claude") state.claude = await fetchClaudeUsageWithFallback(credential.token, { signal });
|
|
626
|
+
if (provider === "zai") state.zai = await fetchZaiUsage(credential.token, "zai", { endpoints, signal });
|
|
627
|
+
if (provider === "zai-cn") state["zai-cn"] = await fetchZaiUsage(credential.token, "zai-cn", { endpoints, signal });
|
|
628
|
+
if (provider === "kimi") state.kimi = await fetchKimiUsage(credential.token, { endpoints, signal });
|
|
629
|
+
if (provider === "minimax") {
|
|
630
|
+
state.minimax = await fetchMiniMaxUsage(credential.token, "minimax", { endpoints, signal });
|
|
631
|
+
}
|
|
632
|
+
if (provider === "minimax-cn") {
|
|
633
|
+
state["minimax-cn"] = await fetchMiniMaxUsage(credential.token, "minimax-cn", { endpoints, signal });
|
|
634
|
+
}
|
|
635
|
+
if (provider === "openrouter") {
|
|
636
|
+
state.openrouter = await fetchOpenRouterUsage(credential.token, { endpoints, signal });
|
|
637
|
+
}
|
|
638
|
+
if (provider === "deepseek") {
|
|
639
|
+
state.deepseek = await fetchDeepSeekBalance(credential.token, { endpoints, signal });
|
|
640
|
+
}
|
|
641
|
+
if (provider === "moonshot") {
|
|
642
|
+
state.moonshot = await fetchMoonshotBalance(credential.token, "moonshot", { endpoints, signal });
|
|
643
|
+
}
|
|
644
|
+
if (provider === "moonshot-cn") {
|
|
645
|
+
state["moonshot-cn"] = await fetchMoonshotBalance(credential.token, "moonshot-cn", { endpoints, signal });
|
|
646
|
+
}
|
|
647
|
+
if (provider === "baseten") state.baseten = await fetchBasetenUsage(credential.token, { endpoints, signal });
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
async function runPoll(): Promise<void> {
|
|
651
|
+
const ctx = currentContext;
|
|
652
|
+
const sessionSignal = sessionController?.signal;
|
|
653
|
+
const provider = state.activeProvider;
|
|
654
|
+
if (!ctx || !sessionSignal || sessionSignal.aborted || ctx.mode !== "tui" || !provider) {
|
|
655
|
+
updateStatus();
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
const controller = new AbortController();
|
|
660
|
+
providerPollController = controller;
|
|
661
|
+
const signal = AbortSignal.any([sessionSignal, controller.signal]);
|
|
662
|
+
try {
|
|
663
|
+
await fetchProvider(ctx, provider, signal);
|
|
664
|
+
if (signal.aborted) return;
|
|
665
|
+
const data = state[provider];
|
|
666
|
+
if (data && !data.error) pi.events.emit(USAGE_UPDATE_EVENT, { provider, ...data });
|
|
667
|
+
updateStatus();
|
|
668
|
+
} finally {
|
|
669
|
+
if (providerPollController === controller) providerPollController = undefined;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
async function poll(): Promise<void> {
|
|
674
|
+
if (pollInFlight) {
|
|
675
|
+
pollQueued = true;
|
|
676
|
+
return pollInFlight;
|
|
677
|
+
}
|
|
678
|
+
do {
|
|
679
|
+
pollQueued = false;
|
|
680
|
+
pollInFlight = runPoll().catch(() => undefined).finally(() => {
|
|
681
|
+
pollInFlight = undefined;
|
|
682
|
+
});
|
|
683
|
+
await pollInFlight;
|
|
684
|
+
} while (pollQueued && !sessionController?.signal.aborted);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
async function fetchAllForContext(ctx: ExtensionContext, signal: AbortSignal): Promise<UsageByProvider> {
|
|
688
|
+
const resolutions = await Promise.all(PROVIDERS.map(async (provider) =>
|
|
689
|
+
[provider, await resolveCredential(ctx, provider)] as const));
|
|
690
|
+
if (signal.aborted) throw new DOMException("Aborted", "AbortError");
|
|
691
|
+
|
|
692
|
+
const tokens: UsageTokens = {};
|
|
693
|
+
const authErrors: Partial<Record<ProviderKey, string>> = {};
|
|
694
|
+
for (const [provider, resolution] of resolutions) {
|
|
695
|
+
if (resolution.token) tokens[provider] = resolution.token;
|
|
696
|
+
if (resolution.error) authErrors[provider] = resolution.error;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const results = await fetchAllUsages(tokens, { endpoints, signal });
|
|
700
|
+
for (const provider of PROVIDERS) {
|
|
701
|
+
const error = authErrors[provider];
|
|
702
|
+
if (error) results[provider] = { session: 0, weekly: 0, error: `auth resolution failed (${error})` };
|
|
703
|
+
}
|
|
704
|
+
return results;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
708
|
+
currentContext = ctx;
|
|
709
|
+
sessionController?.abort();
|
|
710
|
+
sessionController = new AbortController();
|
|
711
|
+
updateProviderFrom(ctx.model);
|
|
712
|
+
|
|
713
|
+
if (pollTimer) clearInterval(pollTimer);
|
|
714
|
+
pollTimer = undefined;
|
|
715
|
+
|
|
716
|
+
if (pi.getFlag("usage") === true) {
|
|
717
|
+
const provider = state.activeProvider;
|
|
718
|
+
if (!provider) {
|
|
719
|
+
console.log(JSON.stringify({ extension: EXTENSION_ID, status: "unsupported", provider: ctx.model?.provider }));
|
|
720
|
+
} else {
|
|
721
|
+
await fetchProvider(ctx, provider, sessionController.signal);
|
|
722
|
+
const data = state[provider];
|
|
723
|
+
console.log(JSON.stringify({
|
|
724
|
+
extension: EXTENSION_ID,
|
|
725
|
+
provider,
|
|
726
|
+
status: !data ? "unconfigured" : data.error ? "error" : "ok",
|
|
727
|
+
...(data ?? {}),
|
|
728
|
+
}));
|
|
729
|
+
}
|
|
730
|
+
ctx.shutdown();
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
if (ctx.mode !== "tui") return;
|
|
735
|
+
|
|
736
|
+
updateStatus();
|
|
737
|
+
void poll();
|
|
738
|
+
pollTimer = setInterval(() => void poll(), POLL_INTERVAL_MS);
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
pi.on("session_shutdown", (_event, ctx) => {
|
|
742
|
+
providerPollController?.abort();
|
|
743
|
+
providerPollController = undefined;
|
|
744
|
+
sessionController?.abort();
|
|
745
|
+
sessionController = undefined;
|
|
746
|
+
pollQueued = false;
|
|
747
|
+
if (pollTimer) clearInterval(pollTimer);
|
|
748
|
+
pollTimer = undefined;
|
|
749
|
+
if (ctx.mode === "tui") ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
750
|
+
currentContext = undefined;
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
pi.on("model_select", (event, ctx) => {
|
|
754
|
+
currentContext = ctx;
|
|
755
|
+
updateProviderFrom(event.model);
|
|
756
|
+
void poll();
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
pi.registerCommand("usage", {
|
|
760
|
+
description: "Show quota, balance, and spend for configured providers",
|
|
761
|
+
handler: async (_args, ctx) => {
|
|
762
|
+
currentContext = ctx;
|
|
763
|
+
updateProviderFrom(ctx.model);
|
|
764
|
+
if (ctx.mode !== "tui") {
|
|
765
|
+
if (ctx.hasUI) ctx.ui.notify("/usage is available in interactive mode", "warning");
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
await ctx.ui.custom<void>((tui, theme, keybindings, done) =>
|
|
770
|
+
new UsageSelectorComponent(
|
|
771
|
+
tui,
|
|
772
|
+
theme,
|
|
773
|
+
keybindings,
|
|
774
|
+
state.activeProvider,
|
|
775
|
+
(signal) => fetchAllForContext(ctx, signal),
|
|
776
|
+
() => done(),
|
|
777
|
+
));
|
|
778
|
+
void poll();
|
|
779
|
+
},
|
|
780
|
+
});
|
|
781
|
+
}
|