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.
@@ -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
- background: var(--dsw-alias-bg-layer-3, var(--dsw-alias-bg-layer-2));
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
- background: var(--dsw-alias-bg-layer-3, var(--dsw-alias-bg-layer-2));
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