dsh-plugin-subscription-oauth 0.2.2 → 0.4.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/README.md +13 -0
- package/README.zh.md +371 -0
- package/lib/auth/ark-control.d.ts +180 -0
- package/lib/auth/ark-control.js +304 -0
- package/lib/auth/ark-flow.d.ts +134 -0
- package/lib/auth/ark-flow.js +239 -0
- package/lib/client/UsageChip.d.ts +120 -0
- package/lib/client/UsageChip.js +232 -0
- package/lib/client/index.d.ts +27 -0
- package/lib/client/index.js +29 -0
- package/lib/client/locales.d.ts +4 -0
- package/lib/client/locales.js +4 -0
- package/lib/client/styles.d.ts +15 -1
- package/lib/client/styles.js +128 -1
- package/lib/client.js +413 -5
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +2 -2
- package/lib/index.js +15 -2
- package/lib/providers/ark-provider.d.ts +42 -0
- package/lib/providers/ark-provider.js +166 -0
- package/lib/providers/ark.d.ts +154 -0
- package/lib/providers/ark.js +412 -0
- package/lib/providers/auto.js +16 -0
- package/lib/providers/usage.js +49 -4
- package/package.json +1 -1
package/lib/client/styles.js
CHANGED
|
@@ -243,7 +243,13 @@ ${scope} .dsh-subscription-disclosure:hover:not(:disabled) {
|
|
|
243
243
|
${scope} .dsh-subscription-meter {
|
|
244
244
|
height: 6px;
|
|
245
245
|
border-radius: 999px;
|
|
246
|
-
|
|
246
|
+
/* The track must stay visible when nothing is spent — a 0 % window otherwise reads as
|
|
247
|
+
"no meter at all", which is the opposite of "nothing used". The layer token falls
|
|
248
|
+
back to the card's own colour on a host that does not define it, so the neutral is
|
|
249
|
+
explicit and translucent, and the inset ring keeps the empty track visible on a
|
|
250
|
+
light theme as well as a dark one. */
|
|
251
|
+
background: #8a8f9826;
|
|
252
|
+
box-shadow: inset 0 0 0 1px #8a8f9833;
|
|
247
253
|
overflow: hidden;
|
|
248
254
|
}
|
|
249
255
|
${scope} .dsh-subscription-meter > span {
|
|
@@ -255,3 +261,124 @@ ${scope} .dsh-subscription-meter > span[data-exhausted="true"] {
|
|
|
255
261
|
background: var(--dsw-alias-state-error-primary, #b42318);
|
|
256
262
|
}
|
|
257
263
|
`;
|
|
264
|
+
/**
|
|
265
|
+
* The composer usage chip's stylesheet.
|
|
266
|
+
*
|
|
267
|
+
* Separate from the page's, because the chip renders in the composer's own bar rather
|
|
268
|
+
* than inside either page container — the page stylesheet is scoped to those
|
|
269
|
+
* containers on purpose, so nothing here may leak into the shell.
|
|
270
|
+
*
|
|
271
|
+
* The chip inherits the bar's type and only draws a meter, so it sits beside the shell's
|
|
272
|
+
* own stats pills without looking like a second design. The trigger is a bare control:
|
|
273
|
+
* the host gives plugin markup no control classes, and a default `<button>` would
|
|
274
|
+
* otherwise read as a raised chip. Every token carries a fallback for hosts that do not
|
|
275
|
+
* define it.
|
|
276
|
+
*/
|
|
277
|
+
export const usageChipCss = `
|
|
278
|
+
.dsh-subscription-usage-chip {
|
|
279
|
+
position: relative;
|
|
280
|
+
display: inline-flex;
|
|
281
|
+
align-items: center;
|
|
282
|
+
/* The bar is a centred flex row whose children are this slot's entries followed by the
|
|
283
|
+
shell's context meter, and a list slot renders its entries as bare children (no
|
|
284
|
+
wrapper). So the chip is itself a flex item: a higher order places it after the
|
|
285
|
+
context meter, and no auto margin is set, so it sits immediately beside it rather
|
|
286
|
+
than being pushed to the edge of the bar. */
|
|
287
|
+
order: 1;
|
|
288
|
+
min-width: 0;
|
|
289
|
+
font-family: inherit;
|
|
290
|
+
font-size: 12px;
|
|
291
|
+
line-height: 1;
|
|
292
|
+
}
|
|
293
|
+
.dsh-subscription-usage-chip-trigger {
|
|
294
|
+
display: inline-flex;
|
|
295
|
+
align-items: center;
|
|
296
|
+
gap: 6px;
|
|
297
|
+
margin: 0;
|
|
298
|
+
padding: 2px 6px;
|
|
299
|
+
border: 0;
|
|
300
|
+
border-radius: var(--dsw-radius-sm, 6px);
|
|
301
|
+
background: transparent;
|
|
302
|
+
color: var(--dsw-alias-label-secondary, #8a8f98);
|
|
303
|
+
font: inherit;
|
|
304
|
+
cursor: pointer;
|
|
305
|
+
}
|
|
306
|
+
.dsh-subscription-usage-chip-trigger:hover {
|
|
307
|
+
background: var(--dsw-alias-interactive-bg-hover, #ffffff14);
|
|
308
|
+
}
|
|
309
|
+
.dsh-subscription-usage-chip-trigger:focus-visible {
|
|
310
|
+
outline: 2px solid var(--dsw-alias-border-focus, #4d6bfe);
|
|
311
|
+
outline-offset: 1px;
|
|
312
|
+
}
|
|
313
|
+
.dsh-subscription-usage-chip-label {
|
|
314
|
+
overflow: hidden;
|
|
315
|
+
max-width: 6ch;
|
|
316
|
+
text-overflow: ellipsis;
|
|
317
|
+
white-space: nowrap;
|
|
318
|
+
}
|
|
319
|
+
.dsh-subscription-usage-chip-cell {
|
|
320
|
+
display: inline-flex;
|
|
321
|
+
align-items: baseline;
|
|
322
|
+
gap: 2px;
|
|
323
|
+
}
|
|
324
|
+
.dsh-subscription-usage-chip-window {
|
|
325
|
+
opacity: 0.75;
|
|
326
|
+
}
|
|
327
|
+
.dsh-subscription-usage-chip-value {
|
|
328
|
+
color: var(--dsw-alias-label-primary, #e8e8e8);
|
|
329
|
+
font-variant-numeric: tabular-nums;
|
|
330
|
+
}
|
|
331
|
+
.dsh-subscription-usage-chip-panel {
|
|
332
|
+
position: absolute;
|
|
333
|
+
bottom: calc(100% + 6px);
|
|
334
|
+
right: 0;
|
|
335
|
+
z-index: 20;
|
|
336
|
+
display: flex;
|
|
337
|
+
flex-direction: column;
|
|
338
|
+
gap: 8px;
|
|
339
|
+
min-width: 168px;
|
|
340
|
+
padding: 10px 12px;
|
|
341
|
+
border: 1px solid var(--dsw-alias-border-secondary, #ffffff1f);
|
|
342
|
+
border-radius: var(--dsw-radius-md, 10px);
|
|
343
|
+
background: var(--dsw-alias-bg-layer-2, #26262a);
|
|
344
|
+
box-shadow: var(--dsw-elevation-prominent, 0 12px 32px #0007);
|
|
345
|
+
color: var(--dsw-alias-label-primary, #e8e8e8);
|
|
346
|
+
white-space: nowrap;
|
|
347
|
+
}
|
|
348
|
+
.dsh-subscription-usage-chip-full {
|
|
349
|
+
font-size: 11px;
|
|
350
|
+
color: var(--dsw-alias-label-secondary, #8a8f98);
|
|
351
|
+
}
|
|
352
|
+
.dsh-subscription-usage-chip-meter {
|
|
353
|
+
display: flex;
|
|
354
|
+
flex-direction: column;
|
|
355
|
+
gap: 4px;
|
|
356
|
+
}
|
|
357
|
+
.dsh-subscription-usage-chip-meterHead {
|
|
358
|
+
display: flex;
|
|
359
|
+
align-items: baseline;
|
|
360
|
+
justify-content: space-between;
|
|
361
|
+
gap: 12px;
|
|
362
|
+
font-size: 12px;
|
|
363
|
+
}
|
|
364
|
+
.dsh-subscription-usage-chip-detail {
|
|
365
|
+
color: var(--dsw-alias-label-secondary, #8a8f98);
|
|
366
|
+
font-variant-numeric: tabular-nums;
|
|
367
|
+
}
|
|
368
|
+
.dsh-subscription-usage-chip-track {
|
|
369
|
+
display: block;
|
|
370
|
+
width: 100%;
|
|
371
|
+
height: 5px;
|
|
372
|
+
border-radius: 999px;
|
|
373
|
+
/* As on the settings meter: an unspent window has to show its track, so the neutral and
|
|
374
|
+
its ring are explicit rather than a layer token that may match the panel. */
|
|
375
|
+
background: #8a8f9826;
|
|
376
|
+
box-shadow: inset 0 0 0 1px #8a8f9833;
|
|
377
|
+
overflow: hidden;
|
|
378
|
+
}
|
|
379
|
+
.dsh-subscription-usage-chip-fill {
|
|
380
|
+
display: block;
|
|
381
|
+
height: 100%;
|
|
382
|
+
background: var(--dsw-alias-state-success-primary, #067647);
|
|
383
|
+
}
|
|
384
|
+
`;
|
package/lib/client.js
CHANGED
|
@@ -126,7 +126,9 @@ const en = {
|
|
|
126
126
|
detail_project: "Cloud project",
|
|
127
127
|
lastError: "Last login attempt failed: {message}",
|
|
128
128
|
loginFailed: "Could not start the sign-in: {message}",
|
|
129
|
-
actionFailed: "Action failed: {message}"
|
|
129
|
+
actionFailed: "Action failed: {message}",
|
|
130
|
+
usageChipTitle: "{provider} subscription usage",
|
|
131
|
+
usageChipFull: "{provider} allowance"
|
|
130
132
|
};
|
|
131
133
|
/** Chinese strings. */
|
|
132
134
|
const zh = {
|
|
@@ -218,7 +220,9 @@ const zh = {
|
|
|
218
220
|
detail_project: "云项目",
|
|
219
221
|
lastError: "上次登录失败:{message}",
|
|
220
222
|
loginFailed: "无法开始登录:{message}",
|
|
221
|
-
actionFailed: "操作失败:{message}"
|
|
223
|
+
actionFailed: "操作失败:{message}",
|
|
224
|
+
usageChipTitle: "{provider} 订阅用量",
|
|
225
|
+
usageChipFull: "{provider} 额度"
|
|
222
226
|
};
|
|
223
227
|
|
|
224
228
|
//#endregion
|
|
@@ -468,7 +472,13 @@ ${scope} .dsh-subscription-disclosure:hover:not(:disabled) {
|
|
|
468
472
|
${scope} .dsh-subscription-meter {
|
|
469
473
|
height: 6px;
|
|
470
474
|
border-radius: 999px;
|
|
471
|
-
|
|
475
|
+
/* The track must stay visible when nothing is spent — a 0 % window otherwise reads as
|
|
476
|
+
"no meter at all", which is the opposite of "nothing used". The layer token falls
|
|
477
|
+
back to the card's own colour on a host that does not define it, so the neutral is
|
|
478
|
+
explicit and translucent, and the inset ring keeps the empty track visible on a
|
|
479
|
+
light theme as well as a dark one. */
|
|
480
|
+
background: #8a8f9826;
|
|
481
|
+
box-shadow: inset 0 0 0 1px #8a8f9833;
|
|
472
482
|
overflow: hidden;
|
|
473
483
|
}
|
|
474
484
|
${scope} .dsh-subscription-meter > span {
|
|
@@ -480,6 +490,127 @@ ${scope} .dsh-subscription-meter > span[data-exhausted="true"] {
|
|
|
480
490
|
background: var(--dsw-alias-state-error-primary, #b42318);
|
|
481
491
|
}
|
|
482
492
|
`;
|
|
493
|
+
/**
|
|
494
|
+
* The composer usage chip's stylesheet.
|
|
495
|
+
*
|
|
496
|
+
* Separate from the page's, because the chip renders in the composer's own bar rather
|
|
497
|
+
* than inside either page container — the page stylesheet is scoped to those
|
|
498
|
+
* containers on purpose, so nothing here may leak into the shell.
|
|
499
|
+
*
|
|
500
|
+
* The chip inherits the bar's type and only draws a meter, so it sits beside the shell's
|
|
501
|
+
* own stats pills without looking like a second design. The trigger is a bare control:
|
|
502
|
+
* the host gives plugin markup no control classes, and a default `<button>` would
|
|
503
|
+
* otherwise read as a raised chip. Every token carries a fallback for hosts that do not
|
|
504
|
+
* define it.
|
|
505
|
+
*/
|
|
506
|
+
const usageChipCss = `
|
|
507
|
+
.dsh-subscription-usage-chip {
|
|
508
|
+
position: relative;
|
|
509
|
+
display: inline-flex;
|
|
510
|
+
align-items: center;
|
|
511
|
+
/* The bar is a centred flex row whose children are this slot's entries followed by the
|
|
512
|
+
shell's context meter, and a list slot renders its entries as bare children (no
|
|
513
|
+
wrapper). So the chip is itself a flex item: a higher order places it after the
|
|
514
|
+
context meter, and no auto margin is set, so it sits immediately beside it rather
|
|
515
|
+
than being pushed to the edge of the bar. */
|
|
516
|
+
order: 1;
|
|
517
|
+
min-width: 0;
|
|
518
|
+
font-family: inherit;
|
|
519
|
+
font-size: 12px;
|
|
520
|
+
line-height: 1;
|
|
521
|
+
}
|
|
522
|
+
.dsh-subscription-usage-chip-trigger {
|
|
523
|
+
display: inline-flex;
|
|
524
|
+
align-items: center;
|
|
525
|
+
gap: 6px;
|
|
526
|
+
margin: 0;
|
|
527
|
+
padding: 2px 6px;
|
|
528
|
+
border: 0;
|
|
529
|
+
border-radius: var(--dsw-radius-sm, 6px);
|
|
530
|
+
background: transparent;
|
|
531
|
+
color: var(--dsw-alias-label-secondary, #8a8f98);
|
|
532
|
+
font: inherit;
|
|
533
|
+
cursor: pointer;
|
|
534
|
+
}
|
|
535
|
+
.dsh-subscription-usage-chip-trigger:hover {
|
|
536
|
+
background: var(--dsw-alias-interactive-bg-hover, #ffffff14);
|
|
537
|
+
}
|
|
538
|
+
.dsh-subscription-usage-chip-trigger:focus-visible {
|
|
539
|
+
outline: 2px solid var(--dsw-alias-border-focus, #4d6bfe);
|
|
540
|
+
outline-offset: 1px;
|
|
541
|
+
}
|
|
542
|
+
.dsh-subscription-usage-chip-label {
|
|
543
|
+
overflow: hidden;
|
|
544
|
+
max-width: 6ch;
|
|
545
|
+
text-overflow: ellipsis;
|
|
546
|
+
white-space: nowrap;
|
|
547
|
+
}
|
|
548
|
+
.dsh-subscription-usage-chip-cell {
|
|
549
|
+
display: inline-flex;
|
|
550
|
+
align-items: baseline;
|
|
551
|
+
gap: 2px;
|
|
552
|
+
}
|
|
553
|
+
.dsh-subscription-usage-chip-window {
|
|
554
|
+
opacity: 0.75;
|
|
555
|
+
}
|
|
556
|
+
.dsh-subscription-usage-chip-value {
|
|
557
|
+
color: var(--dsw-alias-label-primary, #e8e8e8);
|
|
558
|
+
font-variant-numeric: tabular-nums;
|
|
559
|
+
}
|
|
560
|
+
.dsh-subscription-usage-chip-panel {
|
|
561
|
+
position: absolute;
|
|
562
|
+
bottom: calc(100% + 6px);
|
|
563
|
+
right: 0;
|
|
564
|
+
z-index: 20;
|
|
565
|
+
display: flex;
|
|
566
|
+
flex-direction: column;
|
|
567
|
+
gap: 8px;
|
|
568
|
+
min-width: 168px;
|
|
569
|
+
padding: 10px 12px;
|
|
570
|
+
border: 1px solid var(--dsw-alias-border-secondary, #ffffff1f);
|
|
571
|
+
border-radius: var(--dsw-radius-md, 10px);
|
|
572
|
+
background: var(--dsw-alias-bg-layer-2, #26262a);
|
|
573
|
+
box-shadow: var(--dsw-elevation-prominent, 0 12px 32px #0007);
|
|
574
|
+
color: var(--dsw-alias-label-primary, #e8e8e8);
|
|
575
|
+
white-space: nowrap;
|
|
576
|
+
}
|
|
577
|
+
.dsh-subscription-usage-chip-full {
|
|
578
|
+
font-size: 11px;
|
|
579
|
+
color: var(--dsw-alias-label-secondary, #8a8f98);
|
|
580
|
+
}
|
|
581
|
+
.dsh-subscription-usage-chip-meter {
|
|
582
|
+
display: flex;
|
|
583
|
+
flex-direction: column;
|
|
584
|
+
gap: 4px;
|
|
585
|
+
}
|
|
586
|
+
.dsh-subscription-usage-chip-meterHead {
|
|
587
|
+
display: flex;
|
|
588
|
+
align-items: baseline;
|
|
589
|
+
justify-content: space-between;
|
|
590
|
+
gap: 12px;
|
|
591
|
+
font-size: 12px;
|
|
592
|
+
}
|
|
593
|
+
.dsh-subscription-usage-chip-detail {
|
|
594
|
+
color: var(--dsw-alias-label-secondary, #8a8f98);
|
|
595
|
+
font-variant-numeric: tabular-nums;
|
|
596
|
+
}
|
|
597
|
+
.dsh-subscription-usage-chip-track {
|
|
598
|
+
display: block;
|
|
599
|
+
width: 100%;
|
|
600
|
+
height: 5px;
|
|
601
|
+
border-radius: 999px;
|
|
602
|
+
/* As on the settings meter: an unspent window has to show its track, so the neutral and
|
|
603
|
+
its ring are explicit rather than a layer token that may match the panel. */
|
|
604
|
+
background: #8a8f9826;
|
|
605
|
+
box-shadow: inset 0 0 0 1px #8a8f9833;
|
|
606
|
+
overflow: hidden;
|
|
607
|
+
}
|
|
608
|
+
.dsh-subscription-usage-chip-fill {
|
|
609
|
+
display: block;
|
|
610
|
+
height: 100%;
|
|
611
|
+
background: var(--dsw-alias-state-success-primary, #067647);
|
|
612
|
+
}
|
|
613
|
+
`;
|
|
483
614
|
|
|
484
615
|
//#endregion
|
|
485
616
|
//#region src/protocol.ts
|
|
@@ -586,7 +717,7 @@ const RESET_RELATIVE_MS = 1440 * 60 * 1e3;
|
|
|
586
717
|
* @param t - translator, so the wording follows the page language.
|
|
587
718
|
* @returns the reset text.
|
|
588
719
|
*/
|
|
589
|
-
function formatReset(at, t) {
|
|
720
|
+
function formatReset$1(at, t) {
|
|
590
721
|
const remaining = at - Date.now();
|
|
591
722
|
if (remaining <= 0) return t("usageResetDue");
|
|
592
723
|
if (remaining >= RESET_RELATIVE_MS) return t("usageReset", { time: formatTime(at) });
|
|
@@ -1051,7 +1182,7 @@ function UsageBlock(props) {
|
|
|
1051
1182
|
]
|
|
1052
1183
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1053
1184
|
style: styles.modelMeta,
|
|
1054
|
-
children: window$1.resetsAt === void 0 ? "" : formatReset(window$1.resetsAt, t)
|
|
1185
|
+
children: window$1.resetsAt === void 0 ? "" : formatReset$1(window$1.resetsAt, t)
|
|
1055
1186
|
})]
|
|
1056
1187
|
}), window$1.usedPercent !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1057
1188
|
className: "dsh-subscription-meter",
|
|
@@ -2050,13 +2181,280 @@ function SubscriptionsSection(props) {
|
|
|
2050
2181
|
});
|
|
2051
2182
|
}
|
|
2052
2183
|
|
|
2184
|
+
//#endregion
|
|
2185
|
+
//#region src/client/UsageChip.tsx
|
|
2186
|
+
/** How often a visible chip refreshes its figures. */
|
|
2187
|
+
const REFRESH_INTERVAL_MS = 6e4;
|
|
2188
|
+
/**
|
|
2189
|
+
* Abbreviations for the routes this plugin serves.
|
|
2190
|
+
*
|
|
2191
|
+
* The bar is a status line under the composer, where a full product name crowds out the
|
|
2192
|
+
* shell's own pills; the full name is one click away. The fallback shortens an unknown
|
|
2193
|
+
* route's display name rather than dropping it.
|
|
2194
|
+
*/
|
|
2195
|
+
const SHORT_LABELS = {
|
|
2196
|
+
kimi: "Kimi",
|
|
2197
|
+
grok: "Grok",
|
|
2198
|
+
antigravity: "Anti",
|
|
2199
|
+
glm: "GLM",
|
|
2200
|
+
ark: "Ark"
|
|
2201
|
+
};
|
|
2202
|
+
/**
|
|
2203
|
+
* Abbreviations for the window names the providers use.
|
|
2204
|
+
*
|
|
2205
|
+
* The bar has room for a token or two, so `weekly` becomes `7d` — the same figure in a
|
|
2206
|
+
* form that reads at a glance. An unrecognised window keeps its own name, shortened
|
|
2207
|
+
* only if it is long.
|
|
2208
|
+
*/
|
|
2209
|
+
const SHORT_WINDOWS = {
|
|
2210
|
+
"weekly": "7d",
|
|
2211
|
+
"7d": "7d",
|
|
2212
|
+
"5h": "5h",
|
|
2213
|
+
"monthly": "mo",
|
|
2214
|
+
"daily": "1d",
|
|
2215
|
+
"credits": "cr",
|
|
2216
|
+
"free": "free",
|
|
2217
|
+
"session": "s"
|
|
2218
|
+
};
|
|
2219
|
+
/** The session's provider, from its model-selection projection. */
|
|
2220
|
+
function providerOf(projection) {
|
|
2221
|
+
return (projection?.next ?? projection?.lastUsed)?.provider;
|
|
2222
|
+
}
|
|
2223
|
+
/**
|
|
2224
|
+
* The short form of a route's name.
|
|
2225
|
+
*
|
|
2226
|
+
* @param provider - the route id.
|
|
2227
|
+
* @param label - the route's own display name.
|
|
2228
|
+
* @returns an abbreviation that still identifies the route.
|
|
2229
|
+
*/
|
|
2230
|
+
function shortLabel(provider, label) {
|
|
2231
|
+
const known = SHORT_LABELS[provider];
|
|
2232
|
+
if (known !== void 0) return known;
|
|
2233
|
+
const [first] = label.trim().split(/\s+/);
|
|
2234
|
+
return first === void 0 || first === "" ? provider : first;
|
|
2235
|
+
}
|
|
2236
|
+
/**
|
|
2237
|
+
* The short form of a window's name.
|
|
2238
|
+
*
|
|
2239
|
+
* @param label - the window's own name, e.g. `weekly`.
|
|
2240
|
+
* @returns an abbreviation, or the label itself when it is already short.
|
|
2241
|
+
*/
|
|
2242
|
+
function shortWindow(label) {
|
|
2243
|
+
const known = SHORT_WINDOWS[label.toLowerCase()];
|
|
2244
|
+
if (known !== void 0) return known;
|
|
2245
|
+
return label.length <= 4 ? label : label.slice(0, 4);
|
|
2246
|
+
}
|
|
2247
|
+
/**
|
|
2248
|
+
* Pick the window a single line should show.
|
|
2249
|
+
*
|
|
2250
|
+
* A plan can meter several windows at once (a rolling five-hour one and a weekly one).
|
|
2251
|
+
* The most constrained is the one that will stop work soonest, so it earns the space;
|
|
2252
|
+
* ties fall to the earlier window, which is the order the providers state them in.
|
|
2253
|
+
*
|
|
2254
|
+
* @param windows - the route's metered windows.
|
|
2255
|
+
* @returns the window to show, or undefined when none states a figure.
|
|
2256
|
+
*/
|
|
2257
|
+
function pickWindow(windows) {
|
|
2258
|
+
let best;
|
|
2259
|
+
for (const window$1 of windows) {
|
|
2260
|
+
if (window$1.usedPercent === void 0) continue;
|
|
2261
|
+
if (best?.usedPercent === void 0 || window$1.usedPercent > best.usedPercent) best = window$1;
|
|
2262
|
+
}
|
|
2263
|
+
return best;
|
|
2264
|
+
}
|
|
2265
|
+
/**
|
|
2266
|
+
* Reduce one route's usage to what the chip renders.
|
|
2267
|
+
*
|
|
2268
|
+
* @param provider - the route the figures came from.
|
|
2269
|
+
* @param label - the route's display name.
|
|
2270
|
+
* @param usage - the accounts the route answered for.
|
|
2271
|
+
* @returns the reading, or undefined when the route stated no figure.
|
|
2272
|
+
*/
|
|
2273
|
+
function readingOf(provider, label, usage) {
|
|
2274
|
+
for (const entry of usage) {
|
|
2275
|
+
const window$1 = pickWindow(entry.windows);
|
|
2276
|
+
if (window$1 === void 0) continue;
|
|
2277
|
+
return {
|
|
2278
|
+
provider,
|
|
2279
|
+
label,
|
|
2280
|
+
windows: entry.windows.filter((candidate) => candidate.usedPercent !== void 0),
|
|
2281
|
+
window: window$1,
|
|
2282
|
+
...entry.plan === void 0 ? {} : { plan: entry.plan }
|
|
2283
|
+
};
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
/**
|
|
2287
|
+
* Format a window's reset the way the settings cards do.
|
|
2288
|
+
*
|
|
2289
|
+
* The rule is shared with the cards on purpose: a figure reading `6d20h` here and
|
|
2290
|
+
* something else there would be two answers to one question.
|
|
2291
|
+
*
|
|
2292
|
+
* @param at - epoch milliseconds, or undefined when the provider states none.
|
|
2293
|
+
* @param now - the instant to measure from.
|
|
2294
|
+
* @returns a short phrase, or undefined when there is no reset to show.
|
|
2295
|
+
*/
|
|
2296
|
+
function formatReset(at, now) {
|
|
2297
|
+
if (at === void 0) return void 0;
|
|
2298
|
+
const remaining = at - now;
|
|
2299
|
+
if (remaining <= 0) return "now";
|
|
2300
|
+
const hours = remaining / 36e5;
|
|
2301
|
+
if (hours < 1) return `${String(Math.max(1, Math.round(remaining / 6e4)))}m`;
|
|
2302
|
+
if (hours < 24) return `${String(Math.round(hours))}h`;
|
|
2303
|
+
const days = Math.floor(hours / 24);
|
|
2304
|
+
return `${String(days)}d${String(Math.round(hours - days * 24))}h`;
|
|
2305
|
+
}
|
|
2306
|
+
/**
|
|
2307
|
+
* The composer's usage chip.
|
|
2308
|
+
*
|
|
2309
|
+
* @param props - injected RPC caller, copy, and the session's provider reader.
|
|
2310
|
+
* @returns the chip, or null when there is nothing true to say.
|
|
2311
|
+
*/
|
|
2312
|
+
function UsageChip({ call, t, isOwnProvider, useProjection }) {
|
|
2313
|
+
const [reading, setReading] = (0, react.useState)(void 0);
|
|
2314
|
+
const [now, setNow] = (0, react.useState)(() => Date.now());
|
|
2315
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
2316
|
+
const lastOwn = (0, react.useRef)(void 0);
|
|
2317
|
+
const selected = useProjection("modelSelection", (value) => providerOf(value));
|
|
2318
|
+
const load = (0, react.useCallback)(async (target) => {
|
|
2319
|
+
try {
|
|
2320
|
+
const result = await call("usage", { provider: target });
|
|
2321
|
+
return readingOf(target, result.status.providers.find((entry) => entry.provider === target)?.displayName ?? target, result.usage);
|
|
2322
|
+
} catch {
|
|
2323
|
+
return;
|
|
2324
|
+
}
|
|
2325
|
+
}, [call]);
|
|
2326
|
+
(0, react.useEffect)(() => {
|
|
2327
|
+
const target = selected !== void 0 && isOwnProvider(selected) ? selected : lastOwn.current;
|
|
2328
|
+
if (target === void 0) {
|
|
2329
|
+
setReading(void 0);
|
|
2330
|
+
return;
|
|
2331
|
+
}
|
|
2332
|
+
let live = true;
|
|
2333
|
+
const refresh = () => {
|
|
2334
|
+
setNow(Date.now());
|
|
2335
|
+
load(target).then((next) => {
|
|
2336
|
+
if (!live || next === void 0) return;
|
|
2337
|
+
lastOwn.current = target;
|
|
2338
|
+
setReading(next);
|
|
2339
|
+
});
|
|
2340
|
+
};
|
|
2341
|
+
refresh();
|
|
2342
|
+
const timer = setInterval(refresh, REFRESH_INTERVAL_MS);
|
|
2343
|
+
return () => {
|
|
2344
|
+
live = false;
|
|
2345
|
+
clearInterval(timer);
|
|
2346
|
+
};
|
|
2347
|
+
}, [
|
|
2348
|
+
selected,
|
|
2349
|
+
isOwnProvider,
|
|
2350
|
+
load
|
|
2351
|
+
]);
|
|
2352
|
+
(0, react.useEffect)(() => {
|
|
2353
|
+
if (!open) return;
|
|
2354
|
+
const close = () => {
|
|
2355
|
+
setOpen(false);
|
|
2356
|
+
};
|
|
2357
|
+
document.addEventListener("click", close);
|
|
2358
|
+
return () => {
|
|
2359
|
+
document.removeEventListener("click", close);
|
|
2360
|
+
};
|
|
2361
|
+
}, [open]);
|
|
2362
|
+
if (reading === void 0) return null;
|
|
2363
|
+
if (reading.window.usedPercent === void 0) return null;
|
|
2364
|
+
formatReset(reading.window.resetsAt, now);
|
|
2365
|
+
const short = shortLabel(reading.provider, reading.label);
|
|
2366
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2367
|
+
className: "dsh-subscription-usage-chip",
|
|
2368
|
+
children: [
|
|
2369
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("style", { children: usageChipCss }),
|
|
2370
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2371
|
+
type: "button",
|
|
2372
|
+
className: "dsh-subscription-usage-chip-trigger",
|
|
2373
|
+
"aria-expanded": open,
|
|
2374
|
+
title: t("usageChipTitle", { provider: reading.label }),
|
|
2375
|
+
onClick: (event) => {
|
|
2376
|
+
event.stopPropagation();
|
|
2377
|
+
setOpen((was) => !was);
|
|
2378
|
+
},
|
|
2379
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2380
|
+
className: "dsh-subscription-usage-chip-label",
|
|
2381
|
+
children: short
|
|
2382
|
+
}), reading.windows.map((window$1) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2383
|
+
className: "dsh-subscription-usage-chip-cell",
|
|
2384
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2385
|
+
className: "dsh-subscription-usage-chip-window",
|
|
2386
|
+
children: shortWindow(window$1.label)
|
|
2387
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2388
|
+
className: "dsh-subscription-usage-chip-value",
|
|
2389
|
+
children: `${String(Math.round(window$1.usedPercent ?? 0))}%`
|
|
2390
|
+
})]
|
|
2391
|
+
}, window$1.label))]
|
|
2392
|
+
}),
|
|
2393
|
+
open && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2394
|
+
className: "dsh-subscription-usage-chip-panel",
|
|
2395
|
+
role: "status",
|
|
2396
|
+
children: [
|
|
2397
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2398
|
+
className: "dsh-subscription-usage-chip-full",
|
|
2399
|
+
children: t("usageChipFull", { provider: reading.label })
|
|
2400
|
+
}),
|
|
2401
|
+
reading.windows.map((window$1) => {
|
|
2402
|
+
const windowReset = formatReset(window$1.resetsAt, now);
|
|
2403
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2404
|
+
className: "dsh-subscription-usage-chip-meter",
|
|
2405
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2406
|
+
className: "dsh-subscription-usage-chip-meterHead",
|
|
2407
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: window$1.label }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2408
|
+
className: "dsh-subscription-usage-chip-detail",
|
|
2409
|
+
children: `${String(Math.round(window$1.usedPercent ?? 0))}%${windowReset === void 0 ? "" : ` · ${windowReset}`}`
|
|
2410
|
+
})]
|
|
2411
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2412
|
+
className: "dsh-subscription-usage-chip-track",
|
|
2413
|
+
"aria-hidden": "true",
|
|
2414
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2415
|
+
className: "dsh-subscription-usage-chip-fill",
|
|
2416
|
+
style: { width: `${String(Math.max(0, Math.min(100, window$1.usedPercent ?? 0)))}%` }
|
|
2417
|
+
})
|
|
2418
|
+
})]
|
|
2419
|
+
}, window$1.label);
|
|
2420
|
+
}),
|
|
2421
|
+
reading.plan === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2422
|
+
className: "dsh-subscription-usage-chip-detail",
|
|
2423
|
+
children: reading.plan
|
|
2424
|
+
})
|
|
2425
|
+
]
|
|
2426
|
+
})
|
|
2427
|
+
]
|
|
2428
|
+
});
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2053
2431
|
//#endregion
|
|
2054
2432
|
//#region src/client/index.ts
|
|
2433
|
+
/**
|
|
2434
|
+
* Routes this plugin serves.
|
|
2435
|
+
*
|
|
2436
|
+
* The chip only reports for these; a provider somebody else registered gets no chip,
|
|
2437
|
+
* because this plugin has no way to read its allowance.
|
|
2438
|
+
*/
|
|
2439
|
+
const OWN_PROVIDER_IDS = new Set([
|
|
2440
|
+
"kimi",
|
|
2441
|
+
"grok",
|
|
2442
|
+
"antigravity",
|
|
2443
|
+
"glm",
|
|
2444
|
+
"ark"
|
|
2445
|
+
]);
|
|
2055
2446
|
/** Dictionary namespace owned by this plugin. */
|
|
2056
2447
|
const NS = "settings.subscriptions";
|
|
2057
2448
|
/** Settings nav position: after the shipped sections, before plugin tooling. */
|
|
2058
2449
|
const SECTION_ORDER = 92;
|
|
2059
2450
|
/**
|
|
2451
|
+
* Composer-dock position: after the shell's own stats pills, which sit at 0.
|
|
2452
|
+
*
|
|
2453
|
+
* The shell owns that bar and registers `tok/s` and the cache-hit pill into it, so the
|
|
2454
|
+
* chip follows them rather than displacing them.
|
|
2455
|
+
*/
|
|
2456
|
+
const CHIP_ORDER = 10;
|
|
2457
|
+
/**
|
|
2060
2458
|
* Required services: `slots` carries the registration seat, `connection` the
|
|
2061
2459
|
* RPC caller, and `locale` the copy dictionaries.
|
|
2062
2460
|
*/
|
|
@@ -2091,6 +2489,16 @@ function apply(ctx) {
|
|
|
2091
2489
|
t
|
|
2092
2490
|
})
|
|
2093
2491
|
}, SubscriptionsSection));
|
|
2492
|
+
ctx.slots.inject("conversation.composer.dock", () => ctx.slots.register({
|
|
2493
|
+
name: "conversation.composer.dock",
|
|
2494
|
+
id: "subscription-usage",
|
|
2495
|
+
order: CHIP_ORDER,
|
|
2496
|
+
inject: () => ({
|
|
2497
|
+
call,
|
|
2498
|
+
t,
|
|
2499
|
+
isOwnProvider: (provider) => OWN_PROVIDER_IDS.has(provider)
|
|
2500
|
+
})
|
|
2501
|
+
}, UsageChip));
|
|
2094
2502
|
}
|
|
2095
2503
|
|
|
2096
2504
|
//#endregion
|