opencode-visual-cache 1.6.1 → 1.7.0-beta.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/LICENSE +21 -21
- package/README.md +247 -247
- package/README_EN.md +247 -247
- package/dist/_version.d.ts +1 -1
- package/dist/_version.js +1 -1
- package/dist/core/color.d.ts +37 -0
- package/dist/core/color.js +108 -0
- package/dist/core/currency.d.ts +17 -0
- package/dist/core/currency.js +43 -0
- package/dist/core/estimate.d.ts +1 -0
- package/dist/core/estimate.js +40 -0
- package/dist/core/format.d.ts +15 -0
- package/dist/core/format.js +90 -0
- package/dist/core/index.d.ts +5 -0
- package/dist/core/index.js +5 -0
- package/dist/core/types.d.ts +17 -0
- package/dist/core/types.js +1 -0
- package/dist/index.js +159 -815
- package/dist/panel/TokenCachePanel.d.ts +10 -0
- package/dist/panel/TokenCachePanel.js +549 -0
- package/dist/panel/panel-api.d.ts +119 -0
- package/dist/panel/panel-api.js +1 -0
- package/dist/tui.js +349 -264
- package/dist/v2/commands.d.ts +10 -0
- package/dist/v2/commands.js +490 -0
- package/dist/v2/data.d.ts +27 -0
- package/dist/v2/data.js +103 -0
- package/dist/v2/index.d.ts +4 -0
- package/dist/v2/index.js +163 -0
- package/dist/v2/sidebar.d.ts +6 -0
- package/dist/v2/sidebar.js +36 -0
- package/dist/v2/status.d.ts +14 -0
- package/dist/v2/status.js +83 -0
- package/dist/v2/theme.d.ts +8 -0
- package/dist/v2/theme.js +16 -0
- package/dist/v2/types.d.ts +219 -0
- package/dist/v2/types.js +6 -0
- package/dist/v2/v2-panel-api.d.ts +8 -0
- package/dist/v2/v2-panel-api.js +176 -0
- package/dist/v2.js +2941 -0
- package/package.json +70 -67
- package/src/_version.ts +1 -1
- package/src/balance-providers.ts +153 -153
- package/src/core/color.ts +108 -0
- package/src/core/currency.ts +46 -0
- package/src/core/estimate.ts +36 -0
- package/src/core/format.ts +81 -0
- package/src/core/index.ts +5 -0
- package/src/core/types.ts +17 -0
- package/src/i18n.ts +380 -380
- package/src/index.tsx +1035 -1980
- package/src/panel/TokenCachePanel.tsx +776 -0
- package/src/panel/panel-api.ts +104 -0
- package/src/server.ts +10 -10
- package/src/v2/commands.ts +471 -0
- package/src/v2/data.ts +112 -0
- package/src/v2/index.tsx +184 -0
- package/src/v2/sidebar.tsx +69 -0
- package/src/v2/status.tsx +96 -0
- package/src/v2/theme.ts +19 -0
- package/src/v2/types.ts +159 -0
- package/src/v2/v2-panel-api.ts +177 -0
package/src/index.tsx
CHANGED
|
@@ -1,1980 +1,1035 @@
|
|
|
1
|
-
/** @jsxImportSource @opentui/solid */
|
|
2
|
-
|
|
3
|
-
import type { JSX } from "@opentui/solid"
|
|
4
|
-
import type {
|
|
5
|
-
TuiPlugin,
|
|
6
|
-
TuiPluginApi,
|
|
7
|
-
TuiSlotContext,
|
|
8
|
-
TuiSlotPlugin,
|
|
9
|
-
TuiPluginModule,
|
|
10
|
-
TuiThemeCurrent,
|
|
11
|
-
TuiDialogStack,
|
|
12
|
-
TuiPromptRef,
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
import type {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
const
|
|
72
|
-
if (
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const
|
|
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
|
-
|
|
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
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
//
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
-
const
|
|
423
|
-
const
|
|
424
|
-
const
|
|
425
|
-
const
|
|
426
|
-
const
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
const [
|
|
437
|
-
const [
|
|
438
|
-
|
|
439
|
-
const [
|
|
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
|
-
|
|
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
|
-
const
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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
|
-
const
|
|
569
|
-
const
|
|
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
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
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
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
const
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
<Show when={data().providerName}>
|
|
1037
|
-
<text fg={pal().muted}>
|
|
1038
|
-
{justify(t("provider"), data().providerName)}
|
|
1039
|
-
</text>
|
|
1040
|
-
</Show>
|
|
1041
|
-
<text fg={pal().muted}>
|
|
1042
|
-
{justify(t("model"), data().model)}
|
|
1043
|
-
</text>
|
|
1044
|
-
<Show when={data().hasPricing}>
|
|
1045
|
-
<text fg={pal().muted}>
|
|
1046
|
-
{justify(t("rate"), currencySymbol() + (data().inputRate * exchangeRate()).toFixed(2) + "/M " + t("inputRate"))}
|
|
1047
|
-
</text>
|
|
1048
|
-
<Show when={data().cacheReadRate > 0}>
|
|
1049
|
-
<text fg={pal().muted}>
|
|
1050
|
-
{justify("", currencySymbol() + (data().cacheReadRate * exchangeRate()).toFixed(2) + "/M " + t("cacheRate"))}
|
|
1051
|
-
</text>
|
|
1052
|
-
</Show>
|
|
1053
|
-
<Show when={data().cacheWriteRate > 0}>
|
|
1054
|
-
<text fg={pal().muted}>
|
|
1055
|
-
{justify("", currencySymbol() + (data().cacheWriteRate * exchangeRate()).toFixed(2) + "/M " + t("writeRate"))}
|
|
1056
|
-
</text>
|
|
1057
|
-
</Show>
|
|
1058
|
-
</Show>
|
|
1059
|
-
</Show>
|
|
1060
|
-
</Show>
|
|
1061
|
-
|
|
1062
|
-
{/* ── token distribution (collapsible, default closed) ── */}
|
|
1063
|
-
<Show when={sectionDist()}>
|
|
1064
|
-
<Show when={data().hasDistData}>
|
|
1065
|
-
{<text onMouseUp={() => setDistOpen((o) => { const n = !o; persistFold("dist", n); return n })}>
|
|
1066
|
-
<span style={{ fg: pal().muted }}>{distOpen() ? "\u25bc " : "\u25b6 "}</span>
|
|
1067
|
-
<span style={{ fg: pal().primary }}><b>{t("distTitle")}</b></span>
|
|
1068
|
-
<span style={{ fg: pal().muted }}>{sep().slice(visualWidth((distOpen() ? "\u25bc " : "\u25b6 ") + t("distTitle")))}</span>
|
|
1069
|
-
</text>}
|
|
1070
|
-
<Show when={distOpen()}>
|
|
1071
|
-
<Show when={data().dist.system > 0}>
|
|
1072
|
-
<text fg={pal().muted}>
|
|
1073
|
-
{justify(t("distSys"), fmt(data().dist.system), t("tok"))}
|
|
1074
|
-
</text>
|
|
1075
|
-
</Show>
|
|
1076
|
-
<Show when={data().dist.user > 0}>
|
|
1077
|
-
<text fg={pal().muted}>
|
|
1078
|
-
{justify(t("distUser"), fmt(data().dist.user), t("tok"))}
|
|
1079
|
-
</text>
|
|
1080
|
-
</Show>
|
|
1081
|
-
<Show when={data().dist.agent > 0}>
|
|
1082
|
-
<text fg={pal().muted}>
|
|
1083
|
-
{justify(t("distAgent"), fmt(data().dist.agent), t("tok"))}
|
|
1084
|
-
</text>
|
|
1085
|
-
</Show>
|
|
1086
|
-
<Show when={data().dist.toolCall > 0}>
|
|
1087
|
-
<text fg={pal().muted}>
|
|
1088
|
-
{justify(t("distTool"), fmt(data().dist.toolCall), t("tok"))}
|
|
1089
|
-
</text>
|
|
1090
|
-
</Show>
|
|
1091
|
-
<Show when={data().dist.toolResult > 0}>
|
|
1092
|
-
<text fg={pal().muted}>
|
|
1093
|
-
{justify(t("distRes"), fmt(data().dist.toolResult), t("tok"))}
|
|
1094
|
-
</text>
|
|
1095
|
-
</Show>
|
|
1096
|
-
<Show when={data().dist.reasoning > 0}>
|
|
1097
|
-
<text fg={pal().muted}>
|
|
1098
|
-
{justify(t("distReason"), fmt(data().dist.reasoning), t("tok"))}
|
|
1099
|
-
</text>
|
|
1100
|
-
</Show>
|
|
1101
|
-
</Show>
|
|
1102
|
-
</Show>
|
|
1103
|
-
</Show>
|
|
1104
|
-
|
|
1105
|
-
{/* ── loaded skills (collapsible, default open) ── */}
|
|
1106
|
-
<Show when={sectionSkills()}>
|
|
1107
|
-
<Show when={data().hasSkills}>
|
|
1108
|
-
{<text onMouseUp={() => setSkillsOpen((o) => { const n = !o; persistFold("skills", n); return n })}>
|
|
1109
|
-
<span style={{ fg: pal().muted }}>{skillsOpen() ? "\u25bc " : "\u25b6 "}</span>
|
|
1110
|
-
<span style={{ fg: pal().primary }}><b>{t("secSkills")}</b></span>
|
|
1111
|
-
<span style={{ fg: pal().muted }}> ({data().skills.length})</span>
|
|
1112
|
-
<span style={{ fg: pal().muted }}>{sep().slice(visualWidth((skillsOpen() ? "\u25bc " : "\u25b6 ") + t("secSkills") + ` (${data().skills.length})`))}</span>
|
|
1113
|
-
</text>}
|
|
1114
|
-
<Show when={skillsOpen()}>
|
|
1115
|
-
{data().skills.map((sk: { name: string; tokens: number }) => {
|
|
1116
|
-
const rightW = visualWidth(fmt(sk.tokens)) + UNIT_GAP + visualWidth(t("tok"))
|
|
1117
|
-
const maxLabel = Math.max(4, panelWidth() - gutter() - rightW - 1)
|
|
1118
|
-
const label = truncateVisual(sk.name, maxLabel)
|
|
1119
|
-
return (
|
|
1120
|
-
<text fg={pal().muted}>
|
|
1121
|
-
{justify(label, fmt(sk.tokens), t("tok"))}
|
|
1122
|
-
</text>
|
|
1123
|
-
)
|
|
1124
|
-
})}
|
|
1125
|
-
</Show>
|
|
1126
|
-
</Show>
|
|
1127
|
-
</Show>
|
|
1128
|
-
|
|
1129
|
-
{/* ── provider balance (single line) ── */}
|
|
1130
|
-
<Show when={sectionBalance()}>
|
|
1131
|
-
<text fg={pal().muted}>{sep()}</text>
|
|
1132
|
-
<Show when={balanceUnsupported()}>
|
|
1133
|
-
<text fg={pal().muted}>
|
|
1134
|
-
<span style={{ fg: pal().muted }}>{"> "}</span>
|
|
1135
|
-
<span>{t("balUnsupported")}</span>
|
|
1136
|
-
</text>
|
|
1137
|
-
</Show>
|
|
1138
|
-
<Show when={!balanceUnsupported()}>
|
|
1139
|
-
<Show when={balanceState().status === "idle"}>
|
|
1140
|
-
<text fg={pal().muted}>
|
|
1141
|
-
<span style={{ fg: pal().muted }}>{"> "}</span>
|
|
1142
|
-
<span>{t("balNoKey", { p: providerName() })}</span>
|
|
1143
|
-
</text>
|
|
1144
|
-
</Show>
|
|
1145
|
-
<Show when={balanceState().status === "loading"}>
|
|
1146
|
-
<text fg={pal().muted}>
|
|
1147
|
-
<span style={{ fg: pal().muted }}>{"> "}</span>
|
|
1148
|
-
<span>{t("balLoading")}</span>
|
|
1149
|
-
</text>
|
|
1150
|
-
</Show>
|
|
1151
|
-
<Show when={balanceState().status === "error"}>
|
|
1152
|
-
<text fg={pal().error}>
|
|
1153
|
-
<span style={{ fg: pal().muted }}>{"> "}</span>
|
|
1154
|
-
<span>{(() => {
|
|
1155
|
-
const code = balanceState().error
|
|
1156
|
-
if (code === "401") return t("balErr401")
|
|
1157
|
-
if (code === "403") return t("balErr403")
|
|
1158
|
-
if (code === "EMPTY") return t("balErrEmpty")
|
|
1159
|
-
if (code === "TIMEOUT") return t("balErrTimeout")
|
|
1160
|
-
return t("balError") + (code ? ` (${code})` : "")
|
|
1161
|
-
})()}</span>
|
|
1162
|
-
</text>
|
|
1163
|
-
</Show>
|
|
1164
|
-
<Show when={balanceState().status === "ok" && balanceState().data}>
|
|
1165
|
-
<text fg={pal().text}>
|
|
1166
|
-
{justify(t("balTotal"), formatBalanceText(balanceState().data!, balanceCurrency(), exchangeRate()))}
|
|
1167
|
-
</text>
|
|
1168
|
-
</Show>
|
|
1169
|
-
</Show>
|
|
1170
|
-
</Show>
|
|
1171
|
-
</Show>
|
|
1172
|
-
</Show>
|
|
1173
|
-
</box>
|
|
1174
|
-
)
|
|
1175
|
-
}
|
|
1176
|
-
|
|
1177
|
-
// ---------------------------------------------------------------------------
|
|
1178
|
-
// Plugin entry
|
|
1179
|
-
// ---------------------------------------------------------------------------
|
|
1180
|
-
|
|
1181
|
-
/**
|
|
1182
|
-
* 输入框 hint 行(session_prompt slot 的 hint):单行显示 路径 · 命中率 · 余额 · Tokens。
|
|
1183
|
-
* 通过 ui.Prompt 的 hint prop 注入——宿主右侧的 token/commands 提示自动保留,
|
|
1184
|
-
* 三合一信息与路径同行显示在中间位置。
|
|
1185
|
-
*/
|
|
1186
|
-
function BottomStatusBar(props: { api: TuiPluginApi; signals: PanelSignals; sessionId: string }): JSX.Element {
|
|
1187
|
-
const KV_PREFIX = "cache_panel"
|
|
1188
|
-
const t = createT(() => props.signals.langCode())
|
|
1189
|
-
|
|
1190
|
-
const sid = props.sessionId
|
|
1191
|
-
|
|
1192
|
-
// ── 命中率(单条口径:最后一条有 token 的 assistant 消息)+ token 汇总 ──
|
|
1193
|
-
const stats = createMemo(() => {
|
|
1194
|
-
const id = sid
|
|
1195
|
-
if (!id) return null
|
|
1196
|
-
const msgs = props.api.state.session.messages(id) as Message[]
|
|
1197
|
-
const session = typeof props.api.state.session.get === "function"
|
|
1198
|
-
? props.api.state.session.get(id)
|
|
1199
|
-
: undefined
|
|
1200
|
-
let input = session?.tokens?.input ?? 0
|
|
1201
|
-
let read = session?.tokens?.cache?.read ?? 0
|
|
1202
|
-
let write = session?.tokens?.cache?.write ?? 0
|
|
1203
|
-
// 旧 SDK 无 session 聚合字段 → 遍历消息累加(与侧边栏 fallback 一致)
|
|
1204
|
-
if (session?.tokens == null) {
|
|
1205
|
-
for (const m of msgs) {
|
|
1206
|
-
if (m.role !== "assistant") continue
|
|
1207
|
-
const tk = (m as AssistantMessage).tokens
|
|
1208
|
-
if (!tk) continue
|
|
1209
|
-
input += num(tk.input)
|
|
1210
|
-
read += num(tk.cache?.read)
|
|
1211
|
-
write += num(tk.cache?.write)
|
|
1212
|
-
}
|
|
1213
|
-
}
|
|
1214
|
-
// 从后往前取最后两条有 token 数据的 assistant 消息 → 单条命中率 + 趋势
|
|
1215
|
-
// 分母含缓存写(业界口径:read / (input+read+write))
|
|
1216
|
-
let hitRate = -1, prevHitRate = -1
|
|
1217
|
-
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
1218
|
-
const m = msgs[i]
|
|
1219
|
-
if (m.role !== "assistant") continue
|
|
1220
|
-
const tk = (m as AssistantMessage).tokens
|
|
1221
|
-
if (!tk) continue
|
|
1222
|
-
const mit = num(tk.input) + num(tk.cache?.read) + num(tk.cache?.write)
|
|
1223
|
-
const mrt = num(tk.cache?.read)
|
|
1224
|
-
if (mit <= 0) continue
|
|
1225
|
-
const rate = (mrt / mit) * 100
|
|
1226
|
-
if (hitRate < 0) { hitRate = rate; continue }
|
|
1227
|
-
prevHitRate = rate
|
|
1228
|
-
break
|
|
1229
|
-
}
|
|
1230
|
-
return { hitRate, prevHitRate, input, read, write }
|
|
1231
|
-
})
|
|
1232
|
-
|
|
1233
|
-
// 余额查询状态为共享信号(PanelSignals.balanceState),由 tui() 统一轮询
|
|
1234
|
-
|
|
1235
|
-
// 自动切换 provider(跟随当前会话模型;幂等,与侧边栏共享信号)
|
|
1236
|
-
createEffect(() => {
|
|
1237
|
-
if (!props.signals.autoBalance()) return
|
|
1238
|
-
const id = sid
|
|
1239
|
-
if (!id) return
|
|
1240
|
-
const msgs = props.api.state.session.messages(id) as Message[]
|
|
1241
|
-
let pid = ""
|
|
1242
|
-
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
1243
|
-
const m = msgs[i]
|
|
1244
|
-
if (m.role === "assistant" && (m as AssistantMessage).providerID) { pid = (m as AssistantMessage).providerID; break }
|
|
1245
|
-
}
|
|
1246
|
-
if (!pid) {
|
|
1247
|
-
try { pid = props.api.state.session.get(id)?.model?.providerID ?? "" } catch {}
|
|
1248
|
-
}
|
|
1249
|
-
if (!pid) return
|
|
1250
|
-
const hit = matchBalanceProvider(pid)
|
|
1251
|
-
if (hit) {
|
|
1252
|
-
props.signals.setBalanceUnsupported(false)
|
|
1253
|
-
if (hit.id !== props.signals.balanceProviderId()) {
|
|
1254
|
-
props.signals.setBalanceProviderId(hit.id)
|
|
1255
|
-
props.signals.setBalanceRefresh(props.signals.balanceRefresh() + 1)
|
|
1256
|
-
}
|
|
1257
|
-
} else {
|
|
1258
|
-
// 当前提供商没有余额适配器 → 标记不支持,余额显示 N/A 并停止轮询
|
|
1259
|
-
props.signals.setBalanceUnsupported(true)
|
|
1260
|
-
}
|
|
1261
|
-
})
|
|
1262
|
-
|
|
1263
|
-
// ── 主题色(与侧边栏同口径)──
|
|
1264
|
-
const pal = createMemo(() => {
|
|
1265
|
-
const th = props.api.theme.current as Record<string, unknown>
|
|
1266
|
-
const sat = (k: string, fb: string) => desaturateTo(th[k], MAX_SAT, fb)
|
|
1267
|
-
return {
|
|
1268
|
-
text: sat("text", FALLBACK.text),
|
|
1269
|
-
muted: sat("textMuted", FALLBACK.muted),
|
|
1270
|
-
success: sat("success", FALLBACK.success),
|
|
1271
|
-
warning: sat("warning", FALLBACK.warning),
|
|
1272
|
-
error: sat("error", FALLBACK.error),
|
|
1273
|
-
}
|
|
1274
|
-
})
|
|
1275
|
-
|
|
1276
|
-
const hitColor = createMemo(() => {
|
|
1277
|
-
const r = stats()?.hitRate ?? -1
|
|
1278
|
-
if (r >= 85) return pal().success
|
|
1279
|
-
if (r >= 70) return pal().warning
|
|
1280
|
-
return pal().error
|
|
1281
|
-
})
|
|
1282
|
-
|
|
1283
|
-
// 命中率趋势:最后一条与上一条的差值;|Δ| < 0.05 视为无变化(null = 不显示)
|
|
1284
|
-
const trend = createMemo(() => {
|
|
1285
|
-
const s = stats()
|
|
1286
|
-
if (!s || s.prevHitRate < 0 || s.hitRate < 0) return null
|
|
1287
|
-
const d = s.hitRate - s.prevHitRate
|
|
1288
|
-
return Math.abs(d) < 0.05 ? null : d
|
|
1289
|
-
})
|
|
1290
|
-
|
|
1291
|
-
const balanceText = createMemo(() => {
|
|
1292
|
-
const s = props.signals.balanceState()
|
|
1293
|
-
if (s.status === "ok" && s.data) return formatBalanceText(s.data, props.signals.balanceCurrency(), props.signals.exchangeRate())
|
|
1294
|
-
if (s.status === "loading") return "\u2026"
|
|
1295
|
-
if (s.status === "error") return "\u26a0"
|
|
1296
|
-
return "-"
|
|
1297
|
-
})
|
|
1298
|
-
|
|
1299
|
-
// 路径显示(替换宿主默认 hint 左侧的 cwd 文本)
|
|
1300
|
-
const directory = createMemo(() => {
|
|
1301
|
-
try { return props.api.state.path.directory } catch { return "" }
|
|
1302
|
-
})
|
|
1303
|
-
|
|
1304
|
-
// 恢复显隐偏好(默认显示);关闭时回退为仅显示路径,与宿主默认 hint 行一致
|
|
1305
|
-
onMount(() => {
|
|
1306
|
-
try {
|
|
1307
|
-
const v = props.api.kv.get<boolean>(`${KV_PREFIX}.section.bottom`, true)
|
|
1308
|
-
props.signals.setSectionBottom(v !== false)
|
|
1309
|
-
} catch {}
|
|
1310
|
-
})
|
|
1311
|
-
|
|
1312
|
-
return (
|
|
1313
|
-
<Show when={props.signals.sectionBottom()} fallback={<text fg={pal().muted}>{directory()}</text>}>
|
|
1314
|
-
<box marginLeft={1} flexGrow={1} flexShrink={0} flexDirection="row" justifyContent="space-between">
|
|
1315
|
-
<text fg={pal().muted}>{directory()}</text>
|
|
1316
|
-
<box flexDirection="row">
|
|
1317
|
-
<text>
|
|
1318
|
-
<span style={{ fg: pal().muted }}>{t("barHit")} </span>
|
|
1319
|
-
<span style={{ fg: hitColor() }}>{(stats()?.hitRate ?? -1) >= 0 ? (Math.floor(stats()!.hitRate * 10) / 10).toFixed(1) + "%" : "--"}</span>
|
|
1320
|
-
<Show when={trend() !== null}>
|
|
1321
|
-
<span style={{ fg: trend()! > 0 ? pal().success : pal().error }}>
|
|
1322
|
-
{" " + (trend()! > 0 ? "\u2191" : "\u2193") + Math.abs(trend()!).toFixed(1) + "%"}
|
|
1323
|
-
</span>
|
|
1324
|
-
</Show>
|
|
1325
|
-
<span style={{ fg: pal().muted }}>{" \u00b7 " + t("barTok") + " "}</span>
|
|
1326
|
-
<span style={{ fg: pal().text }}>
|
|
1327
|
-
{stats() ? fmtCompact(stats()!.input + stats()!.read + stats()!.write) : "--"}
|
|
1328
|
-
</span>
|
|
1329
|
-
<Show when={!props.signals.balanceUnsupported()}>
|
|
1330
|
-
<span style={{ fg: pal().muted }}>{" \u00b7 " + t("barBal") + " "}</span>
|
|
1331
|
-
<span style={{ fg: pal().text }}>{balanceText()}</span>
|
|
1332
|
-
</Show>
|
|
1333
|
-
</text>
|
|
1334
|
-
<text fg={pal().muted}>{" \u00b7 "}</text>
|
|
1335
|
-
</box>
|
|
1336
|
-
</box>
|
|
1337
|
-
</Show>
|
|
1338
|
-
)
|
|
1339
|
-
}
|
|
1340
|
-
|
|
1341
|
-
function createSidebarSlot(api: TuiPluginApi, signals: PanelSignals): TuiSlotPlugin {
|
|
1342
|
-
let lastSlotSid = ""
|
|
1343
|
-
return {
|
|
1344
|
-
order: 55,
|
|
1345
|
-
slots: {
|
|
1346
|
-
sidebar_content(ctx: TuiSlotContext, input: { session_id: string }): JSX.Element {
|
|
1347
|
-
// ── auto-clear override when the user navigates to a different main session ──
|
|
1348
|
-
if (input.session_id !== lastSlotSid) {
|
|
1349
|
-
lastSlotSid = input.session_id
|
|
1350
|
-
if (signals.overrideSessionId()) {
|
|
1351
|
-
signals.setOverrideSessionId(undefined)
|
|
1352
|
-
api.kv.set("cache_panel.session", "")
|
|
1353
|
-
}
|
|
1354
|
-
}
|
|
1355
|
-
return (
|
|
1356
|
-
<TokenCachePanel
|
|
1357
|
-
theme={ctx.theme.current}
|
|
1358
|
-
api={api}
|
|
1359
|
-
sessionId={input.session_id}
|
|
1360
|
-
signals={signals}
|
|
1361
|
-
/>
|
|
1362
|
-
)
|
|
1363
|
-
},
|
|
1364
|
-
},
|
|
1365
|
-
}
|
|
1366
|
-
}
|
|
1367
|
-
|
|
1368
|
-
const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
1369
|
-
// ── shared panel signals ──────────────────────────────────────
|
|
1370
|
-
const [currencySymbol, setCurrencySymbol] = createSignal("$")
|
|
1371
|
-
const [exchangeRate, setExchangeRate] = createSignal(1)
|
|
1372
|
-
const [sectionDetail, setSectionDetail] = createSignal(true)
|
|
1373
|
-
const [sectionModel, setSectionModel] = createSignal(true)
|
|
1374
|
-
const [sectionDist, setSectionDist] = createSignal(true)
|
|
1375
|
-
const [sectionSkills, setSectionSkills] = createSignal(true)
|
|
1376
|
-
const [sectionBalance, setSectionBalance] = createSignal(true)
|
|
1377
|
-
const [sectionBottom, setSectionBottom] = createSignal(true)
|
|
1378
|
-
const [balanceRefresh, setBalanceRefresh] = createSignal(0)
|
|
1379
|
-
const [balanceProviderId, setBalanceProviderId] = createSignal("deepseek")
|
|
1380
|
-
const [autoBalance, setAutoBalance] = createSignal(true)
|
|
1381
|
-
const [balanceUnsupported, setBalanceUnsupported] = createSignal(false)
|
|
1382
|
-
const [balanceCurrency, setBalanceCurrency] = createSignal("")
|
|
1383
|
-
const [borderVisible, setBorderVisible] = createSignal(true)
|
|
1384
|
-
const [langCode, setLangCode] = createSignal<LangCode>(INIT_LANG)
|
|
1385
|
-
const [overrideSessionId, setOverrideSessionId] = createSignal<string | undefined>(undefined)
|
|
1386
|
-
|
|
1387
|
-
// ── 余额查询状态(共享):侧边栏与底部栏读同一份数据,
|
|
1388
|
-
// 避免重复请求导致两处余额不一致 ──
|
|
1389
|
-
const [balanceState, setBalanceState] = createSignal<BalanceState>({
|
|
1390
|
-
status: "idle", data: null, lastFetch: 0,
|
|
1391
|
-
})
|
|
1392
|
-
// 请求序号:防止定时轮询与手动刷新并发时,慢的旧请求覆盖新结果
|
|
1393
|
-
let balanceSeq = 0
|
|
1394
|
-
|
|
1395
|
-
const signals: PanelSignals = {
|
|
1396
|
-
currencySymbol, setCurrencySymbol,
|
|
1397
|
-
exchangeRate, setExchangeRate,
|
|
1398
|
-
langCode, setLangCode,
|
|
1399
|
-
sectionDetail, setSectionDetail,
|
|
1400
|
-
sectionModel, setSectionModel,
|
|
1401
|
-
sectionDist, setSectionDist,
|
|
1402
|
-
sectionSkills, setSectionSkills,
|
|
1403
|
-
sectionBalance, setSectionBalance,
|
|
1404
|
-
sectionBottom, setSectionBottom,
|
|
1405
|
-
balanceRefresh, setBalanceRefresh,
|
|
1406
|
-
balanceProviderId, setBalanceProviderId,
|
|
1407
|
-
autoBalance, setAutoBalance,
|
|
1408
|
-
balanceUnsupported, setBalanceUnsupported,
|
|
1409
|
-
balanceState,
|
|
1410
|
-
balanceCurrency, setBalanceCurrency,
|
|
1411
|
-
borderVisible, setBorderVisible,
|
|
1412
|
-
overrideSessionId, setOverrideSessionId,
|
|
1413
|
-
}
|
|
1414
|
-
|
|
1415
|
-
api.slots.register(createSidebarSlot(api, signals))
|
|
1416
|
-
|
|
1417
|
-
// 输入框 hint 行(session_prompt slot,replace 模式):
|
|
1418
|
-
// 用宿主同一 Prompt 组件重渲染输入框,仅替换 hint 行左侧——
|
|
1419
|
-
// 在路径与右侧 token/commands 提示之间插入 命中率 · 余额 · Tokens。
|
|
1420
|
-
api.slots.register({
|
|
1421
|
-
order: 55,
|
|
1422
|
-
slots: {
|
|
1423
|
-
session_prompt(
|
|
1424
|
-
_ctx: TuiSlotContext,
|
|
1425
|
-
input: {
|
|
1426
|
-
session_id: string
|
|
1427
|
-
visible?: boolean
|
|
1428
|
-
disabled?: boolean
|
|
1429
|
-
on_submit?: () => void
|
|
1430
|
-
ref?: (ref: TuiPromptRef | undefined) => void
|
|
1431
|
-
},
|
|
1432
|
-
): JSX.Element {
|
|
1433
|
-
return (
|
|
1434
|
-
<api.ui.Prompt
|
|
1435
|
-
sessionID={input.session_id}
|
|
1436
|
-
visible={input.visible}
|
|
1437
|
-
disabled={input.disabled}
|
|
1438
|
-
onSubmit={input.on_submit}
|
|
1439
|
-
ref={input.ref}
|
|
1440
|
-
hint={<BottomStatusBar api={api} signals={signals} sessionId={input.session_id} />}
|
|
1441
|
-
/>
|
|
1442
|
-
)
|
|
1443
|
-
},
|
|
1444
|
-
},
|
|
1445
|
-
})
|
|
1446
|
-
|
|
1447
|
-
// ── slash commands for runtime config ──
|
|
1448
|
-
const KV_PREFIX = "cache_panel"
|
|
1449
|
-
|
|
1450
|
-
// ── 语言偏好恢复:KV 就绪后优先用户设置(/cache-lang),覆盖自动识别 ──
|
|
1451
|
-
const restoreLang = () => {
|
|
1452
|
-
try {
|
|
1453
|
-
const saved = api.kv.get<string>(`${KV_PREFIX}.lang`)
|
|
1454
|
-
if (saved && LANG_META.some((m) => m.code === saved)) setLangCode(saved as LangCode)
|
|
1455
|
-
} catch {}
|
|
1456
|
-
}
|
|
1457
|
-
if (api.kv.ready) {
|
|
1458
|
-
restoreLang()
|
|
1459
|
-
} else {
|
|
1460
|
-
const langTimer = setInterval(() => {
|
|
1461
|
-
if (api.kv.ready) { clearInterval(langTimer); restoreLang() }
|
|
1462
|
-
}, 10)
|
|
1463
|
-
api.lifecycle.onDispose(() => clearInterval(langTimer))
|
|
1464
|
-
}
|
|
1465
|
-
|
|
1466
|
-
const pollBalance = async () => {
|
|
1467
|
-
const provider = getBalanceProvider(balanceProviderId())
|
|
1468
|
-
// 手动配置的 key 优先;缺失时自动复用 OpenCode 已认证的 key(auth.json / config)
|
|
1469
|
-
const key = api.kv.get<string>(`${KV_PREFIX}.balance.${provider.id}.key`, "")
|
|
1470
|
-
|| findOpencodeKey(api, provider)
|
|
1471
|
-
if (balanceUnsupported()) { setBalanceState({ status: "idle", data: null, lastFetch: 0, error: undefined, key: undefined }); return }
|
|
1472
|
-
if (!key) { setBalanceState({ status: "idle", data: null, lastFetch: 0, error: undefined, key: undefined }); return }
|
|
1473
|
-
const now = Date.now()
|
|
1474
|
-
const prev = balanceState()
|
|
1475
|
-
// key 已更换(重新输入)→ 强制重新查询,绕过缓存
|
|
1476
|
-
if (prev.status === "ok" && prev.key === key && now - prev.lastFetch < BALANCE_POLL_MS) return // cache still fresh
|
|
1477
|
-
const seq = ++balanceSeq
|
|
1478
|
-
setBalanceState({ ...prev, status: "loading", error: undefined, key })
|
|
1479
|
-
const controller = new AbortController()
|
|
1480
|
-
let timedOut = false
|
|
1481
|
-
const timer = setTimeout(() => { timedOut = true; controller.abort() }, 10_000)
|
|
1482
|
-
try {
|
|
1483
|
-
const data = await provider.fetchBalance(key, controller.signal)
|
|
1484
|
-
clearTimeout(timer)
|
|
1485
|
-
if (seq !== balanceSeq) return // 已被更新的请求取代,丢弃过期结果
|
|
1486
|
-
setBalanceState({ status: "ok", data, lastFetch: Date.now(), error: undefined, key })
|
|
1487
|
-
} catch (err) {
|
|
1488
|
-
clearTimeout(timer)
|
|
1489
|
-
if (seq !== balanceSeq) return
|
|
1490
|
-
const code = timedOut ? "TIMEOUT" : (err instanceof Error ? err.message : "")
|
|
1491
|
-
// 失败时清空旧数据,避免显示过期余额
|
|
1492
|
-
setBalanceState({ status: "error", data: null, lastFetch: 0, error: code, key })
|
|
1493
|
-
}
|
|
1494
|
-
}
|
|
1495
|
-
|
|
1496
|
-
// Re-fetch when the API key is (re)configured via /cache-balance-key.
|
|
1497
|
-
// 注意:pollBalance 内部读写 balanceState 信号,若不做 untrack 包裹,
|
|
1498
|
-
// effect 会追踪 balanceState 的变化并与 pollBalance 的 setBalanceState
|
|
1499
|
-
// 形成无限循环(每次重跑都发起新的 fetch 请求)。
|
|
1500
|
-
createEffect(() => {
|
|
1501
|
-
void balanceRefresh()
|
|
1502
|
-
untrack(() => { void pollBalance() })
|
|
1503
|
-
})
|
|
1504
|
-
|
|
1505
|
-
// 定时轮询(5 分钟);随插件生命周期清理
|
|
1506
|
-
const balanceTimer = setInterval(pollBalance, BALANCE_POLL_MS)
|
|
1507
|
-
api.lifecycle.onDispose(() => clearInterval(balanceTimer))
|
|
1508
|
-
|
|
1509
|
-
/** 菜单中 provider 选项标题:标注 key 来源(手动配置 / OpenCode 自动复用 / 未配置)。 */
|
|
1510
|
-
const providerOptionTitle = (p: BalanceProvider, current?: string) => {
|
|
1511
|
-
const t = createT(() => langCode())
|
|
1512
|
-
const hasManual = !!api.kv.get<string>(`${KV_PREFIX}.balance.${p.id}.key`, "")
|
|
1513
|
-
const hasAuto = !hasManual && !!findOpencodeKey(api, p)
|
|
1514
|
-
const mark = hasManual
|
|
1515
|
-
? t("keyUser")
|
|
1516
|
-
: hasAuto
|
|
1517
|
-
? t("keyOpenCode")
|
|
1518
|
-
: t("keyNotSet")
|
|
1519
|
-
return p.name + mark + (current && p.id === current ? " *" : "")
|
|
1520
|
-
}
|
|
1521
|
-
|
|
1522
|
-
/** 弹出指定 provider 的 API Key 输入框(脱敏预填;空清除 / 含 * 保留原 key / 新 key 实时刷新)。 */
|
|
1523
|
-
const promptBalanceKey = (dialog: TuiDialogStack | undefined, provider: BalanceProvider) => {
|
|
1524
|
-
const t = createT(() => langCode())
|
|
1525
|
-
const current = api.kv.get<string>(`${KV_PREFIX}.balance.${provider.id}.key`, "")
|
|
1526
|
-
const masked = maskKey(current)
|
|
1527
|
-
dialog?.replace(() => (
|
|
1528
|
-
<api.ui.DialogPrompt
|
|
1529
|
-
title={provider.name}
|
|
1530
|
-
description={() => <text>{t("balKeyPrompt", { p: provider.name })}</text>}
|
|
1531
|
-
placeholder={provider.keyPlaceholder ?? "sk-..."}
|
|
1532
|
-
value={masked}
|
|
1533
|
-
onConfirm={(val) => {
|
|
1534
|
-
const input = val.trim()
|
|
1535
|
-
let key: string
|
|
1536
|
-
if (input === "") {
|
|
1537
|
-
key = ""
|
|
1538
|
-
} else if (input.includes("*")) {
|
|
1539
|
-
key = current
|
|
1540
|
-
} else {
|
|
1541
|
-
key = input
|
|
1542
|
-
}
|
|
1543
|
-
api.kv.set(`${KV_PREFIX}.balance.${provider.id}.key`, key)
|
|
1544
|
-
setBalanceRefresh(v => v + 1)
|
|
1545
|
-
if (key) {
|
|
1546
|
-
api.ui.toast({ message: t("keySaved") })
|
|
1547
|
-
} else {
|
|
1548
|
-
api.ui.toast({ message: t("keyCleared") })
|
|
1549
|
-
}
|
|
1550
|
-
dialog?.clear()
|
|
1551
|
-
}}
|
|
1552
|
-
onCancel={() => dialog?.clear()}
|
|
1553
|
-
/>
|
|
1554
|
-
))
|
|
1555
|
-
}
|
|
1556
|
-
|
|
1557
|
-
api.command?.register(() => [
|
|
1558
|
-
{
|
|
1559
|
-
title: "Cache: Set Currency",
|
|
1560
|
-
value: "cache.currency",
|
|
1561
|
-
description: "Change the currency unit for cost display",
|
|
1562
|
-
slash: { name: "cache-currency" },
|
|
1563
|
-
onSelect: (dialog) => {
|
|
1564
|
-
dialog?.replace(() => (
|
|
1565
|
-
<api.ui.DialogSelect
|
|
1566
|
-
title="Select Currency"
|
|
1567
|
-
options={Object.entries(CURRENCIES).map(([code, sym]) => ({
|
|
1568
|
-
title: `${code} (${sym})`,
|
|
1569
|
-
value: code,
|
|
1570
|
-
}))}
|
|
1571
|
-
onSelect={(opt) => {
|
|
1572
|
-
const t = createT(() => langCode())
|
|
1573
|
-
const sym = CURRENCIES[opt.value] ?? "$"
|
|
1574
|
-
const defRate = DEFAULT_RATES[opt.value] ?? 1
|
|
1575
|
-
api.kv.set(`${KV_PREFIX}.currency`, sym)
|
|
1576
|
-
api.kv.set(`${KV_PREFIX}.rate`, defRate)
|
|
1577
|
-
// 同步余额显示币种偏好:CNY/USD 原生直显,其余币种按汇率换算
|
|
1578
|
-
api.kv.set(`${KV_PREFIX}.balance_currency`, opt.value)
|
|
1579
|
-
signals.setBalanceCurrency(opt.value)
|
|
1580
|
-
signals.setCurrencySymbol(sym)
|
|
1581
|
-
signals.setExchangeRate(defRate)
|
|
1582
|
-
api.ui.toast({ message: t("currencySet", { v: opt.value, s: sym, r: defRate }) })
|
|
1583
|
-
dialog?.clear()
|
|
1584
|
-
}}
|
|
1585
|
-
/>
|
|
1586
|
-
))
|
|
1587
|
-
},
|
|
1588
|
-
},
|
|
1589
|
-
{
|
|
1590
|
-
title: "Cache: Set Exchange Rate",
|
|
1591
|
-
value: "cache.rate",
|
|
1592
|
-
description: "Set the exchange rate multiplier for the selected currency",
|
|
1593
|
-
slash: { name: "cache-rate" },
|
|
1594
|
-
onSelect: (dialog) => {
|
|
1595
|
-
dialog?.replace(() => (
|
|
1596
|
-
<api.ui.DialogPrompt
|
|
1597
|
-
title="Exchange Rate"
|
|
1598
|
-
description={() => <text>Enter the exchange rate from USD to your currency (e.g. 7.2 for CNY)</text>}
|
|
1599
|
-
placeholder="1.0"
|
|
1600
|
-
value={String(api.kv.get<number>(`${KV_PREFIX}.rate`, 1))}
|
|
1601
|
-
onConfirm={(val) => {
|
|
1602
|
-
const t = createT(() => langCode())
|
|
1603
|
-
const n = parseFloat(val)
|
|
1604
|
-
if (n > 0) {
|
|
1605
|
-
api.kv.set(`${KV_PREFIX}.rate`, n)
|
|
1606
|
-
signals.setExchangeRate(n)
|
|
1607
|
-
api.ui.toast({ message: t("rateSet", { r: n }) })
|
|
1608
|
-
}
|
|
1609
|
-
dialog?.clear()
|
|
1610
|
-
}}
|
|
1611
|
-
/>
|
|
1612
|
-
))
|
|
1613
|
-
},
|
|
1614
|
-
},
|
|
1615
|
-
{
|
|
1616
|
-
title: "Cache: Toggle Section",
|
|
1617
|
-
value: "cache.section",
|
|
1618
|
-
description: "Show or hide a sidebar section",
|
|
1619
|
-
slash: { name: "cache-section" },
|
|
1620
|
-
onSelect: (dialog) => {
|
|
1621
|
-
const t = createT(() => langCode())
|
|
1622
|
-
const detailOn = Boolean(api.kv.get(`${KV_PREFIX}.section.detail`, true))
|
|
1623
|
-
const modelOn = Boolean(api.kv.get(`${KV_PREFIX}.section.model`, true))
|
|
1624
|
-
const distOn = Boolean(api.kv.get(`${KV_PREFIX}.section.dist`, true))
|
|
1625
|
-
const skillsOn = Boolean(api.kv.get(`${KV_PREFIX}.section.skills`, true))
|
|
1626
|
-
const balanceOn = Boolean(api.kv.get(`${KV_PREFIX}.section.balance`, true))
|
|
1627
|
-
const bottomOn = Boolean(api.kv.get(`${KV_PREFIX}.section.bottom`, true))
|
|
1628
|
-
const borderOn = Boolean(api.kv.get(`${KV_PREFIX}.border`, true))
|
|
1629
|
-
const labels: Record<string, string> = {
|
|
1630
|
-
detail: t("secDetail"),
|
|
1631
|
-
model: t("secModel"),
|
|
1632
|
-
dist: t("distTitle"),
|
|
1633
|
-
skills: t("secSkills"),
|
|
1634
|
-
balance: t("secBalance"),
|
|
1635
|
-
bottom: t("secBottom"),
|
|
1636
|
-
border: t("secBorder"),
|
|
1637
|
-
}
|
|
1638
|
-
const optTitle = (label: string, on: boolean) => `${visualPadEnd(label, 15)}[${on ? "ON" : "OFF"}]`
|
|
1639
|
-
dialog?.replace(() => (
|
|
1640
|
-
<api.ui.DialogSelect
|
|
1641
|
-
title={t("secToggle")}
|
|
1642
|
-
options={[
|
|
1643
|
-
{ title: optTitle(labels.detail, detailOn), value: "detail" },
|
|
1644
|
-
{ title: optTitle(labels.model, modelOn), value: "model" },
|
|
1645
|
-
{ title: optTitle(labels.dist, distOn), value: "dist" },
|
|
1646
|
-
{ title: optTitle(labels.skills, skillsOn), value: "skills" },
|
|
1647
|
-
{ title: optTitle(labels.balance, balanceOn), value: "balance" },
|
|
1648
|
-
{ title: optTitle(labels.bottom, bottomOn), value: "bottom" },
|
|
1649
|
-
{ title: optTitle(labels.border, borderOn), value: "border" },
|
|
1650
|
-
]}
|
|
1651
|
-
onSelect={(opt) => {
|
|
1652
|
-
if (opt.value === "border") {
|
|
1653
|
-
const cur = Boolean(api.kv.get(`${KV_PREFIX}.border`, true))
|
|
1654
|
-
api.kv.set(`${KV_PREFIX}.border`, !cur)
|
|
1655
|
-
signals.setBorderVisible(!cur)
|
|
1656
|
-
api.ui.toast({ message: !cur ? t("borderShown") : t("borderHidden") })
|
|
1657
|
-
} else {
|
|
1658
|
-
const key = `${KV_PREFIX}.section.${opt.value}`
|
|
1659
|
-
const cur = Boolean(api.kv.get(key, true))
|
|
1660
|
-
api.kv.set(key, !cur)
|
|
1661
|
-
if (opt.value === "detail") signals.setSectionDetail(!cur)
|
|
1662
|
-
if (opt.value === "model") signals.setSectionModel(!cur)
|
|
1663
|
-
if (opt.value === "dist") signals.setSectionDist(!cur)
|
|
1664
|
-
if (opt.value === "skills") signals.setSectionSkills(!cur)
|
|
1665
|
-
if (opt.value === "balance") signals.setSectionBalance(!cur)
|
|
1666
|
-
if (opt.value === "bottom") signals.setSectionBottom(!cur)
|
|
1667
|
-
const name = labels[opt.value] ?? opt.value
|
|
1668
|
-
api.ui.toast({ message: t(!cur ? "sectionShown" : "sectionHidden", { s: name }) })
|
|
1669
|
-
}
|
|
1670
|
-
dialog?.clear()
|
|
1671
|
-
}}
|
|
1672
|
-
/>
|
|
1673
|
-
))
|
|
1674
|
-
},
|
|
1675
|
-
},
|
|
1676
|
-
{
|
|
1677
|
-
title: "Cache: Show Config",
|
|
1678
|
-
value: "cache.config",
|
|
1679
|
-
description: "Display the current plugin configuration",
|
|
1680
|
-
slash: { name: "cache-config" },
|
|
1681
|
-
onSelect: (dialog) => {
|
|
1682
|
-
const t = createT(() => langCode())
|
|
1683
|
-
const sym = api.kv.get<string>(`${KV_PREFIX}.currency`) ?? "$"
|
|
1684
|
-
const rate = api.kv.get<number>(`${KV_PREFIX}.rate`) ?? 1
|
|
1685
|
-
const detail = Boolean(api.kv.get(`${KV_PREFIX}.section.detail`, true))
|
|
1686
|
-
const model = Boolean(api.kv.get(`${KV_PREFIX}.section.model`, true))
|
|
1687
|
-
const dist = Boolean(api.kv.get(`${KV_PREFIX}.section.dist`, true))
|
|
1688
|
-
const skills = Boolean(api.kv.get(`${KV_PREFIX}.section.skills`, true))
|
|
1689
|
-
const balance = Boolean(api.kv.get(`${KV_PREFIX}.section.balance`, true))
|
|
1690
|
-
const bottom = Boolean(api.kv.get(`${KV_PREFIX}.section.bottom`, true))
|
|
1691
|
-
const on = (v: boolean) => v ? "ON" : "OFF"
|
|
1692
|
-
api.ui.toast({
|
|
1693
|
-
title: t("panelConfigTitle"),
|
|
1694
|
-
message: t("panelConfigMsg", {
|
|
1695
|
-
c: sym, r: rate,
|
|
1696
|
-
d: on(detail), m: on(model),
|
|
1697
|
-
t: on(dist), k: on(skills),
|
|
1698
|
-
b: on(balance), f: on(bottom),
|
|
1699
|
-
}),
|
|
1700
|
-
duration: 8000,
|
|
1701
|
-
})
|
|
1702
|
-
dialog?.clear()
|
|
1703
|
-
},
|
|
1704
|
-
},
|
|
1705
|
-
{
|
|
1706
|
-
title: "Cache: Switch Language",
|
|
1707
|
-
value: "cache.lang",
|
|
1708
|
-
description: "Switch between Chinese and English display",
|
|
1709
|
-
slash: { name: "cache-lang" },
|
|
1710
|
-
onSelect: (dialog) => {
|
|
1711
|
-
const t = createT(() => langCode())
|
|
1712
|
-
const cur = langCode()
|
|
1713
|
-
dialog?.replace(() => (
|
|
1714
|
-
<api.ui.DialogSelect
|
|
1715
|
-
title={t("langTitle")}
|
|
1716
|
-
options={LANG_META.map((m) => ({
|
|
1717
|
-
title: `${visualPadEnd(m.label, 9)}${cur === m.code ? "\u2713" : ""}`,
|
|
1718
|
-
value: m.code,
|
|
1719
|
-
}))}
|
|
1720
|
-
onSelect={(opt) => {
|
|
1721
|
-
const code = opt.value as LangCode
|
|
1722
|
-
api.kv.set(`${KV_PREFIX}.lang`, code)
|
|
1723
|
-
setLangCode(code)
|
|
1724
|
-
api.ui.toast({ message: t("langSwitched") })
|
|
1725
|
-
dialog?.clear()
|
|
1726
|
-
}}
|
|
1727
|
-
/>
|
|
1728
|
-
))
|
|
1729
|
-
},
|
|
1730
|
-
},
|
|
1731
|
-
{
|
|
1732
|
-
title: "Cache: Switch Balance Provider",
|
|
1733
|
-
value: "cache.balance",
|
|
1734
|
-
description: "切换余额提供商 / 自动切换当前会话提供商 | Switch balance provider / auto-switch session provider",
|
|
1735
|
-
slash: { name: "cache-balance" },
|
|
1736
|
-
onSelect: (dialog) => {
|
|
1737
|
-
const t = createT(() => langCode())
|
|
1738
|
-
const current = signals.balanceProviderId()
|
|
1739
|
-
const auto = signals.autoBalance()
|
|
1740
|
-
const autoLabel = `${t("autoSwitchOpt")} [${auto ? "ON" : "OFF"}]`
|
|
1741
|
-
dialog?.replace(() => (
|
|
1742
|
-
<api.ui.DialogSelect
|
|
1743
|
-
title={t("balProvTitle")}
|
|
1744
|
-
options={[
|
|
1745
|
-
{
|
|
1746
|
-
title: autoLabel,
|
|
1747
|
-
value: "__auto__",
|
|
1748
|
-
},
|
|
1749
|
-
...balanceProviders.map((p) => ({
|
|
1750
|
-
title: providerOptionTitle(p, current),
|
|
1751
|
-
value: p.id,
|
|
1752
|
-
})),
|
|
1753
|
-
]}
|
|
1754
|
-
onSelect={(opt) => {
|
|
1755
|
-
if (opt.value === "__auto__") {
|
|
1756
|
-
const next = !auto
|
|
1757
|
-
api.kv.set(`${KV_PREFIX}.balance.auto`, next)
|
|
1758
|
-
signals.setAutoBalance(next)
|
|
1759
|
-
api.ui.toast({ message: next ? t("autoSwitchOn") : t("autoSwitchOff") })
|
|
1760
|
-
dialog?.clear()
|
|
1761
|
-
} else {
|
|
1762
|
-
const provider = getBalanceProvider(opt.value)
|
|
1763
|
-
// 手动切换会关闭自动切换
|
|
1764
|
-
api.kv.set(`${KV_PREFIX}.balance.provider`, provider.id)
|
|
1765
|
-
api.kv.set(`${KV_PREFIX}.balance.auto`, false)
|
|
1766
|
-
signals.setBalanceProviderId(provider.id)
|
|
1767
|
-
signals.setAutoBalance(false)
|
|
1768
|
-
signals.setBalanceUnsupported(false)
|
|
1769
|
-
// 切换后立即按新 provider 刷新显示(无 key 时显示 idle,避免残留上一 provider 余额)
|
|
1770
|
-
signals.setBalanceRefresh(signals.balanceRefresh() + 1)
|
|
1771
|
-
const hasKey = !!api.kv.get<string>(`${KV_PREFIX}.balance.${provider.id}.key`, "")
|
|
1772
|
-
if (!hasKey) {
|
|
1773
|
-
// 未配置 key → 进入设置流程(对话框保持打开等待输入)
|
|
1774
|
-
promptBalanceKey(dialog, provider)
|
|
1775
|
-
} else {
|
|
1776
|
-
api.ui.toast({ message: t("providerManual", { p: provider.name }) })
|
|
1777
|
-
dialog?.clear()
|
|
1778
|
-
}
|
|
1779
|
-
}
|
|
1780
|
-
}}
|
|
1781
|
-
/>
|
|
1782
|
-
))
|
|
1783
|
-
},
|
|
1784
|
-
},
|
|
1785
|
-
{
|
|
1786
|
-
title: "Cache: Set Balance API Key",
|
|
1787
|
-
value: "cache.balance.key",
|
|
1788
|
-
description: "Select a provider and set its API key for balance display",
|
|
1789
|
-
slash: { name: "cache-balance-key" },
|
|
1790
|
-
onSelect: (dialog) => {
|
|
1791
|
-
const t = createT(() => langCode())
|
|
1792
|
-
// 步骤 1:选择 provider
|
|
1793
|
-
dialog?.replace(() => (
|
|
1794
|
-
<api.ui.DialogSelect
|
|
1795
|
-
title={t("balSelectTitle")}
|
|
1796
|
-
options={balanceProviders.map((p) => ({
|
|
1797
|
-
title: providerOptionTitle(p),
|
|
1798
|
-
value: p.id,
|
|
1799
|
-
}))}
|
|
1800
|
-
onSelect={(opt) => {
|
|
1801
|
-
const provider = getBalanceProvider(opt.value)
|
|
1802
|
-
// 手动指定 provider 会关闭自动切换
|
|
1803
|
-
api.kv.set(`${KV_PREFIX}.balance.provider`, provider.id)
|
|
1804
|
-
api.kv.set(`${KV_PREFIX}.balance.auto`, false)
|
|
1805
|
-
signals.setBalanceProviderId(provider.id)
|
|
1806
|
-
signals.setAutoBalance(false)
|
|
1807
|
-
// 切换后立即刷新显示(防止取消输入时残留上一 provider 的余额)
|
|
1808
|
-
signals.setBalanceRefresh(signals.balanceRefresh() + 1)
|
|
1809
|
-
// 步骤 2:输入 key
|
|
1810
|
-
promptBalanceKey(dialog, provider)
|
|
1811
|
-
}}
|
|
1812
|
-
/>
|
|
1813
|
-
))
|
|
1814
|
-
},
|
|
1815
|
-
},
|
|
1816
|
-
{
|
|
1817
|
-
title: "Cache: Debug Skills Detection",
|
|
1818
|
-
value: "cache.debug-skills",
|
|
1819
|
-
description: "Dump all tool parts found in the current session for skill detection debugging",
|
|
1820
|
-
slash: { name: "cache-debug-skills" },
|
|
1821
|
-
onSelect: () => {
|
|
1822
|
-
const t = createT(() => langCode())
|
|
1823
|
-
const rt = api.route.current
|
|
1824
|
-
if (rt.name !== "session" || !rt.params) {
|
|
1825
|
-
api.ui.toast({ message: t("runInSession"), variant: "warning" })
|
|
1826
|
-
return
|
|
1827
|
-
}
|
|
1828
|
-
const sid = String(rt.params.sessionID)
|
|
1829
|
-
const msgs = api.state.session.messages(sid)
|
|
1830
|
-
const byTool: Record<string, number> = {}
|
|
1831
|
-
const skillParts: string[] = []
|
|
1832
|
-
for (const msg of msgs) {
|
|
1833
|
-
if (msg.role !== "assistant") continue
|
|
1834
|
-
let parts: readonly any[] = []
|
|
1835
|
-
try { parts = api.state.part(msg.id) } catch {}
|
|
1836
|
-
for (const p of parts) {
|
|
1837
|
-
if (p.type === "tool") {
|
|
1838
|
-
const t = String(p.tool ?? "?")
|
|
1839
|
-
byTool[t] = (byTool[t] ?? 0) + 1
|
|
1840
|
-
if (t === "skill") {
|
|
1841
|
-
const meta = p.state?.metadata
|
|
1842
|
-
const rootMeta = p.metadata
|
|
1843
|
-
skillParts.push(`state.metadata=${JSON.stringify(meta)} | root.metadata=${JSON.stringify(rootMeta)} | state.title="${p.state?.title}" | state.output[:80]="${String(p.state?.output ?? "").slice(0, 80)}"`)
|
|
1844
|
-
}
|
|
1845
|
-
}
|
|
1846
|
-
}
|
|
1847
|
-
}
|
|
1848
|
-
const summary = Object.entries(byTool).map(([k, v]) => `${k}: ${v}`).join(" | ")
|
|
1849
|
-
const extra = skillParts.length > 0 ? "\n\nSkill parts:\n" + skillParts.join("\n") : "\n\n⚠ No skill tool parts found — AI may be reading SKILL.md instead. Try: 'Use the skill tool to load karpathy-guidelines'"
|
|
1850
|
-
api.ui.toast({
|
|
1851
|
-
title: `Tool Summary (${Object.keys(byTool).length} types)`,
|
|
1852
|
-
message: summary + extra,
|
|
1853
|
-
duration: 15000,
|
|
1854
|
-
})
|
|
1855
|
-
},
|
|
1856
|
-
},
|
|
1857
|
-
{
|
|
1858
|
-
title: "Cache: Sub-Agent Stats",
|
|
1859
|
-
value: "cache.session",
|
|
1860
|
-
description: "View token cache statistics for a sub-agent by session ID",
|
|
1861
|
-
slash: { name: "cache-session" },
|
|
1862
|
-
onSelect: (dialog) => {
|
|
1863
|
-
// ── 扫描当前主 session 的子代理 session ID 列表 ──
|
|
1864
|
-
const rt = api.route.current
|
|
1865
|
-
const parentSid = rt.name === "session" && rt.params ? String(rt.params.sessionID) : ""
|
|
1866
|
-
const SUBAGENT_TOOLS = new Set(["task", "delegate", "call_omo_agent"])
|
|
1867
|
-
|
|
1868
|
-
interface ChildEntry { title: string; value: string; description: string }
|
|
1869
|
-
const children: ChildEntry[] = []
|
|
1870
|
-
if (parentSid) {
|
|
1871
|
-
try {
|
|
1872
|
-
const msgs = api.state.session.messages(parentSid)
|
|
1873
|
-
for (const msg of msgs) {
|
|
1874
|
-
if (msg.role !== "assistant") continue
|
|
1875
|
-
let parts: readonly Part[] = []
|
|
1876
|
-
try { parts = api.state.part(msg.id) } catch {}
|
|
1877
|
-
for (const p of parts) {
|
|
1878
|
-
if (p.type !== "tool") continue
|
|
1879
|
-
const tool = String((p as ToolPart).tool ?? "")
|
|
1880
|
-
if (!SUBAGENT_TOOLS.has(tool)) continue
|
|
1881
|
-
const st = (p as any).state as Record<string, unknown> | undefined
|
|
1882
|
-
const stMeta = st?.metadata as Record<string, unknown> | undefined
|
|
1883
|
-
const subSid = stMeta?.session_id ?? stMeta?.sessionId
|
|
1884
|
-
if (!subSid) continue
|
|
1885
|
-
const sidStr = String(subSid)
|
|
1886
|
-
const input = st?.input as Record<string, unknown> | undefined
|
|
1887
|
-
const agent = String((p as any).subagent_type ?? input?.subagent_type ?? input?.category ?? tool)
|
|
1888
|
-
const prompt = String(input?.prompt ?? "")
|
|
1889
|
-
const desc = input?.description ? String(input.description) : ""
|
|
1890
|
-
const title = desc || prompt.replace(/\n/g, " ").replace(/\s+/g, " ").trim().slice(0, 40) || agent
|
|
1891
|
-
children.push({ title, value: sidStr, description: `${agent} · ${sidStr.slice(0, 24)}…` })
|
|
1892
|
-
}
|
|
1893
|
-
}
|
|
1894
|
-
} catch {}
|
|
1895
|
-
}
|
|
1896
|
-
|
|
1897
|
-
// 去重
|
|
1898
|
-
const seen = new Set<string>()
|
|
1899
|
-
const unique = children.filter(c => { if (seen.has(c.value)) return false; seen.add(c.value); return true })
|
|
1900
|
-
|
|
1901
|
-
if (unique.length > 0) {
|
|
1902
|
-
// ── 有子代理 → DialogSelect 列表选择 ──
|
|
1903
|
-
const t = createT(() => langCode())
|
|
1904
|
-
const currentSid = signals.overrideSessionId() ?? api.kv.get<string>(`${KV_PREFIX}.session`, "")
|
|
1905
|
-
const options = unique.map((c, i) => ({
|
|
1906
|
-
title: `${i + 1}. ${c.title}`,
|
|
1907
|
-
value: c.value,
|
|
1908
|
-
description: c.description,
|
|
1909
|
-
}))
|
|
1910
|
-
// 首尾各放一个"回到主会话",长列表时顶部底部均可直达
|
|
1911
|
-
const backValue = "__main__"
|
|
1912
|
-
const backTitle = `\u2500 ${t("backToMainTitle")}`
|
|
1913
|
-
options.unshift({ title: backTitle, value: backValue, description: "" })
|
|
1914
|
-
options.push({ title: backTitle, value: backValue, description: "" })
|
|
1915
|
-
const currentIdx = currentSid ? options.findIndex(o => o.value === currentSid) : -1
|
|
1916
|
-
dialog?.replace(() => (
|
|
1917
|
-
<api.ui.DialogSelect
|
|
1918
|
-
title={t("subSelectTitle")}
|
|
1919
|
-
options={options}
|
|
1920
|
-
current={currentIdx >= 0 ? options[currentIdx].value : undefined}
|
|
1921
|
-
onSelect={(opt) => {
|
|
1922
|
-
if (opt.value === backValue) {
|
|
1923
|
-
signals.setOverrideSessionId(undefined)
|
|
1924
|
-
api.kv.set(`${KV_PREFIX}.session`, "")
|
|
1925
|
-
api.ui.toast({ message: t("backToMain") })
|
|
1926
|
-
} else {
|
|
1927
|
-
signals.setOverrideSessionId(opt.value)
|
|
1928
|
-
api.kv.set(`${KV_PREFIX}.session`, opt.value)
|
|
1929
|
-
api.ui.toast({ message: t("subAgentSwitched", { s: opt.value.slice(0, 24) + "\u2026" }) })
|
|
1930
|
-
}
|
|
1931
|
-
dialog?.clear()
|
|
1932
|
-
}}
|
|
1933
|
-
/>
|
|
1934
|
-
))
|
|
1935
|
-
} else {
|
|
1936
|
-
// ── 无子代理 → DialogPrompt 手动粘贴 ──
|
|
1937
|
-
const t = createT(() => langCode())
|
|
1938
|
-
dialog?.replace(() => (
|
|
1939
|
-
<api.ui.DialogPrompt
|
|
1940
|
-
title={signals.overrideSessionId() ? t("subSwitchTitle") : t("subViewTitle")}
|
|
1941
|
-
description={() => <text>{t("subNoFound")}</text>}
|
|
1942
|
-
placeholder="ses_..."
|
|
1943
|
-
value={signals.overrideSessionId() ?? api.kv.get<string>(`${KV_PREFIX}.session`, "") ?? ""}
|
|
1944
|
-
onConfirm={(val) => {
|
|
1945
|
-
const sid = val.trim()
|
|
1946
|
-
if (sid) {
|
|
1947
|
-
signals.setOverrideSessionId(sid)
|
|
1948
|
-
api.kv.set(`${KV_PREFIX}.session`, sid)
|
|
1949
|
-
api.ui.toast({ message: t("subAgentSwitched", { s: sid.slice(0, 24) + "\u2026" }) })
|
|
1950
|
-
}
|
|
1951
|
-
dialog?.clear()
|
|
1952
|
-
}}
|
|
1953
|
-
onCancel={() => dialog?.clear()}
|
|
1954
|
-
/>
|
|
1955
|
-
))
|
|
1956
|
-
}
|
|
1957
|
-
},
|
|
1958
|
-
},
|
|
1959
|
-
{
|
|
1960
|
-
title: "Cache: Back to Main",
|
|
1961
|
-
value: "cache.session.back",
|
|
1962
|
-
description: "Return to main session stats",
|
|
1963
|
-
slash: { name: "cache-session-back" },
|
|
1964
|
-
onSelect: (dialog) => {
|
|
1965
|
-
const t = createT(() => langCode())
|
|
1966
|
-
signals.setOverrideSessionId(undefined)
|
|
1967
|
-
api.kv.set(`${KV_PREFIX}.session`, "")
|
|
1968
|
-
api.ui.toast({ message: t("backToMain") })
|
|
1969
|
-
dialog?.clear()
|
|
1970
|
-
},
|
|
1971
|
-
},
|
|
1972
|
-
])
|
|
1973
|
-
}
|
|
1974
|
-
|
|
1975
|
-
const mod: TuiPluginModule & { id: string } = {
|
|
1976
|
-
id: "opencode-visual-cache",
|
|
1977
|
-
tui,
|
|
1978
|
-
}
|
|
1979
|
-
|
|
1980
|
-
export default mod
|
|
1
|
+
/** @jsxImportSource @opentui/solid */
|
|
2
|
+
|
|
3
|
+
import type { JSX } from "@opentui/solid"
|
|
4
|
+
import type {
|
|
5
|
+
TuiPlugin,
|
|
6
|
+
TuiPluginApi,
|
|
7
|
+
TuiSlotContext,
|
|
8
|
+
TuiSlotPlugin,
|
|
9
|
+
TuiPluginModule,
|
|
10
|
+
TuiThemeCurrent,
|
|
11
|
+
TuiDialogStack,
|
|
12
|
+
TuiPromptRef,
|
|
13
|
+
SequenceBindingLike,
|
|
14
|
+
} from "@opencode-ai/plugin/tui"
|
|
15
|
+
import type { UserMessage, AssistantMessage, Message } from "@opencode-ai/sdk"
|
|
16
|
+
import type {
|
|
17
|
+
Part,
|
|
18
|
+
TextPart,
|
|
19
|
+
ToolPart,
|
|
20
|
+
FilePart,
|
|
21
|
+
ReasoningPart,
|
|
22
|
+
} from "@opencode-ai/sdk/v2"
|
|
23
|
+
import { createMemo, createSignal, createEffect, onMount, onCleanup, Show, For, untrack } from "solid-js"
|
|
24
|
+
import { PLUGIN_VERSION } from "./_version"
|
|
25
|
+
import { balanceProviders, getBalanceProvider, maskKey, matchBalanceProvider, type BalanceEntry, type BalanceProvider } from "./balance-providers"
|
|
26
|
+
import { LANG_META, createT, detectLang, type LangCode } from "./i18n"
|
|
27
|
+
import {
|
|
28
|
+
MAX_SAT, FALLBACK, CURRENCIES, DEFAULT_RATES,
|
|
29
|
+
charColumns, visualWidth, visualPadEnd, truncateVisual, progressBar,
|
|
30
|
+
fmt, num, fmtCost, fmtCompact, formatBalanceAmount, formatBalanceText,
|
|
31
|
+
balanceSymbol, convertBalance, estimateTokens,
|
|
32
|
+
rgb, saturation, desaturateTo, dimColor,
|
|
33
|
+
type TokenDist,
|
|
34
|
+
} from "./core"
|
|
35
|
+
import { TokenCachePanel } from "./panel/TokenCachePanel"
|
|
36
|
+
import type { PanelSignals, BalanceState } from "./panel/panel-api"
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Helpers
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
// Bun / Node globals — available at runtime in the OpenCode TUI process
|
|
43
|
+
declare const process: { env: Record<string, string | undefined> } | undefined
|
|
44
|
+
// ── language ──────────────────────────────────────────────────────
|
|
45
|
+
// 语言初始化:环境变量 CACHE_TUI_LANG 覆盖 → 否则按系统 locale 自动检测。
|
|
46
|
+
// 用户通过 /cache-lang 设置的偏好会在 KV 就绪后优先覆盖(见 tui() 内恢复逻辑)。
|
|
47
|
+
|
|
48
|
+
const DEBUG_LANG = typeof process !== "undefined" ? process.env?.CACHE_TUI_LANG : undefined
|
|
49
|
+
const INIT_LANG: LangCode = DEBUG_LANG !== undefined && LANG_META.some((m) => m.code === DEBUG_LANG)
|
|
50
|
+
? (DEBUG_LANG as LangCode)
|
|
51
|
+
: detectLang()
|
|
52
|
+
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Balance state
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
const BALANCE_POLL_MS = 5 * 60 * 1000 // 5 minutes
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 从 OpenCode 已认证的 provider 读取 API key 作为余额查询的自动兜底。
|
|
62
|
+
* 匹配复用前缀逻辑:先精确匹配 id,再前缀匹配(如 moonshotai-cn → moonshot)。
|
|
63
|
+
* key 来源:auth.json(provider.key)或配置(provider.options.apiKey)。
|
|
64
|
+
* 仅当手动配置的 key 缺失时使用;读取失败或未匹配返回空串。
|
|
65
|
+
*/
|
|
66
|
+
function findOpencodeKey(api: TuiPluginApi, provider: BalanceProvider): string {
|
|
67
|
+
try {
|
|
68
|
+
const provs = api.state.provider as unknown as Array<{ id: string; key?: string; options?: { apiKey?: string } }>
|
|
69
|
+
// 大小写不敏感:精确匹配 id,否则前缀匹配(如 moonshotai-cn → moonshot)
|
|
70
|
+
const id = provider.id.toLowerCase()
|
|
71
|
+
const hit = provs.find((p) => p.id.toLowerCase() === id) ?? provs.find((p) => p.id.toLowerCase().startsWith(id))
|
|
72
|
+
if (!hit) return ""
|
|
73
|
+
const k = typeof hit.key === "string" ? hit.key : ""
|
|
74
|
+
if (k) return k
|
|
75
|
+
return typeof hit.options?.apiKey === "string" ? hit.options.apiKey : ""
|
|
76
|
+
} catch {
|
|
77
|
+
return ""
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// Sidebar component
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
/** Signals shared between the TUI component and slash commands.
|
|
86
|
+
* Created in the `tui` function scope so they do not survive module reload —
|
|
87
|
+
* the component re-creates them on mount and restores user config from kv. */
|
|
88
|
+
|
|
89
|
+
const MIN_PANEL_WIDTH = 20
|
|
90
|
+
const DEFAULT_PANEL_WIDTH = 26
|
|
91
|
+
|
|
92
|
+
/** ── layout measurement constants (visual columns) ── */
|
|
93
|
+
const LABEL_GAP = 1 // label(如 "Hit")后面的空格
|
|
94
|
+
const BAR_BRACKETS = 2 // "[" + "]" 包围进度条
|
|
95
|
+
const BAR_GAP = 1 // "]" 后面的空格
|
|
96
|
+
const PCT_FIXED_WIDTH = 5 // "XX.X%" 固定 5 字符宽度
|
|
97
|
+
const HEADER_PREFIX = 2 // 折叠态标题行:▶/▼ 图标 + 后面的空格
|
|
98
|
+
const UNIT_GAP = 1 // 计量单位前的空格(如 "tok")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// Plugin entry
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 路径截断的固定开销(列数):仅宿主布局常量,不含任何内容宽度——
|
|
109
|
+
* 统计宽度、宿主 usage、commands 快捷键均为运行时动态计算。
|
|
110
|
+
* - marginLeft=1 + space-between 余量 ≈ 2 列
|
|
111
|
+
*/
|
|
112
|
+
const PATH_CHROME = 2
|
|
113
|
+
/**
|
|
114
|
+
* 路径可用宽度低于此列数时整体隐藏(极窄终端下路径信息价值太低,
|
|
115
|
+
* 残留的 "E:\Work…" 反而挤压右侧统计与 commands,直接让位更干净)。
|
|
116
|
+
*/
|
|
117
|
+
const HIDE_PATH_BELOW = 14
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 从宿主 keymap 动态读取命令的快捷键显示文本(与宿主 Prompt 同源),
|
|
121
|
+
* 取不到时回退到传入的默认文本。
|
|
122
|
+
*/
|
|
123
|
+
function keyShortcut(api: TuiPluginApi, command: string, fallback: string): string {
|
|
124
|
+
try {
|
|
125
|
+
const binds = api.tuiConfig.keybinds.get(command)
|
|
126
|
+
const seq = binds?.map((b) => ({ key: b.key }))
|
|
127
|
+
const s = api.keys.formatBindings(seq as unknown as SequenceBindingLike[])
|
|
128
|
+
return s || fallback
|
|
129
|
+
} catch {
|
|
130
|
+
return fallback
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* 输入框 hint 行(session_prompt slot 的 hint):单行显示 路径 · 命中率 · 余额 · Tokens。
|
|
136
|
+
* 通过 ui.Prompt 的 hint prop 注入——宿主右侧的 token/commands 提示自动保留,
|
|
137
|
+
* 三合一信息与路径同行显示在中间位置。
|
|
138
|
+
*/
|
|
139
|
+
function BottomStatusBar(props: { api: TuiPluginApi; signals: PanelSignals; sessionId: string }): JSX.Element {
|
|
140
|
+
const KV_PREFIX = "cache_panel"
|
|
141
|
+
const t = createT(() => props.signals.langCode())
|
|
142
|
+
|
|
143
|
+
const sid = props.sessionId
|
|
144
|
+
|
|
145
|
+
// ── 命中率(单条口径:最后一条有 token 的 assistant 消息)+ token 汇总 ──
|
|
146
|
+
const stats = createMemo(() => {
|
|
147
|
+
const id = sid
|
|
148
|
+
if (!id) return null
|
|
149
|
+
const msgs = props.api.state.session.messages(id) as Message[]
|
|
150
|
+
const session = typeof props.api.state.session.get === "function"
|
|
151
|
+
? props.api.state.session.get(id)
|
|
152
|
+
: undefined
|
|
153
|
+
let input = session?.tokens?.input ?? 0
|
|
154
|
+
let read = session?.tokens?.cache?.read ?? 0
|
|
155
|
+
let write = session?.tokens?.cache?.write ?? 0
|
|
156
|
+
// 旧 SDK 无 session 聚合字段 → 遍历消息累加(与侧边栏 fallback 一致)
|
|
157
|
+
if (session?.tokens == null) {
|
|
158
|
+
for (const m of msgs) {
|
|
159
|
+
if (m.role !== "assistant") continue
|
|
160
|
+
const tk = (m as AssistantMessage).tokens
|
|
161
|
+
if (!tk) continue
|
|
162
|
+
input += num(tk.input)
|
|
163
|
+
read += num(tk.cache?.read)
|
|
164
|
+
write += num(tk.cache?.write)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// 从后往前取最后两条有 token 数据的 assistant 消息 → 单条命中率 + 趋势
|
|
168
|
+
// 分母含缓存写(业界口径:read / (input+read+write))
|
|
169
|
+
let hitRate = -1, prevHitRate = -1
|
|
170
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
171
|
+
const m = msgs[i]
|
|
172
|
+
if (m.role !== "assistant") continue
|
|
173
|
+
const tk = (m as AssistantMessage).tokens
|
|
174
|
+
if (!tk) continue
|
|
175
|
+
const mit = num(tk.input) + num(tk.cache?.read) + num(tk.cache?.write)
|
|
176
|
+
const mrt = num(tk.cache?.read)
|
|
177
|
+
if (mit <= 0) continue
|
|
178
|
+
const rate = (mrt / mit) * 100
|
|
179
|
+
if (hitRate < 0) { hitRate = rate; continue }
|
|
180
|
+
prevHitRate = rate
|
|
181
|
+
break
|
|
182
|
+
}
|
|
183
|
+
return { hitRate, prevHitRate, input, read, write }
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
// 余额查询状态为共享信号(PanelSignals.balanceState),由 tui() 统一轮询
|
|
187
|
+
|
|
188
|
+
// 自动切换 provider(跟随当前会话模型;幂等,与侧边栏共享信号)
|
|
189
|
+
createEffect(() => {
|
|
190
|
+
if (!props.signals.autoBalance()) return
|
|
191
|
+
const id = sid
|
|
192
|
+
if (!id) return
|
|
193
|
+
const msgs = props.api.state.session.messages(id) as Message[]
|
|
194
|
+
let pid = ""
|
|
195
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
196
|
+
const m = msgs[i]
|
|
197
|
+
if (m.role === "assistant" && (m as AssistantMessage).providerID) { pid = (m as AssistantMessage).providerID; break }
|
|
198
|
+
}
|
|
199
|
+
if (!pid) {
|
|
200
|
+
try { pid = props.api.state.session.get(id)?.model?.providerID ?? "" } catch {}
|
|
201
|
+
}
|
|
202
|
+
if (!pid) return
|
|
203
|
+
const hit = matchBalanceProvider(pid)
|
|
204
|
+
if (hit) {
|
|
205
|
+
props.signals.setBalanceUnsupported(false)
|
|
206
|
+
if (hit.id !== props.signals.balanceProviderId()) {
|
|
207
|
+
props.signals.setBalanceProviderId(hit.id)
|
|
208
|
+
props.signals.setBalanceRefresh(props.signals.balanceRefresh() + 1)
|
|
209
|
+
}
|
|
210
|
+
} else {
|
|
211
|
+
// 当前提供商没有余额适配器 → 标记不支持,余额显示 N/A 并停止轮询
|
|
212
|
+
props.signals.setBalanceUnsupported(true)
|
|
213
|
+
}
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
// ── 主题色(与侧边栏同口径)──
|
|
217
|
+
const pal = createMemo(() => {
|
|
218
|
+
const th = props.api.theme.current as Record<string, unknown>
|
|
219
|
+
const sat = (k: string, fb: string) => desaturateTo(th[k], MAX_SAT, fb)
|
|
220
|
+
return {
|
|
221
|
+
text: sat("text", FALLBACK.text),
|
|
222
|
+
muted: sat("textMuted", FALLBACK.muted),
|
|
223
|
+
success: sat("success", FALLBACK.success),
|
|
224
|
+
warning: sat("warning", FALLBACK.warning),
|
|
225
|
+
error: sat("error", FALLBACK.error),
|
|
226
|
+
}
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
const hitColor = createMemo(() => {
|
|
230
|
+
const r = stats()?.hitRate ?? -1
|
|
231
|
+
if (r >= 85) return pal().success
|
|
232
|
+
if (r >= 70) return pal().warning
|
|
233
|
+
return pal().error
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
// 命中率趋势:最后一条与上一条的差值;|Δ| < 0.05 视为无变化(null = 不显示)
|
|
237
|
+
const trend = createMemo(() => {
|
|
238
|
+
const s = stats()
|
|
239
|
+
if (!s || s.prevHitRate < 0 || s.hitRate < 0) return null
|
|
240
|
+
const d = s.hitRate - s.prevHitRate
|
|
241
|
+
return Math.abs(d) < 0.05 ? null : d
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
const balanceText = createMemo(() => {
|
|
245
|
+
const s = props.signals.balanceState()
|
|
246
|
+
if (s.status === "ok" && s.data) return formatBalanceText(s.data, props.signals.balanceCurrency(), props.signals.exchangeRate())
|
|
247
|
+
if (s.status === "loading") return "\u2026"
|
|
248
|
+
if (s.status === "error") return "\u26a0"
|
|
249
|
+
return "-"
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
// 路径显示(替换宿主默认 hint 左侧的 cwd 文本)
|
|
253
|
+
const directory = createMemo(() => {
|
|
254
|
+
try { return props.api.state.path.directory } catch { return "" }
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
// 终端宽度信号:初始读取渲染器,窗口 resize 时更新(宿主不约束 hint 行宽度,
|
|
258
|
+
// 路径截断必须基于终端宽度手动计算)。
|
|
259
|
+
// CliRenderer 继承的 EventEmitter 因项目未装 @types/node 类型不可见,
|
|
260
|
+
// 用最小接口声明补齐 resize 事件的 on/off。
|
|
261
|
+
interface ResizeEmitter {
|
|
262
|
+
on(event: "resize", cb: () => void): unknown
|
|
263
|
+
off(event: "resize", cb: () => void): unknown
|
|
264
|
+
}
|
|
265
|
+
const [termW, setTermW] = createSignal(props.api.renderer.terminalWidth)
|
|
266
|
+
// resize 事件(主通道);事件接口若在插件环境不可用则跳过,由轮询兜底
|
|
267
|
+
createEffect(() => {
|
|
268
|
+
const r = props.api.renderer as unknown as ResizeEmitter
|
|
269
|
+
if (typeof r.on !== "function" || typeof r.off !== "function") return
|
|
270
|
+
const onResize = () => setTermW(props.api.renderer.terminalWidth)
|
|
271
|
+
r.on("resize", onResize)
|
|
272
|
+
onCleanup(() => r.off("resize", onResize))
|
|
273
|
+
})
|
|
274
|
+
// 轮询兜底:事件通道若在插件环境不可用,定期同步终端宽度(值不变时不触发更新)
|
|
275
|
+
createEffect(() => {
|
|
276
|
+
const timer = setInterval(() => setTermW(props.api.renderer.terminalWidth), 500)
|
|
277
|
+
onCleanup(() => clearInterval(timer))
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
// 统计部分分段(单一数据源):量宽拼接 text,渲染逐段着色,避免双源漂移
|
|
281
|
+
const statsSegs = createMemo<{ text: string; color: string | undefined }[]>(() => {
|
|
282
|
+
const s = stats()
|
|
283
|
+
const hr = s && s.hitRate >= 0 ? (Math.floor(s.hitRate * 10) / 10).toFixed(1) + "%" : "--"
|
|
284
|
+
const segs: { text: string; color: string | undefined }[] = [
|
|
285
|
+
{ text: t("barHit") + " ", color: pal().muted },
|
|
286
|
+
{ text: hr, color: hitColor() },
|
|
287
|
+
]
|
|
288
|
+
const tr = trend()
|
|
289
|
+
if (tr !== null) {
|
|
290
|
+
segs.push({ text: " " + (tr > 0 ? "\u2191" : "\u2193") + Math.abs(tr).toFixed(1) + "%", color: tr > 0 ? pal().success : pal().error })
|
|
291
|
+
}
|
|
292
|
+
segs.push({ text: " \u00b7 " + t("barTok") + " ", color: pal().muted })
|
|
293
|
+
segs.push({ text: s ? fmtCompact(s.input + s.read + s.write) : "--", color: pal().text })
|
|
294
|
+
if (!props.signals.balanceUnsupported()) {
|
|
295
|
+
segs.push({ text: " \u00b7 " + t("barBal") + " ", color: pal().muted })
|
|
296
|
+
segs.push({ text: balanceText(), color: pal().text })
|
|
297
|
+
}
|
|
298
|
+
segs.push({ text: " \u00b7 ", color: pal().muted })
|
|
299
|
+
return segs
|
|
300
|
+
})
|
|
301
|
+
const statsW = createMemo(() => {
|
|
302
|
+
let w = 0
|
|
303
|
+
for (const sg of statsSegs()) w += visualWidth(sg.text)
|
|
304
|
+
return w
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
// 宿主右侧 usage 文本复刻(1.18.16 Prompt 口径):
|
|
308
|
+
// 最后一条 output>0 的 assistant 消息 → tokens 合计格式化 + 模型 context limit 百分比 + session 累计费用
|
|
309
|
+
const sessionCost = createMemo(() => {
|
|
310
|
+
try { return num(props.api.state.session.get(sid)?.cost) } catch { return 0 }
|
|
311
|
+
})
|
|
312
|
+
const usageText = createMemo(() => {
|
|
313
|
+
const id = sid
|
|
314
|
+
if (!id) return ""
|
|
315
|
+
const msgs = props.api.state.session.messages(id) as Message[]
|
|
316
|
+
let last: AssistantMessage | undefined
|
|
317
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
318
|
+
const m = msgs[i]
|
|
319
|
+
if (m.role !== "assistant") continue
|
|
320
|
+
const tk = (m as AssistantMessage).tokens
|
|
321
|
+
if (tk && num(tk.output) > 0) { last = m as AssistantMessage; break }
|
|
322
|
+
}
|
|
323
|
+
if (!last) return ""
|
|
324
|
+
const tk = last.tokens
|
|
325
|
+
if (!tk) return ""
|
|
326
|
+
const tokens = num(tk.input) + num(tk.output) + num(tk.reasoning) + num(tk.cache?.read) + num(tk.cache?.write)
|
|
327
|
+
if (tokens <= 0) return ""
|
|
328
|
+
let pct = ""
|
|
329
|
+
try {
|
|
330
|
+
const p = props.api.state.provider.find((x) => x.id === last.providerID)
|
|
331
|
+
const limit = p?.models?.[last.modelID]?.limit?.context
|
|
332
|
+
if (typeof limit === "number" && limit > 0) pct = ` (${Math.round((tokens / limit) * 100)}%)`
|
|
333
|
+
} catch {}
|
|
334
|
+
const context = fmtCompact(tokens) + pct
|
|
335
|
+
const cost = sessionCost()
|
|
336
|
+
return cost > 0 ? context + " \u00b7 " + fmtCost(cost) : context
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
// 宿主 1513 行右侧文本:usage(有数据)或 "快捷键 agents"(无数据)+ commands,
|
|
340
|
+
// 快捷键从宿主 keymap 动态读取
|
|
341
|
+
const rightText = createMemo(() => {
|
|
342
|
+
const cmds = keyShortcut(props.api, "command.palette.show", "ctrl+p") + " commands"
|
|
343
|
+
const u = usageText()
|
|
344
|
+
if (u) return u + " " + cmds
|
|
345
|
+
return keyShortcut(props.api, "agent.cycle", "") + " agents " + cmds
|
|
346
|
+
})
|
|
347
|
+
const rightW = createMemo(() => visualWidth(rightText()))
|
|
348
|
+
|
|
349
|
+
// 输入框实际宽度 = 终端宽度 - 侧边栏(可见时 42) - 边距 4
|
|
350
|
+
// (与宿主 session 布局 contentWidth 口径一致;侧边栏可见性由本面板挂载状态驱动)
|
|
351
|
+
const inputW = createMemo(() => termW() - (props.signals.sidebarVisible() ? 42 : 0) - 4)
|
|
352
|
+
|
|
353
|
+
// 路径可用宽度 = 输入框宽度 - 统计宽度(精确) - 宿主右侧宽度(动态) - 布局开销;
|
|
354
|
+
// 低于 HIDE_PATH_BELOW 时整体隐藏路径(宽度归零),把空间让给统计与 commands
|
|
355
|
+
const dirDisplay = createMemo(() => {
|
|
356
|
+
const avail = inputW() - statsW() - rightW() - PATH_CHROME
|
|
357
|
+
if (avail < HIDE_PATH_BELOW) return ""
|
|
358
|
+
return truncateVisual(directory(), avail)
|
|
359
|
+
})
|
|
360
|
+
// 状态栏关闭(仅路径)时同样在极窄条件下隐藏路径
|
|
361
|
+
const dirFallback = createMemo(() => {
|
|
362
|
+
const avail = inputW() - rightW() - PATH_CHROME
|
|
363
|
+
if (avail < HIDE_PATH_BELOW) return ""
|
|
364
|
+
return truncateVisual(directory(), avail)
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
// 恢复显隐偏好(默认显示);关闭时回退为仅显示路径,与宿主默认 hint 行一致
|
|
368
|
+
onMount(() => {
|
|
369
|
+
try {
|
|
370
|
+
const v = props.api.kv.get<boolean>(`${KV_PREFIX}.section.bottom`, true)
|
|
371
|
+
props.signals.setSectionBottom(v !== false)
|
|
372
|
+
} catch {}
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
return (
|
|
376
|
+
<Show
|
|
377
|
+
when={props.signals.sectionBottom()}
|
|
378
|
+
fallback={<text fg={pal().muted}>{dirFallback()}</text>}
|
|
379
|
+
>
|
|
380
|
+
<box marginLeft={1} flexGrow={1} flexShrink={0} flexDirection="row" justifyContent="space-between">
|
|
381
|
+
<text fg={pal().muted}>{dirDisplay()}</text>
|
|
382
|
+
<box flexDirection="row">
|
|
383
|
+
<text>
|
|
384
|
+
<For each={statsSegs()}>
|
|
385
|
+
{(sg) => <span style={{ fg: sg.color }}>{sg.text}</span>}
|
|
386
|
+
</For>
|
|
387
|
+
</text>
|
|
388
|
+
</box>
|
|
389
|
+
</box>
|
|
390
|
+
</Show>
|
|
391
|
+
)
|
|
392
|
+
}
|
|
393
|
+
function createSidebarSlot(api: TuiPluginApi, signals: PanelSignals): TuiSlotPlugin {
|
|
394
|
+
let lastSlotSid = ""
|
|
395
|
+
return {
|
|
396
|
+
order: 55,
|
|
397
|
+
slots: {
|
|
398
|
+
sidebar_content(ctx: TuiSlotContext, input: { session_id: string }): JSX.Element {
|
|
399
|
+
// ── auto-clear override when the user navigates to a different main session ──
|
|
400
|
+
if (input.session_id !== lastSlotSid) {
|
|
401
|
+
lastSlotSid = input.session_id
|
|
402
|
+
if (signals.overrideSessionId()) {
|
|
403
|
+
signals.setOverrideSessionId(undefined)
|
|
404
|
+
api.kv.set("cache_panel.session", "")
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return (
|
|
408
|
+
<TokenCachePanel
|
|
409
|
+
theme={ctx.theme.current}
|
|
410
|
+
api={api}
|
|
411
|
+
sessionId={input.session_id}
|
|
412
|
+
signals={signals}
|
|
413
|
+
/>
|
|
414
|
+
)
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const tui: TuiPlugin = async (api: TuiPluginApi) => {
|
|
421
|
+
// ── shared panel signals ──────────────────────────────────────
|
|
422
|
+
const [currencySymbol, setCurrencySymbol] = createSignal("$")
|
|
423
|
+
const [exchangeRate, setExchangeRate] = createSignal(1)
|
|
424
|
+
const [sectionDetail, setSectionDetail] = createSignal(true)
|
|
425
|
+
const [sectionModel, setSectionModel] = createSignal(true)
|
|
426
|
+
const [sectionDist, setSectionDist] = createSignal(true)
|
|
427
|
+
const [sectionSkills, setSectionSkills] = createSignal(true)
|
|
428
|
+
const [sectionBalance, setSectionBalance] = createSignal(true)
|
|
429
|
+
const [sectionBottom, setSectionBottom] = createSignal(true)
|
|
430
|
+
const [balanceRefresh, setBalanceRefresh] = createSignal(0)
|
|
431
|
+
const [balanceProviderId, setBalanceProviderId] = createSignal("deepseek")
|
|
432
|
+
const [autoBalance, setAutoBalance] = createSignal(true)
|
|
433
|
+
const [balanceUnsupported, setBalanceUnsupported] = createSignal(false)
|
|
434
|
+
const [balanceCurrency, setBalanceCurrency] = createSignal("")
|
|
435
|
+
const [borderVisible, setBorderVisible] = createSignal(true)
|
|
436
|
+
const [langCode, setLangCode] = createSignal<LangCode>(INIT_LANG)
|
|
437
|
+
const [overrideSessionId, setOverrideSessionId] = createSignal<string | undefined>(undefined)
|
|
438
|
+
// 侧边栏可见性(由 TokenCachePanel 挂载状态驱动):可见时宿主输入框宽度 = 终端宽 - 42 - 4
|
|
439
|
+
const [sidebarVisible, setSidebarVisible] = createSignal(false)
|
|
440
|
+
|
|
441
|
+
// ── 余额查询状态(共享):侧边栏与底部栏读同一份数据,
|
|
442
|
+
// 避免重复请求导致两处余额不一致 ──
|
|
443
|
+
const [balanceState, setBalanceState] = createSignal<BalanceState>({
|
|
444
|
+
status: "idle", data: null, lastFetch: 0,
|
|
445
|
+
})
|
|
446
|
+
// 请求序号:防止定时轮询与手动刷新并发时,慢的旧请求覆盖新结果
|
|
447
|
+
let balanceSeq = 0
|
|
448
|
+
|
|
449
|
+
const signals: PanelSignals = {
|
|
450
|
+
currencySymbol, setCurrencySymbol,
|
|
451
|
+
exchangeRate, setExchangeRate,
|
|
452
|
+
langCode, setLangCode,
|
|
453
|
+
sectionDetail, setSectionDetail,
|
|
454
|
+
sectionModel, setSectionModel,
|
|
455
|
+
sectionDist, setSectionDist,
|
|
456
|
+
sectionSkills, setSectionSkills,
|
|
457
|
+
sectionBalance, setSectionBalance,
|
|
458
|
+
sectionBottom, setSectionBottom,
|
|
459
|
+
balanceRefresh, setBalanceRefresh,
|
|
460
|
+
balanceProviderId, setBalanceProviderId,
|
|
461
|
+
autoBalance, setAutoBalance,
|
|
462
|
+
balanceUnsupported, setBalanceUnsupported,
|
|
463
|
+
balanceState,
|
|
464
|
+
balanceCurrency, setBalanceCurrency,
|
|
465
|
+
borderVisible, setBorderVisible,
|
|
466
|
+
overrideSessionId, setOverrideSessionId,
|
|
467
|
+
sidebarVisible, setSidebarVisible,
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
api.slots.register(createSidebarSlot(api, signals))
|
|
471
|
+
|
|
472
|
+
// 输入框 hint 行(session_prompt slot,replace 模式):
|
|
473
|
+
// 用宿主同一 Prompt 组件重渲染输入框,仅替换 hint 行左侧——
|
|
474
|
+
// 在路径与右侧 token/commands 提示之间插入 命中率 · 余额 · Tokens。
|
|
475
|
+
api.slots.register({
|
|
476
|
+
order: 55,
|
|
477
|
+
slots: {
|
|
478
|
+
session_prompt(
|
|
479
|
+
_ctx: TuiSlotContext,
|
|
480
|
+
input: {
|
|
481
|
+
session_id: string
|
|
482
|
+
visible?: boolean
|
|
483
|
+
disabled?: boolean
|
|
484
|
+
on_submit?: () => void
|
|
485
|
+
ref?: (ref: TuiPromptRef | undefined) => void
|
|
486
|
+
},
|
|
487
|
+
): JSX.Element {
|
|
488
|
+
return (
|
|
489
|
+
<api.ui.Prompt
|
|
490
|
+
sessionID={input.session_id}
|
|
491
|
+
visible={input.visible}
|
|
492
|
+
disabled={input.disabled}
|
|
493
|
+
onSubmit={input.on_submit}
|
|
494
|
+
ref={input.ref}
|
|
495
|
+
hint={<BottomStatusBar api={api} signals={signals} sessionId={input.session_id} />}
|
|
496
|
+
/>
|
|
497
|
+
)
|
|
498
|
+
},
|
|
499
|
+
},
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
// ── slash commands for runtime config ──
|
|
503
|
+
const KV_PREFIX = "cache_panel"
|
|
504
|
+
|
|
505
|
+
// ── 语言偏好恢复:KV 就绪后优先用户设置(/cache-lang),覆盖自动识别 ──
|
|
506
|
+
const restoreLang = () => {
|
|
507
|
+
try {
|
|
508
|
+
const saved = api.kv.get<string>(`${KV_PREFIX}.lang`)
|
|
509
|
+
if (saved && LANG_META.some((m) => m.code === saved)) setLangCode(saved as LangCode)
|
|
510
|
+
} catch {}
|
|
511
|
+
}
|
|
512
|
+
if (api.kv.ready) {
|
|
513
|
+
restoreLang()
|
|
514
|
+
} else {
|
|
515
|
+
const langTimer = setInterval(() => {
|
|
516
|
+
if (api.kv.ready) { clearInterval(langTimer); restoreLang() }
|
|
517
|
+
}, 10)
|
|
518
|
+
api.lifecycle.onDispose(() => clearInterval(langTimer))
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const pollBalance = async () => {
|
|
522
|
+
const provider = getBalanceProvider(balanceProviderId())
|
|
523
|
+
// 手动配置的 key 优先;缺失时自动复用 OpenCode 已认证的 key(auth.json / config)
|
|
524
|
+
const key = api.kv.get<string>(`${KV_PREFIX}.balance.${provider.id}.key`, "")
|
|
525
|
+
|| findOpencodeKey(api, provider)
|
|
526
|
+
if (balanceUnsupported()) { setBalanceState({ status: "idle", data: null, lastFetch: 0, error: undefined, key: undefined }); return }
|
|
527
|
+
if (!key) { setBalanceState({ status: "idle", data: null, lastFetch: 0, error: undefined, key: undefined }); return }
|
|
528
|
+
const now = Date.now()
|
|
529
|
+
const prev = balanceState()
|
|
530
|
+
// key 已更换(重新输入)→ 强制重新查询,绕过缓存
|
|
531
|
+
if (prev.status === "ok" && prev.key === key && now - prev.lastFetch < BALANCE_POLL_MS) return // cache still fresh
|
|
532
|
+
const seq = ++balanceSeq
|
|
533
|
+
setBalanceState({ ...prev, status: "loading", error: undefined, key })
|
|
534
|
+
const controller = new AbortController()
|
|
535
|
+
let timedOut = false
|
|
536
|
+
const timer = setTimeout(() => { timedOut = true; controller.abort() }, 10_000)
|
|
537
|
+
try {
|
|
538
|
+
const data = await provider.fetchBalance(key, controller.signal)
|
|
539
|
+
clearTimeout(timer)
|
|
540
|
+
if (seq !== balanceSeq) return // 已被更新的请求取代,丢弃过期结果
|
|
541
|
+
setBalanceState({ status: "ok", data, lastFetch: Date.now(), error: undefined, key })
|
|
542
|
+
} catch (err) {
|
|
543
|
+
clearTimeout(timer)
|
|
544
|
+
if (seq !== balanceSeq) return
|
|
545
|
+
const code = timedOut ? "TIMEOUT" : (err instanceof Error ? err.message : "")
|
|
546
|
+
// 失败时清空旧数据,避免显示过期余额
|
|
547
|
+
setBalanceState({ status: "error", data: null, lastFetch: 0, error: code, key })
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Re-fetch when the API key is (re)configured via /cache-balance-key.
|
|
552
|
+
// 注意:pollBalance 内部读写 balanceState 信号,若不做 untrack 包裹,
|
|
553
|
+
// effect 会追踪 balanceState 的变化并与 pollBalance 的 setBalanceState
|
|
554
|
+
// 形成无限循环(每次重跑都发起新的 fetch 请求)。
|
|
555
|
+
createEffect(() => {
|
|
556
|
+
void balanceRefresh()
|
|
557
|
+
untrack(() => { void pollBalance() })
|
|
558
|
+
})
|
|
559
|
+
|
|
560
|
+
// 定时轮询(5 分钟);随插件生命周期清理
|
|
561
|
+
const balanceTimer = setInterval(pollBalance, BALANCE_POLL_MS)
|
|
562
|
+
api.lifecycle.onDispose(() => clearInterval(balanceTimer))
|
|
563
|
+
|
|
564
|
+
/** 菜单中 provider 选项标题:标注 key 来源(手动配置 / OpenCode 自动复用 / 未配置)。 */
|
|
565
|
+
const providerOptionTitle = (p: BalanceProvider, current?: string) => {
|
|
566
|
+
const t = createT(() => langCode())
|
|
567
|
+
const hasManual = !!api.kv.get<string>(`${KV_PREFIX}.balance.${p.id}.key`, "")
|
|
568
|
+
const hasAuto = !hasManual && !!findOpencodeKey(api, p)
|
|
569
|
+
const mark = hasManual
|
|
570
|
+
? t("keyUser")
|
|
571
|
+
: hasAuto
|
|
572
|
+
? t("keyOpenCode")
|
|
573
|
+
: t("keyNotSet")
|
|
574
|
+
return p.name + mark + (current && p.id === current ? " *" : "")
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/** 弹出指定 provider 的 API Key 输入框(脱敏预填;空清除 / 含 * 保留原 key / 新 key 实时刷新)。 */
|
|
578
|
+
const promptBalanceKey = (dialog: TuiDialogStack | undefined, provider: BalanceProvider) => {
|
|
579
|
+
const t = createT(() => langCode())
|
|
580
|
+
const current = api.kv.get<string>(`${KV_PREFIX}.balance.${provider.id}.key`, "")
|
|
581
|
+
const masked = maskKey(current)
|
|
582
|
+
dialog?.replace(() => (
|
|
583
|
+
<api.ui.DialogPrompt
|
|
584
|
+
title={provider.name}
|
|
585
|
+
description={() => <text>{t("balKeyPrompt", { p: provider.name })}</text>}
|
|
586
|
+
placeholder={provider.keyPlaceholder ?? "sk-..."}
|
|
587
|
+
value={masked}
|
|
588
|
+
onConfirm={(val) => {
|
|
589
|
+
const input = val.trim()
|
|
590
|
+
let key: string
|
|
591
|
+
if (input === "") {
|
|
592
|
+
key = ""
|
|
593
|
+
} else if (input.includes("*")) {
|
|
594
|
+
key = current
|
|
595
|
+
} else {
|
|
596
|
+
key = input
|
|
597
|
+
}
|
|
598
|
+
api.kv.set(`${KV_PREFIX}.balance.${provider.id}.key`, key)
|
|
599
|
+
setBalanceRefresh(v => v + 1)
|
|
600
|
+
if (key) {
|
|
601
|
+
api.ui.toast({ message: t("keySaved") })
|
|
602
|
+
} else {
|
|
603
|
+
api.ui.toast({ message: t("keyCleared") })
|
|
604
|
+
}
|
|
605
|
+
dialog?.clear()
|
|
606
|
+
}}
|
|
607
|
+
onCancel={() => dialog?.clear()}
|
|
608
|
+
/>
|
|
609
|
+
))
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
api.command?.register(() => [
|
|
613
|
+
{
|
|
614
|
+
title: "Cache: Set Currency",
|
|
615
|
+
value: "cache.currency",
|
|
616
|
+
description: "Change the currency unit for cost display",
|
|
617
|
+
slash: { name: "cache-currency" },
|
|
618
|
+
onSelect: (dialog) => {
|
|
619
|
+
dialog?.replace(() => (
|
|
620
|
+
<api.ui.DialogSelect
|
|
621
|
+
title="Select Currency"
|
|
622
|
+
options={Object.entries(CURRENCIES).map(([code, sym]) => ({
|
|
623
|
+
title: `${code} (${sym})`,
|
|
624
|
+
value: code,
|
|
625
|
+
}))}
|
|
626
|
+
onSelect={(opt) => {
|
|
627
|
+
const t = createT(() => langCode())
|
|
628
|
+
const sym = CURRENCIES[opt.value] ?? "$"
|
|
629
|
+
const defRate = DEFAULT_RATES[opt.value] ?? 1
|
|
630
|
+
api.kv.set(`${KV_PREFIX}.currency`, sym)
|
|
631
|
+
api.kv.set(`${KV_PREFIX}.rate`, defRate)
|
|
632
|
+
// 同步余额显示币种偏好:CNY/USD 原生直显,其余币种按汇率换算
|
|
633
|
+
api.kv.set(`${KV_PREFIX}.balance_currency`, opt.value)
|
|
634
|
+
signals.setBalanceCurrency(opt.value)
|
|
635
|
+
signals.setCurrencySymbol(sym)
|
|
636
|
+
signals.setExchangeRate(defRate)
|
|
637
|
+
api.ui.toast({ message: t("currencySet", { v: opt.value, s: sym, r: defRate }) })
|
|
638
|
+
dialog?.clear()
|
|
639
|
+
}}
|
|
640
|
+
/>
|
|
641
|
+
))
|
|
642
|
+
},
|
|
643
|
+
},
|
|
644
|
+
{
|
|
645
|
+
title: "Cache: Set Exchange Rate",
|
|
646
|
+
value: "cache.rate",
|
|
647
|
+
description: "Set the exchange rate multiplier for the selected currency",
|
|
648
|
+
slash: { name: "cache-rate" },
|
|
649
|
+
onSelect: (dialog) => {
|
|
650
|
+
dialog?.replace(() => (
|
|
651
|
+
<api.ui.DialogPrompt
|
|
652
|
+
title="Exchange Rate"
|
|
653
|
+
description={() => <text>Enter the exchange rate from USD to your currency (e.g. 7.2 for CNY)</text>}
|
|
654
|
+
placeholder="1.0"
|
|
655
|
+
value={String(api.kv.get<number>(`${KV_PREFIX}.rate`, 1))}
|
|
656
|
+
onConfirm={(val) => {
|
|
657
|
+
const t = createT(() => langCode())
|
|
658
|
+
const n = parseFloat(val)
|
|
659
|
+
if (n > 0) {
|
|
660
|
+
api.kv.set(`${KV_PREFIX}.rate`, n)
|
|
661
|
+
signals.setExchangeRate(n)
|
|
662
|
+
api.ui.toast({ message: t("rateSet", { r: n }) })
|
|
663
|
+
}
|
|
664
|
+
dialog?.clear()
|
|
665
|
+
}}
|
|
666
|
+
/>
|
|
667
|
+
))
|
|
668
|
+
},
|
|
669
|
+
},
|
|
670
|
+
{
|
|
671
|
+
title: "Cache: Toggle Section",
|
|
672
|
+
value: "cache.section",
|
|
673
|
+
description: "Show or hide a sidebar section",
|
|
674
|
+
slash: { name: "cache-section" },
|
|
675
|
+
onSelect: (dialog) => {
|
|
676
|
+
const t = createT(() => langCode())
|
|
677
|
+
const detailOn = Boolean(api.kv.get(`${KV_PREFIX}.section.detail`, true))
|
|
678
|
+
const modelOn = Boolean(api.kv.get(`${KV_PREFIX}.section.model`, true))
|
|
679
|
+
const distOn = Boolean(api.kv.get(`${KV_PREFIX}.section.dist`, true))
|
|
680
|
+
const skillsOn = Boolean(api.kv.get(`${KV_PREFIX}.section.skills`, true))
|
|
681
|
+
const balanceOn = Boolean(api.kv.get(`${KV_PREFIX}.section.balance`, true))
|
|
682
|
+
const bottomOn = Boolean(api.kv.get(`${KV_PREFIX}.section.bottom`, true))
|
|
683
|
+
const borderOn = Boolean(api.kv.get(`${KV_PREFIX}.border`, true))
|
|
684
|
+
const labels: Record<string, string> = {
|
|
685
|
+
detail: t("secDetail"),
|
|
686
|
+
model: t("secModel"),
|
|
687
|
+
dist: t("distTitle"),
|
|
688
|
+
skills: t("secSkills"),
|
|
689
|
+
balance: t("secBalance"),
|
|
690
|
+
bottom: t("secBottom"),
|
|
691
|
+
border: t("secBorder"),
|
|
692
|
+
}
|
|
693
|
+
const optTitle = (label: string, on: boolean) => `${visualPadEnd(label, 15)}[${on ? "ON" : "OFF"}]`
|
|
694
|
+
dialog?.replace(() => (
|
|
695
|
+
<api.ui.DialogSelect
|
|
696
|
+
title={t("secToggle")}
|
|
697
|
+
options={[
|
|
698
|
+
{ title: optTitle(labels.detail, detailOn), value: "detail" },
|
|
699
|
+
{ title: optTitle(labels.model, modelOn), value: "model" },
|
|
700
|
+
{ title: optTitle(labels.dist, distOn), value: "dist" },
|
|
701
|
+
{ title: optTitle(labels.skills, skillsOn), value: "skills" },
|
|
702
|
+
{ title: optTitle(labels.balance, balanceOn), value: "balance" },
|
|
703
|
+
{ title: optTitle(labels.bottom, bottomOn), value: "bottom" },
|
|
704
|
+
{ title: optTitle(labels.border, borderOn), value: "border" },
|
|
705
|
+
]}
|
|
706
|
+
onSelect={(opt) => {
|
|
707
|
+
if (opt.value === "border") {
|
|
708
|
+
const cur = Boolean(api.kv.get(`${KV_PREFIX}.border`, true))
|
|
709
|
+
api.kv.set(`${KV_PREFIX}.border`, !cur)
|
|
710
|
+
signals.setBorderVisible(!cur)
|
|
711
|
+
api.ui.toast({ message: !cur ? t("borderShown") : t("borderHidden") })
|
|
712
|
+
} else {
|
|
713
|
+
const key = `${KV_PREFIX}.section.${opt.value}`
|
|
714
|
+
const cur = Boolean(api.kv.get(key, true))
|
|
715
|
+
api.kv.set(key, !cur)
|
|
716
|
+
if (opt.value === "detail") signals.setSectionDetail(!cur)
|
|
717
|
+
if (opt.value === "model") signals.setSectionModel(!cur)
|
|
718
|
+
if (opt.value === "dist") signals.setSectionDist(!cur)
|
|
719
|
+
if (opt.value === "skills") signals.setSectionSkills(!cur)
|
|
720
|
+
if (opt.value === "balance") signals.setSectionBalance(!cur)
|
|
721
|
+
if (opt.value === "bottom") signals.setSectionBottom(!cur)
|
|
722
|
+
const name = labels[opt.value] ?? opt.value
|
|
723
|
+
api.ui.toast({ message: t(!cur ? "sectionShown" : "sectionHidden", { s: name }) })
|
|
724
|
+
}
|
|
725
|
+
dialog?.clear()
|
|
726
|
+
}}
|
|
727
|
+
/>
|
|
728
|
+
))
|
|
729
|
+
},
|
|
730
|
+
},
|
|
731
|
+
{
|
|
732
|
+
title: "Cache: Show Config",
|
|
733
|
+
value: "cache.config",
|
|
734
|
+
description: "Display the current plugin configuration",
|
|
735
|
+
slash: { name: "cache-config" },
|
|
736
|
+
onSelect: (dialog) => {
|
|
737
|
+
const t = createT(() => langCode())
|
|
738
|
+
const sym = api.kv.get<string>(`${KV_PREFIX}.currency`) ?? "$"
|
|
739
|
+
const rate = api.kv.get<number>(`${KV_PREFIX}.rate`) ?? 1
|
|
740
|
+
const detail = Boolean(api.kv.get(`${KV_PREFIX}.section.detail`, true))
|
|
741
|
+
const model = Boolean(api.kv.get(`${KV_PREFIX}.section.model`, true))
|
|
742
|
+
const dist = Boolean(api.kv.get(`${KV_PREFIX}.section.dist`, true))
|
|
743
|
+
const skills = Boolean(api.kv.get(`${KV_PREFIX}.section.skills`, true))
|
|
744
|
+
const balance = Boolean(api.kv.get(`${KV_PREFIX}.section.balance`, true))
|
|
745
|
+
const bottom = Boolean(api.kv.get(`${KV_PREFIX}.section.bottom`, true))
|
|
746
|
+
const on = (v: boolean) => v ? "ON" : "OFF"
|
|
747
|
+
api.ui.toast({
|
|
748
|
+
title: t("panelConfigTitle"),
|
|
749
|
+
message: t("panelConfigMsg", {
|
|
750
|
+
c: sym, r: rate,
|
|
751
|
+
d: on(detail), m: on(model),
|
|
752
|
+
t: on(dist), k: on(skills),
|
|
753
|
+
b: on(balance), f: on(bottom),
|
|
754
|
+
}),
|
|
755
|
+
duration: 8000,
|
|
756
|
+
})
|
|
757
|
+
dialog?.clear()
|
|
758
|
+
},
|
|
759
|
+
},
|
|
760
|
+
{
|
|
761
|
+
title: "Cache: Switch Language",
|
|
762
|
+
value: "cache.lang",
|
|
763
|
+
description: "Switch between Chinese and English display",
|
|
764
|
+
slash: { name: "cache-lang" },
|
|
765
|
+
onSelect: (dialog) => {
|
|
766
|
+
const t = createT(() => langCode())
|
|
767
|
+
const cur = langCode()
|
|
768
|
+
dialog?.replace(() => (
|
|
769
|
+
<api.ui.DialogSelect
|
|
770
|
+
title={t("langTitle")}
|
|
771
|
+
options={LANG_META.map((m) => ({
|
|
772
|
+
title: `${visualPadEnd(m.label, 9)}${cur === m.code ? "\u2713" : ""}`,
|
|
773
|
+
value: m.code,
|
|
774
|
+
}))}
|
|
775
|
+
onSelect={(opt) => {
|
|
776
|
+
const code = opt.value as LangCode
|
|
777
|
+
api.kv.set(`${KV_PREFIX}.lang`, code)
|
|
778
|
+
setLangCode(code)
|
|
779
|
+
api.ui.toast({ message: t("langSwitched") })
|
|
780
|
+
dialog?.clear()
|
|
781
|
+
}}
|
|
782
|
+
/>
|
|
783
|
+
))
|
|
784
|
+
},
|
|
785
|
+
},
|
|
786
|
+
{
|
|
787
|
+
title: "Cache: Switch Balance Provider",
|
|
788
|
+
value: "cache.balance",
|
|
789
|
+
description: "切换余额提供商 / 自动切换当前会话提供商 | Switch balance provider / auto-switch session provider",
|
|
790
|
+
slash: { name: "cache-balance" },
|
|
791
|
+
onSelect: (dialog) => {
|
|
792
|
+
const t = createT(() => langCode())
|
|
793
|
+
const current = signals.balanceProviderId()
|
|
794
|
+
const auto = signals.autoBalance()
|
|
795
|
+
const autoLabel = `${t("autoSwitchOpt")} [${auto ? "ON" : "OFF"}]`
|
|
796
|
+
dialog?.replace(() => (
|
|
797
|
+
<api.ui.DialogSelect
|
|
798
|
+
title={t("balProvTitle")}
|
|
799
|
+
options={[
|
|
800
|
+
{
|
|
801
|
+
title: autoLabel,
|
|
802
|
+
value: "__auto__",
|
|
803
|
+
},
|
|
804
|
+
...balanceProviders.map((p) => ({
|
|
805
|
+
title: providerOptionTitle(p, current),
|
|
806
|
+
value: p.id,
|
|
807
|
+
})),
|
|
808
|
+
]}
|
|
809
|
+
onSelect={(opt) => {
|
|
810
|
+
if (opt.value === "__auto__") {
|
|
811
|
+
const next = !auto
|
|
812
|
+
api.kv.set(`${KV_PREFIX}.balance.auto`, next)
|
|
813
|
+
signals.setAutoBalance(next)
|
|
814
|
+
api.ui.toast({ message: next ? t("autoSwitchOn") : t("autoSwitchOff") })
|
|
815
|
+
dialog?.clear()
|
|
816
|
+
} else {
|
|
817
|
+
const provider = getBalanceProvider(opt.value)
|
|
818
|
+
// 手动切换会关闭自动切换
|
|
819
|
+
api.kv.set(`${KV_PREFIX}.balance.provider`, provider.id)
|
|
820
|
+
api.kv.set(`${KV_PREFIX}.balance.auto`, false)
|
|
821
|
+
signals.setBalanceProviderId(provider.id)
|
|
822
|
+
signals.setAutoBalance(false)
|
|
823
|
+
signals.setBalanceUnsupported(false)
|
|
824
|
+
// 切换后立即按新 provider 刷新显示(无 key 时显示 idle,避免残留上一 provider 余额)
|
|
825
|
+
signals.setBalanceRefresh(signals.balanceRefresh() + 1)
|
|
826
|
+
const hasKey = !!api.kv.get<string>(`${KV_PREFIX}.balance.${provider.id}.key`, "")
|
|
827
|
+
if (!hasKey) {
|
|
828
|
+
// 未配置 key → 进入设置流程(对话框保持打开等待输入)
|
|
829
|
+
promptBalanceKey(dialog, provider)
|
|
830
|
+
} else {
|
|
831
|
+
api.ui.toast({ message: t("providerManual", { p: provider.name }) })
|
|
832
|
+
dialog?.clear()
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}}
|
|
836
|
+
/>
|
|
837
|
+
))
|
|
838
|
+
},
|
|
839
|
+
},
|
|
840
|
+
{
|
|
841
|
+
title: "Cache: Set Balance API Key",
|
|
842
|
+
value: "cache.balance.key",
|
|
843
|
+
description: "Select a provider and set its API key for balance display",
|
|
844
|
+
slash: { name: "cache-balance-key" },
|
|
845
|
+
onSelect: (dialog) => {
|
|
846
|
+
const t = createT(() => langCode())
|
|
847
|
+
// 步骤 1:选择 provider
|
|
848
|
+
dialog?.replace(() => (
|
|
849
|
+
<api.ui.DialogSelect
|
|
850
|
+
title={t("balSelectTitle")}
|
|
851
|
+
options={balanceProviders.map((p) => ({
|
|
852
|
+
title: providerOptionTitle(p),
|
|
853
|
+
value: p.id,
|
|
854
|
+
}))}
|
|
855
|
+
onSelect={(opt) => {
|
|
856
|
+
const provider = getBalanceProvider(opt.value)
|
|
857
|
+
// 手动指定 provider 会关闭自动切换
|
|
858
|
+
api.kv.set(`${KV_PREFIX}.balance.provider`, provider.id)
|
|
859
|
+
api.kv.set(`${KV_PREFIX}.balance.auto`, false)
|
|
860
|
+
signals.setBalanceProviderId(provider.id)
|
|
861
|
+
signals.setAutoBalance(false)
|
|
862
|
+
// 切换后立即刷新显示(防止取消输入时残留上一 provider 的余额)
|
|
863
|
+
signals.setBalanceRefresh(signals.balanceRefresh() + 1)
|
|
864
|
+
// 步骤 2:输入 key
|
|
865
|
+
promptBalanceKey(dialog, provider)
|
|
866
|
+
}}
|
|
867
|
+
/>
|
|
868
|
+
))
|
|
869
|
+
},
|
|
870
|
+
},
|
|
871
|
+
{
|
|
872
|
+
title: "Cache: Debug Skills Detection",
|
|
873
|
+
value: "cache.debug-skills",
|
|
874
|
+
description: "Dump all tool parts found in the current session for skill detection debugging",
|
|
875
|
+
slash: { name: "cache-debug-skills" },
|
|
876
|
+
onSelect: () => {
|
|
877
|
+
const t = createT(() => langCode())
|
|
878
|
+
const rt = api.route.current
|
|
879
|
+
if (rt.name !== "session" || !rt.params) {
|
|
880
|
+
api.ui.toast({ message: t("runInSession"), variant: "warning" })
|
|
881
|
+
return
|
|
882
|
+
}
|
|
883
|
+
const sid = String(rt.params.sessionID)
|
|
884
|
+
const msgs = api.state.session.messages(sid)
|
|
885
|
+
const byTool: Record<string, number> = {}
|
|
886
|
+
const skillParts: string[] = []
|
|
887
|
+
for (const msg of msgs) {
|
|
888
|
+
if (msg.role !== "assistant") continue
|
|
889
|
+
let parts: readonly any[] = []
|
|
890
|
+
try { parts = api.state.part(msg.id) } catch {}
|
|
891
|
+
for (const p of parts) {
|
|
892
|
+
if (p.type === "tool") {
|
|
893
|
+
const t = String(p.tool ?? "?")
|
|
894
|
+
byTool[t] = (byTool[t] ?? 0) + 1
|
|
895
|
+
if (t === "skill") {
|
|
896
|
+
const meta = p.state?.metadata
|
|
897
|
+
const rootMeta = p.metadata
|
|
898
|
+
skillParts.push(`state.metadata=${JSON.stringify(meta)} | root.metadata=${JSON.stringify(rootMeta)} | state.title="${p.state?.title}" | state.output[:80]="${String(p.state?.output ?? "").slice(0, 80)}"`)
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
const summary = Object.entries(byTool).map(([k, v]) => `${k}: ${v}`).join(" | ")
|
|
904
|
+
const extra = skillParts.length > 0 ? "\n\nSkill parts:\n" + skillParts.join("\n") : "\n\n⚠ No skill tool parts found — AI may be reading SKILL.md instead. Try: 'Use the skill tool to load karpathy-guidelines'"
|
|
905
|
+
api.ui.toast({
|
|
906
|
+
title: `Tool Summary (${Object.keys(byTool).length} types)`,
|
|
907
|
+
message: summary + extra,
|
|
908
|
+
duration: 15000,
|
|
909
|
+
})
|
|
910
|
+
},
|
|
911
|
+
},
|
|
912
|
+
{
|
|
913
|
+
title: "Cache: Sub-Agent Stats",
|
|
914
|
+
value: "cache.session",
|
|
915
|
+
description: "View token cache statistics for a sub-agent by session ID",
|
|
916
|
+
slash: { name: "cache-session" },
|
|
917
|
+
onSelect: (dialog) => {
|
|
918
|
+
// ── 扫描当前主 session 的子代理 session ID 列表 ──
|
|
919
|
+
const rt = api.route.current
|
|
920
|
+
const parentSid = rt.name === "session" && rt.params ? String(rt.params.sessionID) : ""
|
|
921
|
+
const SUBAGENT_TOOLS = new Set(["task", "delegate", "call_omo_agent"])
|
|
922
|
+
|
|
923
|
+
interface ChildEntry { title: string; value: string; description: string }
|
|
924
|
+
const children: ChildEntry[] = []
|
|
925
|
+
if (parentSid) {
|
|
926
|
+
try {
|
|
927
|
+
const msgs = api.state.session.messages(parentSid)
|
|
928
|
+
for (const msg of msgs) {
|
|
929
|
+
if (msg.role !== "assistant") continue
|
|
930
|
+
let parts: readonly Part[] = []
|
|
931
|
+
try { parts = api.state.part(msg.id) } catch {}
|
|
932
|
+
for (const p of parts) {
|
|
933
|
+
if (p.type !== "tool") continue
|
|
934
|
+
const tool = String((p as ToolPart).tool ?? "")
|
|
935
|
+
if (!SUBAGENT_TOOLS.has(tool)) continue
|
|
936
|
+
const st = (p as any).state as Record<string, unknown> | undefined
|
|
937
|
+
const stMeta = st?.metadata as Record<string, unknown> | undefined
|
|
938
|
+
const subSid = stMeta?.session_id ?? stMeta?.sessionId
|
|
939
|
+
if (!subSid) continue
|
|
940
|
+
const sidStr = String(subSid)
|
|
941
|
+
const input = st?.input as Record<string, unknown> | undefined
|
|
942
|
+
const agent = String((p as any).subagent_type ?? input?.subagent_type ?? input?.category ?? tool)
|
|
943
|
+
const prompt = String(input?.prompt ?? "")
|
|
944
|
+
const desc = input?.description ? String(input.description) : ""
|
|
945
|
+
const title = desc || prompt.replace(/\n/g, " ").replace(/\s+/g, " ").trim().slice(0, 40) || agent
|
|
946
|
+
children.push({ title, value: sidStr, description: `${agent} · ${sidStr.slice(0, 24)}…` })
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
} catch {}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// 去重
|
|
953
|
+
const seen = new Set<string>()
|
|
954
|
+
const unique = children.filter(c => { if (seen.has(c.value)) return false; seen.add(c.value); return true })
|
|
955
|
+
|
|
956
|
+
if (unique.length > 0) {
|
|
957
|
+
// ── 有子代理 → DialogSelect 列表选择 ──
|
|
958
|
+
const t = createT(() => langCode())
|
|
959
|
+
const currentSid = signals.overrideSessionId() ?? api.kv.get<string>(`${KV_PREFIX}.session`, "")
|
|
960
|
+
const options = unique.map((c, i) => ({
|
|
961
|
+
title: `${i + 1}. ${c.title}`,
|
|
962
|
+
value: c.value,
|
|
963
|
+
description: c.description,
|
|
964
|
+
}))
|
|
965
|
+
// 首尾各放一个"回到主会话",长列表时顶部底部均可直达
|
|
966
|
+
const backValue = "__main__"
|
|
967
|
+
const backTitle = `\u2500 ${t("backToMainTitle")}`
|
|
968
|
+
options.unshift({ title: backTitle, value: backValue, description: "" })
|
|
969
|
+
options.push({ title: backTitle, value: backValue, description: "" })
|
|
970
|
+
const currentIdx = currentSid ? options.findIndex(o => o.value === currentSid) : -1
|
|
971
|
+
dialog?.replace(() => (
|
|
972
|
+
<api.ui.DialogSelect
|
|
973
|
+
title={t("subSelectTitle")}
|
|
974
|
+
options={options}
|
|
975
|
+
current={currentIdx >= 0 ? options[currentIdx].value : undefined}
|
|
976
|
+
onSelect={(opt) => {
|
|
977
|
+
if (opt.value === backValue) {
|
|
978
|
+
signals.setOverrideSessionId(undefined)
|
|
979
|
+
api.kv.set(`${KV_PREFIX}.session`, "")
|
|
980
|
+
api.ui.toast({ message: t("backToMain") })
|
|
981
|
+
} else {
|
|
982
|
+
signals.setOverrideSessionId(opt.value)
|
|
983
|
+
api.kv.set(`${KV_PREFIX}.session`, opt.value)
|
|
984
|
+
api.ui.toast({ message: t("subAgentSwitched", { s: opt.value.slice(0, 24) + "\u2026" }) })
|
|
985
|
+
}
|
|
986
|
+
dialog?.clear()
|
|
987
|
+
}}
|
|
988
|
+
/>
|
|
989
|
+
))
|
|
990
|
+
} else {
|
|
991
|
+
// ── 无子代理 → DialogPrompt 手动粘贴 ──
|
|
992
|
+
const t = createT(() => langCode())
|
|
993
|
+
dialog?.replace(() => (
|
|
994
|
+
<api.ui.DialogPrompt
|
|
995
|
+
title={signals.overrideSessionId() ? t("subSwitchTitle") : t("subViewTitle")}
|
|
996
|
+
description={() => <text>{t("subNoFound")}</text>}
|
|
997
|
+
placeholder="ses_..."
|
|
998
|
+
value={signals.overrideSessionId() ?? api.kv.get<string>(`${KV_PREFIX}.session`, "") ?? ""}
|
|
999
|
+
onConfirm={(val) => {
|
|
1000
|
+
const sid = val.trim()
|
|
1001
|
+
if (sid) {
|
|
1002
|
+
signals.setOverrideSessionId(sid)
|
|
1003
|
+
api.kv.set(`${KV_PREFIX}.session`, sid)
|
|
1004
|
+
api.ui.toast({ message: t("subAgentSwitched", { s: sid.slice(0, 24) + "\u2026" }) })
|
|
1005
|
+
}
|
|
1006
|
+
dialog?.clear()
|
|
1007
|
+
}}
|
|
1008
|
+
onCancel={() => dialog?.clear()}
|
|
1009
|
+
/>
|
|
1010
|
+
))
|
|
1011
|
+
}
|
|
1012
|
+
},
|
|
1013
|
+
},
|
|
1014
|
+
{
|
|
1015
|
+
title: "Cache: Back to Main",
|
|
1016
|
+
value: "cache.session.back",
|
|
1017
|
+
description: "Return to main session stats",
|
|
1018
|
+
slash: { name: "cache-session-back" },
|
|
1019
|
+
onSelect: (dialog) => {
|
|
1020
|
+
const t = createT(() => langCode())
|
|
1021
|
+
signals.setOverrideSessionId(undefined)
|
|
1022
|
+
api.kv.set(`${KV_PREFIX}.session`, "")
|
|
1023
|
+
api.ui.toast({ message: t("backToMain") })
|
|
1024
|
+
dialog?.clear()
|
|
1025
|
+
},
|
|
1026
|
+
},
|
|
1027
|
+
])
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
const mod: TuiPluginModule & { id: string } = {
|
|
1031
|
+
id: "opencode-visual-cache",
|
|
1032
|
+
tui,
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
export default mod
|