dsh-subscriptions 0.1.2 → 0.1.4

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.
Files changed (68) hide show
  1. package/README.md +92 -10
  2. package/lib/auth/rpc.d.ts +3 -1
  3. package/lib/auth/rpc.js +10 -1
  4. package/lib/client/ExternalUsageCards.d.ts +11 -1
  5. package/lib/client/ExternalUsageCards.js +18 -3
  6. package/lib/client/ProviderAccountManager.js +34 -41
  7. package/lib/client/ProviderModelEditor.js +5 -9
  8. package/lib/client/SpeedSelect.js +8 -7
  9. package/lib/client/SubscriptionUsageBadge.d.ts +19 -2
  10. package/lib/client/SubscriptionUsageBadge.js +65 -9
  11. package/lib/client/SubscriptionsSection.d.ts +22 -0
  12. package/lib/client/SubscriptionsSection.js +128 -38
  13. package/lib/client/UsageBadgeDisplaySetting.js +6 -10
  14. package/lib/client/UsageMeter.js +11 -6
  15. package/lib/client/host-contract.d.ts +1 -1
  16. package/lib/client/host-contract.js +22 -0
  17. package/lib/client/locales.d.ts +60 -0
  18. package/lib/client/locales.js +62 -2
  19. package/lib/client/provider-settings-styles.d.ts +3 -0
  20. package/lib/client/provider-settings-styles.js +206 -0
  21. package/lib/client/reset-action.d.ts +14 -0
  22. package/lib/client/reset-action.js +15 -0
  23. package/lib/client/reset-credits-view.d.ts +53 -0
  24. package/lib/client/reset-credits-view.js +103 -0
  25. package/lib/client/usage-badge-preferences.d.ts +7 -2
  26. package/lib/client/usage-badge-preferences.js +3 -2
  27. package/lib/client/usage-pace.d.ts +14 -1
  28. package/lib/client/usage-pace.js +14 -1
  29. package/lib/client.js +1036 -334
  30. package/lib/client.js.map +1 -1
  31. package/lib/index.d.ts +9 -0
  32. package/lib/index.js +1120 -602
  33. package/lib/providers/account-preferences.js +3 -2
  34. package/lib/providers/catalog-store.d.ts +3 -2
  35. package/lib/providers/catalog-store.js +57 -26
  36. package/lib/providers/claude.d.ts +22 -3
  37. package/lib/providers/claude.js +113 -15
  38. package/lib/providers/codex.d.ts +15 -2
  39. package/lib/providers/codex.js +72 -4
  40. package/lib/providers/common.d.ts +25 -0
  41. package/lib/providers/common.js +22 -18
  42. package/lib/providers/copilot.d.ts +2 -1
  43. package/lib/providers/copilot.js +10 -3
  44. package/lib/providers/grok.d.ts +2 -2
  45. package/lib/providers/grok.js +2 -2
  46. package/lib/providers/minimax-usage.d.ts +22 -1
  47. package/lib/providers/minimax-usage.js +37 -7
  48. package/lib/providers/pool-health.d.ts +2 -0
  49. package/lib/providers/pool-health.js +9 -0
  50. package/lib/providers/pool-usage.d.ts +13 -1
  51. package/lib/providers/pool-usage.js +39 -7
  52. package/lib/providers/pool.d.ts +7 -5
  53. package/lib/providers/pool.js +89 -12
  54. package/lib/providers/rate-limit.d.ts +0 -14
  55. package/lib/providers/rate-limit.js +0 -26
  56. package/lib/providers/replay.d.ts +4 -0
  57. package/lib/providers/replay.js +50 -0
  58. package/lib/providers/reset-redemption.d.ts +51 -0
  59. package/lib/providers/reset-redemption.js +102 -0
  60. package/lib/translate/anthropic.d.ts +21 -2
  61. package/lib/translate/anthropic.js +35 -11
  62. package/lib/translate/antigravity.js +13 -6
  63. package/lib/translate/chat-completions.js +1 -1
  64. package/lib/translate/resolved.d.ts +21 -0
  65. package/lib/translate/resolved.js +63 -3
  66. package/lib/translate/responses.js +8 -6
  67. package/lib/translate/sse.js +8 -0
  68. package/package.json +1 -1
package/README.md CHANGED
@@ -68,13 +68,15 @@ and tool settings.
68
68
 
69
69
  ### Quota while you work
70
70
 
71
- The composer's stats row gains a **subscription usage** pill showing the
71
+ The composer's stats row gains a **provider usage** pill showing the
72
72
  used percentage and reset window for the provider of the session's current
73
73
  model. It shows at most one provider; switching to a non-subscription model
74
74
  keeps the most recent subscription selected in the mounted conversation
75
- view, or stays hidden if there is none.
75
+ view, or stays hidden if there is none. **Always show** instead rotates
76
+ through the providers that report usage every 10 seconds while the current
77
+ model has no quota source, so the pill never disappears.
76
78
 
77
- ![Subscription usage pill in the stats row](docs/images/usage-pill.png)
79
+ ![Provider usage pill in the stats row](docs/images/usage-pill.png)
78
80
 
79
81
  Click the pill to expand every provider and account that reports usage —
80
82
  the default account is starred, and the current provider is listed first.
@@ -83,7 +85,54 @@ neither do accounts without a usage window. Antigravity previews only the
83
85
  current model's windows; the other model windows stay in a closed
84
86
  disclosure.
85
87
 
86
- ![Subscription usage pill expanded](docs/images/usage-badge.png)
88
+ Quota bars include a dark elapsed-time cursor whenever the provider's
89
+ timing places the window in time, including at 0% used and for stale or old
90
+ readings. Compare the colored fill (quota used) with the cursor (time
91
+ elapsed); hover for the percentages and reset countdown. Standard coloring
92
+ turns the fill orange when a fresh reading leads elapsed time by 10 points,
93
+ and Relaxed uses 15; 90% used turns it red and takes priority. Windows
94
+ without known timing omit the cursor, and a stale or old reading keeps its
95
+ cursor without a pace warning. Choose Standard, Relaxed, or Remaining colors
96
+ in **Settings → Subscriptions**.
97
+
98
+ ![Usage dialog with elapsed-time cursors](docs/images/usage-badge.png)
99
+
100
+ #### Codex usage limit resets
101
+
102
+ The Codex account card shows banked full-reset credits and their expirations
103
+ in a closed disclosure. A failed optional lookup reports an error without
104
+ hiding ordinary quota windows. **Use…** selects the earliest-expiring available
105
+ credit and requires a fresh unscoped weekly reading of at least 80%, then an
106
+ explicit confirmation and acknowledgment. Confirmation expires after 60
107
+ seconds; the server rechecks usage and credit availability before submission.
108
+
109
+ One reset consumes one credit and resets weekly and 5-hour limits. This
110
+ interface cannot undo it. An unconfirmed outcome blocks further redemption
111
+ for that account in the current host process. Restarting clears that block,
112
+ but must not be used to bypass it: check credits and limits in Codex before
113
+ retrying, because the first submission may already have spent a credit.
114
+ The private endpoint is covered by offline fakes, not live-account checks.
115
+ See the [manual reset decision](.agents/notes/implemented/provider/2026-10-04-codex-manual-reset.md)
116
+ for the retry boundary and verification limits.
117
+
118
+ #### Claude usage limit resets
119
+
120
+ The Claude account card shows banked limit resets from Anthropic's
121
+ `cedar_ember` program, in the same closed disclosure Codex uses. It lists one
122
+ row per grant, not one per credit, and the summary counts remaining resets by
123
+ summing `resetsLeft` over grants that are neither expired nor paused. Each row
124
+ states whether that grant is available, not yet usable, paused, exhausted, or
125
+ expired, with its remaining count and when it lapses. That time is the grant's
126
+ expiry: it is not a window reset and does not move the elapsed-time cursor.
127
+
128
+ This is **display only**. `claimable` is shown as data on a text row, and the
129
+ page has no control that spends a Claude reset, because the response shape is
130
+ not an Anthropic contract. An ineligible account, a grant-less block, or a
131
+ rejected optional query leaves ordinary session and weekly windows untouched
132
+ and shows no list; a failed lookup shows a fixed sentence rather than the
133
+ provider's text. No live account was used to verify what Anthropic returns. See
134
+ [the evidence and limits](docs/claude-banked-resets.md) and the
135
+ [display decision](.agents/notes/implemented/provider/2026-10-08-claude-banked-reset-display.md).
87
136
 
88
137
  ## Install
89
138
 
@@ -171,6 +220,33 @@ usage.
171
220
  The plugin follows DSH network settings. Its former proxy configuration and
172
221
  UI have been retired; an old plugin proxy config file is ignored.
173
222
 
223
+ ## Claude prompt-cache TTL
224
+
225
+ Set `claudePromptCacheTtl: '5m'` (default) or `'1h'` in the plugin config and
226
+ restart DSH. This affects Claude only; other providers keep their own caching.
227
+ Five minutes omits the wire `ttl` field, preserving the existing request. One
228
+ hour adds `ttl: "1h"` to the tools+system prefix and up to three conversation
229
+ breakpoints. All marks share one TTL: Anthropic rejects a one-hour mark after
230
+ a five-minute mark. Requests classified with `purpose` as compaction or
231
+ session-title keep five minutes. Unclassified calls, including subagents and
232
+ calls from hosts that do not supply `purpose`, follow the configured TTL.
233
+ The TTL setting does not require a newer host capability or dependency.
234
+
235
+ Cache reads have the same API price for either TTL. One-hour writes are priced
236
+ at 2× base input instead of 1.25×. They may pay off with reusable prefixes and
237
+ 5–60-minute pauses; benefits depend on the workload and do not establish
238
+ subscription quota savings. Frequent reuse within five minutes may not offset
239
+ the higher write price. Claude Code requests one hour for its main subscription
240
+ conversation; see
241
+ [Claude Code's prompt-caching notes](https://code.claude.com/docs/en/prompt-caching#which-ttl-each-request-gets).
242
+
243
+ Changing TTL on a cached conversation may rewrite the prefix. Upstream testing
244
+ found that the first one-hour request after five-minute writes rewrote a
245
+ conversation-tail prefix, while a system-only breakpoint could hit. A
246
+ five-minute request can read a one-hour entry; a one-hour-marked read does not
247
+ upgrade an existing five-minute entry's lifetime. These observations are not
248
+ live-account verification of this fork.
249
+
174
250
  ## Usage coloring
175
251
 
176
252
  **Settings → Subscriptions → Usage coloring** offers one browser-local dropdown:
@@ -183,7 +259,7 @@ Every preset shows reported usage of 90% or more in red, before considering pace
183
259
 
184
260
  A vertical marker shows elapsed time only for an explicit valid start/end interval or a provider-verified fixed duration. A session/weekly label alone does not establish a fixed window; rolling or unknown intervals have no marker. Claude's recognized five-hour/seven-day buckets and Codex's reported quota durations supply fixed-window metadata. Unknown timing falls back to green below 90%, without a marker or yellow warning.
185
261
 
186
- Expired, failed-refresh or more-than-five-minute-old readings retain the last reported green/red color, without pace warnings or markers. Cached numbers remain visible, with an accessible stale label; refresh to confirm them. Invalid percentages show an unavailable state without a fabricated fill. Tooltips explain remaining quota, time progress and reset countdown where available. See the [decision record](.agents/notes/implemented/architecture/2026-10-01-usage-pace-colors.md) for rationale and verification limits.
262
+ The marker depends on the window's timing alone, not on the reading: it stays at 0% used, for an invalid percentage, and when a refresh failed or the reading is more than five minutes old. Such a reading is not set against the current time, so it gets no yellow pace warning and its tooltip omits the ahead/behind comparison. It retains the last reported green/red color with an accessible stale label; refresh to confirm it. Once the reset time has passed the window has no position and the marker disappears. Invalid percentages show an unavailable state without a fabricated fill. Tooltips explain remaining quota, time progress and reset countdown where available. See the [decision record](.agents/notes/implemented/architecture/2026-10-01-usage-pace-colors.md) and the [cursor note](.agents/notes/implemented/architecture/2026-10-06-usage-cursor-timing-only.md) for rationale and verification limits.
187
263
 
188
264
  ### MiniMax subscription usage
189
265
 
@@ -193,11 +269,17 @@ The plugin reuses those credential references; no separate subscription ref is
193
269
  required. Quota reads require subscription keys, not `sk-api-*` pay-as-you-go keys. The plugin reads the
194
270
  region's `/v1/token_plan/remains` endpoint and displays finite per-model quotas
195
271
  with API-provided start/end times. It does not add model routing or OAuth login.
196
- Unlimited, boosted-above-100% and unsupported quotas are omitted; if none remain,
197
- the card reports unavailable rather than inventing a percentage. Explicit
198
- remaining percentages take priority; older responses follow the official CLI's
199
- remaining-count compatibility convention. No live-account response was used in
200
- verification.
272
+ Rows are labelled with the model name, and the plugin shows the windows the API
273
+ returns for each model. MiniMax documents a five-hour and a weekly window for
274
+ non-video models and only a weekly one for video. Standard (non-video) models
275
+ on time-based plans report an explicit remaining percentage beside zero counts,
276
+ and that percentage is enough for a row. Unlimited, boosted-above-100% and
277
+ unsupported quotas are omitted (a status-3 row also covers a model the plan
278
+ does not include); if none remain, the card reports unavailable rather than
279
+ inventing a percentage. Explicit remaining percentages take priority; older
280
+ responses follow the official CLI's remaining-count compatibility convention. No
281
+ live-account response was used in verification, so a missing model may still
282
+ come from the account or the plan rather than from this plugin.
201
283
 
202
284
  ## Development
203
285
 
package/lib/auth/rpc.d.ts CHANGED
@@ -23,7 +23,7 @@ export declare const SUBSCRIPTIONS_AUTH_PREFIX = "subscriptions-auth.";
23
23
  * Every endpoint {@link dispatch} answers; each gets one exact Fetch route.
24
24
  * Kept in one place so the route table and the switch cannot drift apart.
25
25
  */
26
- export declare const SUBSCRIPTIONS_AUTH_ENDPOINTS: readonly ["providerSettings", "setProviderSettings", "status", "login", "manual", "cancel", "logout", "setDefault", "usage", "externalStatus", "externalUsage", "cursorStatus", "cursorLogin", "cursorCancel", "cursorLogout", "cursorUsage", "cursorModels", "cursorSettings", "cursorSetSettings", "image", "video", "speed", "setSpeed", "modelDefaults", "setModelDefault"];
26
+ export declare const SUBSCRIPTIONS_AUTH_ENDPOINTS: readonly ["providerSettings", "setProviderSettings", "status", "login", "manual", "cancel", "logout", "setDefault", "usage", "externalStatus", "externalUsage", "cursorStatus", "cursorLogin", "cursorCancel", "cursorLogout", "cursorUsage", "cursorModels", "cursorSettings", "cursorSetSettings", "image", "video", "prepareReset", "consumeReset", "speed", "setSpeed", "modelDefaults", "setModelDefault"];
27
27
  /** Decoded image bytes returned by the `image` endpoint. */
28
28
  export interface ImageBytesResult {
29
29
  mediaType: string;
@@ -121,6 +121,8 @@ export interface ModelDefaultsController {
121
121
  }
122
122
  /** Provider-agnostic auth operations the RPC handler delegates to. */
123
123
  export interface AuthController {
124
+ prepareReset?(account: string, signal: AbortSignal): Promise<unknown>;
125
+ consumeReset?(account: string, ticket: string, signal: AbortSignal): Promise<void>;
124
126
  /** Current status of one provider. */
125
127
  status(provider: ProviderId): Promise<ProviderStatus>;
126
128
  /**
package/lib/auth/rpc.js CHANGED
@@ -25,7 +25,7 @@ export const SUBSCRIPTIONS_AUTH_ENDPOINTS = [
25
25
  'externalStatus', 'externalUsage',
26
26
  'cursorStatus', 'cursorLogin', 'cursorCancel', 'cursorLogout', 'cursorUsage', 'cursorModels',
27
27
  'cursorSettings', 'cursorSetSettings',
28
- 'image', 'video',
28
+ 'image', 'video', 'prepareReset', 'consumeReset',
29
29
  'speed', 'setSpeed',
30
30
  'modelDefaults', 'setModelDefault',
31
31
  ];
@@ -322,6 +322,15 @@ async function dispatch(controller, speed, modelDefaults, endpoint, payload, sig
322
322
  throw new BadRequest('payload must be an object');
323
323
  await cursor.setSettings(payload.settings);
324
324
  return ok({ ok: true });
325
+ case 'prepareReset':
326
+ if (!controller.prepareReset)
327
+ throw new BadRequest('Reset redemption unavailable');
328
+ return ok(await controller.prepareReset(readString(payload, 'account'), signal));
329
+ case 'consumeReset':
330
+ if (!controller.consumeReset)
331
+ throw new BadRequest('Reset redemption unavailable');
332
+ await controller.consumeReset(readString(payload, 'account'), readString(payload, 'ticket'), signal);
333
+ return ok({ ok: true });
325
334
  case 'image':
326
335
  return ok(await controller.readImage(readImageRef(payload), signal));
327
336
  case 'video':
@@ -1,6 +1,16 @@
1
1
  import type { ConnectionHandle } from '@deepseek-ai/dsh-api-remotes/client';
2
- import type { SubscriptionsSectionInjected } from './SubscriptionsSection.js';
2
+ import type { SubscriptionsSectionInjected, UsageWindow } from './SubscriptionsSection.js';
3
3
  type Translate = SubscriptionsSectionInjected['t'];
4
+ /**
5
+ * Row label of one window on an external usage card. A session or weekly
6
+ * window appends the model it belongs to when the provider names one (MiniMax
7
+ * reports a window per model), so two models' weekly rows stay distinguishable.
8
+ * The monthly pool is a scope on an `other` window, not a model.
9
+ * @param t - section translate.
10
+ * @param window - the reported window.
11
+ * @returns the localized label, e.g. "Weekly · general".
12
+ */
13
+ export declare function externalUsageWindowLabel(t: Translate, window: UsageWindow): string;
4
14
  /** Quota cards for API-key providers already available through DSH itself. */
5
15
  export declare function ExternalUsageCards({ rpc, t }: {
6
16
  rpc: ConnectionHandle['rpc'];
@@ -14,6 +14,23 @@ const SOURCES = [
14
14
  function errorText(error) {
15
15
  return error instanceof Error ? error.message : String(error);
16
16
  }
17
+ /**
18
+ * Row label of one window on an external usage card. A session or weekly
19
+ * window appends the model it belongs to when the provider names one (MiniMax
20
+ * reports a window per model), so two models' weekly rows stay distinguishable.
21
+ * The monthly pool is a scope on an `other` window, not a model.
22
+ * @param t - section translate.
23
+ * @param window - the reported window.
24
+ * @returns the localized label, e.g. "Weekly · general".
25
+ */
26
+ export function externalUsageWindowLabel(t, window) {
27
+ const named = window.scope !== undefined && window.scope !== '';
28
+ if (window.kind === 'other') {
29
+ return window.scope === 'Monthly' ? t('usageMonthly') : named ? window.scope : t('usageWindow');
30
+ }
31
+ const base = window.kind === 'session' ? t('usageSession') : t('usageWeekly');
32
+ return named ? `${base} · ${window.scope}` : base;
33
+ }
17
34
  /** Quota cards for API-key providers already available through DSH itself. */
18
35
  export function ExternalUsageCards({ rpc, t }) {
19
36
  const [status, setStatus] = useState();
@@ -62,9 +79,7 @@ export function ExternalUsageCards({ rpc, t }) {
62
79
  ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-label-dimmed)' } }), _jsx("span", { style: styles.name, children: name })] }), !(status === undefined && statusError !== undefined) && _jsx("p", { style: styles.status, children: status === undefined ? t('checking')
63
80
  : configured ? t('externalUsageConnected') : t('externalUsageNotConfigured') }), statusError !== undefined && _jsx("p", { style: styles.error, children: t('externalUsageUnavailable', { message: statusError }) }), status !== undefined && !configured && _jsx("p", { style: styles.status, children: t('externalUsageConfigure', { ref }) }), configured && _jsxs("div", { style: styles.account, children: [_jsxs("div", { style: styles.accountHeader, children: [_jsx("span", { style: styles.defaultStar, title: t('defaultBadge'), children: "\u2605" }), _jsx("span", { style: styles.accountName, children: t('externalUsageApiKey') })] }), _jsxs("div", { style: styles.usage, children: [_jsxs("div", { style: styles.usageHeader, children: [_jsx("span", { style: styles.usageTitle, children: t('usageTitle') }), snapshot?.plan !== undefined && _jsx("span", { style: styles.usagePlan, children: t('usagePlan', { plan: snapshot.plan }) }), _jsx("button", { type: "button", style: styles.usageRefresh, disabled: loading[id] === true, onClick: () => { void refresh(id); }, children: t('usageRefresh') })] }), loading[id] === true && snapshot === undefined && _jsx("p", { style: styles.status, children: t('usageLoading') }), errors[id] !== undefined && _jsx("p", { style: styles.error, children: t('usageError', { message: errors[id] }) }), snapshot?.windows?.length === 0 && _jsx("p", { style: styles.status, children: t('usageEmpty') }), snapshot?.windows?.map((window, index) => {
64
81
  const percent = displayUsedPercent(window.usedPercent);
65
- const label = window.kind === 'session' ? t('usageSession')
66
- : window.kind === 'weekly' ? t('usageWeekly')
67
- : window.scope === 'Monthly' ? t('usageMonthly') : window.scope ?? t('usageWindow');
82
+ const label = externalUsageWindowLabel(t, window);
68
83
  return _jsxs("div", { style: styles.usageRow, children: [_jsxs("div", { style: styles.usageMeta, children: [_jsx("span", { children: label }), _jsxs("span", { children: [percent === undefined ? t('usageMeterInvalid') : `${percent}%`, window.resetsAt === undefined ? ''
69
84
  : ` · ${t('usageResets', { date: new Date(window.resetsAt).toLocaleString() })}`] })] }), _jsx(UsageMeter, { window: window, t: t, observedAt: observedAt[id], stale: errors[id] !== undefined || snapshot?.stale === true })] }, index);
70
85
  })] })] }), !configured && _jsx("div", { style: styles.actions, children: _jsx("button", { type: "button", style: styles.button, onClick: () => { void refreshStatus(); }, children: t('usageRefresh') }) })] }, id);
@@ -3,13 +3,8 @@ import { useEffect, useId, useRef, useState } from 'react';
3
3
  import { callSubscriptionsAuth } from './subscriptions-rpc.js';
4
4
  import { accountModelRows, accountPoolSelection, mergeAccountChanges, mergeLatestAccounts } from './account-preferences.js';
5
5
  import { ProviderModelEditor } from './ProviderModelEditor.js';
6
- const border = '1px solid var(--dsw-alias-border-l2)';
7
- const control = {
8
- font: 'inherit', color: 'inherit', background: 'var(--dsw-alias-bg-layer-1)',
9
- border, borderRadius: 8, padding: '7px 12px', minWidth: 0,
10
- };
11
- const button = { ...control, cursor: 'pointer' };
12
- const hint = { margin: 0, fontSize: 12, lineHeight: 1.6, color: 'var(--dsw-alias-label-tertiary)' };
6
+ import { providerSettingsCss } from './provider-settings-styles.js';
7
+ const border = '0.5px solid var(--dsw-alias-border-l2)';
13
8
  const stack = { display: 'grid', gap: 12, minWidth: 0 };
14
9
  const actions = { display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'center' };
15
10
  /** Native top-layer dialog provides keyboard containment, Escape and focus restoration. */
@@ -110,38 +105,36 @@ export function ProviderAccountManager({ provider, name, rpc, t, onClose }) {
110
105
  setSaving(false);
111
106
  }
112
107
  }
113
- return _jsx("dialog", { ref: dialog, "aria-labelledby": title, "aria-describedby": description, onCancel: event => { if (saveLock.current)
114
- event.preventDefault(); }, onClose: onClose, style: { width: 620, maxWidth: 'calc(100vw - 32px)', maxHeight: 'calc(100dvh - 32px)', boxSizing: 'border-box',
115
- padding: 0, border, borderRadius: 16, color: 'var(--dsw-alias-label-primary)',
116
- background: 'var(--dsw-alias-bg-layer-1)', boxShadow: '0 20px 70px #0004', overflow: 'auto' }, children: _jsxs("div", { style: { ...stack, padding: 20 }, children: [_jsxs("header", { style: { ...actions, justifyContent: 'space-between' }, children: [_jsx("h2", { id: title, style: { margin: 0, fontSize: 18 }, children: t('accountsTitle', { provider: name }) }), _jsx("button", { type: "button", autoFocus: true, style: button, disabled: saving, onClick: onClose, children: t('imageClose') })] }), _jsx("p", { id: description, style: hint, children: t(provider === 'cursor-subscription' ? 'cursorAccountsHint' : 'accountsHint') }), error && _jsx("p", { role: "alert", style: { ...hint, color: 'var(--dsw-alias-state-error-primary)' }, children: error }), loading && _jsx("p", { role: "status", style: hint, children: t('accountsLoading') }), !loading && !catalog && _jsx("button", { type: "button", style: button, onClick: () => setAttempt(value => value + 1), children: t('modelDefaultsRetry') }), catalog && _jsxs("fieldset", { disabled: saving || loading, style: { ...stack, border: 0, margin: 0, padding: 0 }, children: [catalog.accounts.length === 0 && _jsx("p", { style: hint, children: t(provider === 'cursor-subscription' ? 'cursorManageSignIn' : 'accountsEmpty') }), provider === 'cursor-subscription' && catalog.accounts.length > 0 &&
117
- _jsxs("fieldset", { style: { border, borderRadius: 12, padding: 14 }, children: [_jsx("legend", { style: { padding: '0 6px', fontWeight: 600, fontSize: 14 }, children: name }), _jsx("p", { style: hint, children: t('cursorConnected') })] }), provider !== 'cursor-subscription' && catalog.accounts.map(account => {
118
- const preferences = changes[account.key] ?? catalog.settings.accounts?.[account.key] ?? {};
119
- const selected = accountPoolSelection(preferences, account.models);
120
- const models = accountModelRows(account, preferences);
121
- return _jsxs("fieldset", { style: { ...stack, border, borderRadius: 12, padding: 14, margin: 0 }, children: [_jsx("legend", { style: { padding: '0 6px', fontWeight: 600, fontSize: 14, overflowWrap: 'anywhere', maxWidth: '100%' }, children: account.label }), account.unavailable && _jsx("p", { style: hint, children: t('accountsCatalogUnavailable') }), _jsxs("label", { style: { ...stack, gap: 6 }, children: [_jsx("span", { style: { fontSize: 12 }, children: t('accountsAlias') }), _jsx("input", { style: control, value: preferences.alias ?? '', placeholder: account.label, onChange: event => {
122
- const next = { ...preferences };
123
- if (event.target.value.trim())
124
- next.alias = event.target.value;
125
- else
126
- delete next.alias;
127
- edit(account.key, next);
128
- } })] }), _jsxs("label", { children: [_jsx("input", { type: "checkbox", checked: preferences.poolEnabled !== false, onChange: event => edit(account.key, { ...preferences, poolEnabled: event.target.checked }) }), " ", t('accountsPoolEnabled')] }), _jsxs("label", { children: [_jsx("input", { type: "checkbox", checked: preferences.independentEntry === true, onChange: event => edit(account.key, { ...preferences, independentEntry: event.target.checked }) }), " ", t('accountsIndependent')] }), _jsx("p", { style: hint, children: t('accountsIndependentHint') }), _jsxs("details", { children: [_jsxs("summary", { style: { cursor: 'pointer', fontSize: 13 }, children: [t('accountsModels'), " \u00B7 ", preferences.poolModels === undefined
129
- ? t('accountsModelsAll') : t('accountsModelsCount', { count: preferences.poolModels.length })] }), _jsxs("div", { style: { ...stack, marginTop: 12 }, children: [_jsx("p", { style: hint, children: t('accountsModelsHint') }), _jsxs("label", { children: [_jsx("input", { type: "checkbox", checked: preferences.poolModels === undefined, onChange: event => {
130
- const next = { ...preferences };
131
- if (event.target.checked)
132
- delete next.poolModels;
133
- else
134
- next.poolModels = account.models.map(model => model.id);
135
- edit(account.key, next);
136
- } }), " ", t('accountsModelsAutomatic')] }), _jsxs("div", { style: actions, children: [_jsx("button", { type: "button", style: button, onClick: () => edit(account.key, { ...preferences, poolModels: models.map(model => model.id) }), children: t('modelsSelectAll') }), _jsx("button", { type: "button", style: button, onClick: () => edit(account.key, { ...preferences, poolModels: [] }), children: t('modelsSelectNone') })] }), _jsxs("div", { style: { ...stack, gap: 8, maxHeight: 240, overflowY: 'auto' }, children: [models.map(model => _jsxs("label", { style: { fontSize: 13, overflowWrap: 'anywhere' }, children: [_jsx("input", { type: "checkbox", checked: selected.has(model.id), onChange: event => {
137
- const next = new Set(selected);
138
- if (event.target.checked)
139
- next.add(model.id);
140
- else
141
- next.delete(model.id);
142
- edit(account.key, { ...preferences, poolModels: [...next] });
143
- } }), " ", model.name, model.unavailable && ` (${t('modelsUnavailable')})`] }, model.id)), models.length === 0 && _jsx("p", { style: hint, children: t('accountsNoModels') })] })] })] })] }, account.key);
144
- })] }), (provider !== 'cursor-subscription' || (catalog?.accounts.length ?? 0) > 0) &&
145
- _jsx(ProviderModelEditor, { ref: editor, provider: provider, rpc: rpc, t: t, disabled: saving, onDirtyChange: setModelsDirty }), _jsxs("footer", { style: { ...stack, gap: 8, borderTop: border, padding: '14px 0 0',
146
- position: 'sticky', bottom: 0, background: 'var(--dsw-alias-bg-layer-1)' }, children: [saveError && _jsx("p", { role: "alert", style: { ...hint, color: 'var(--dsw-alias-state-error-primary)' }, children: saveError }), _jsxs("div", { style: { ...actions, justifyContent: 'flex-end' }, children: [_jsx("button", { type: "button", style: button, disabled: saving, onClick: onClose, children: t('cancel') }), _jsx("button", { type: "button", style: { ...button, fontWeight: 600 }, disabled: loading || saving || (!Object.keys(changes).length && !modelsDirty), onClick: () => { void save(); }, children: saving ? t('modelDefaultsSaving') : t('modelsSave') })] })] })] }) });
108
+ return _jsxs("dialog", { className: "dsh-subscription-manager", ref: dialog, "aria-labelledby": title, "aria-describedby": description, onCancel: event => { if (saveLock.current)
109
+ event.preventDefault(); }, onClose: onClose, style: { width: 620, maxWidth: 'calc(100vw - 32px)', maxHeight: 'calc(100dvh - 32px)', overflow: 'auto' }, children: [_jsx("style", { children: providerSettingsCss }), _jsxs("div", { style: { ...stack, padding: 20 }, children: [_jsxs("header", { style: { ...actions, justifyContent: 'space-between' }, children: [_jsx("h2", { id: title, style: { margin: 0 }, children: t('accountsTitle', { provider: name }) }), _jsx("button", { type: "button", autoFocus: true, disabled: saving, onClick: onClose, children: t('imageClose') })] }), _jsx("p", { id: description, className: "dsh-subscription-hint", children: t(provider === 'cursor-subscription' ? 'cursorAccountsHint' : 'accountsHint') }), error && _jsx("p", { role: "alert", children: error }), loading && _jsx("p", { role: "status", className: "dsh-subscription-hint", children: t('accountsLoading') }), !loading && !catalog && _jsx("button", { type: "button", onClick: () => setAttempt(value => value + 1), children: t('modelDefaultsRetry') }), catalog && _jsxs("fieldset", { disabled: saving || loading, style: { ...stack, border: 0, margin: 0, padding: 0 }, children: [catalog.accounts.length === 0 && _jsx("p", { className: "dsh-subscription-hint", children: t(provider === 'cursor-subscription' ? 'cursorManageSignIn' : 'accountsEmpty') }), provider === 'cursor-subscription' && catalog.accounts.length > 0 &&
110
+ _jsxs("fieldset", { style: { border, borderRadius: 'var(--dsw-radius-lg, 12px)', padding: 14 }, children: [_jsx("legend", { style: { padding: '0 6px' }, children: name }), _jsx("p", { className: "dsh-subscription-hint", children: t('cursorConnected') })] }), provider !== 'cursor-subscription' && catalog.accounts.map(account => {
111
+ const preferences = changes[account.key] ?? catalog.settings.accounts?.[account.key] ?? {};
112
+ const selected = accountPoolSelection(preferences, account.models);
113
+ const models = accountModelRows(account, preferences);
114
+ return _jsxs("fieldset", { style: { ...stack, border, borderRadius: 'var(--dsw-radius-lg, 12px)', padding: 14, margin: 0 }, children: [_jsx("legend", { style: { padding: '0 6px', overflowWrap: 'anywhere', maxWidth: '100%' }, children: account.label }), account.unavailable && _jsx("p", { className: "dsh-subscription-hint", children: t('accountsCatalogUnavailable') }), _jsxs("label", { style: { ...stack, gap: 6 }, children: [_jsx("span", { className: "dsh-subscription-label", children: t('accountsAlias') }), _jsx("input", { value: preferences.alias ?? '', placeholder: account.label, onChange: event => {
115
+ const next = { ...preferences };
116
+ if (event.target.value.trim())
117
+ next.alias = event.target.value;
118
+ else
119
+ delete next.alias;
120
+ edit(account.key, next);
121
+ } })] }), _jsxs("label", { children: [_jsx("input", { type: "checkbox", checked: preferences.poolEnabled !== false, onChange: event => edit(account.key, { ...preferences, poolEnabled: event.target.checked }) }), " ", t('accountsPoolEnabled')] }), _jsxs("label", { children: [_jsx("input", { type: "checkbox", checked: preferences.independentEntry === true, onChange: event => edit(account.key, { ...preferences, independentEntry: event.target.checked }) }), " ", t('accountsIndependent')] }), _jsx("p", { className: "dsh-subscription-hint", children: t('accountsIndependentHint') }), _jsxs("details", { children: [_jsxs("summary", { style: { cursor: 'pointer', fontSize: 13 }, children: [t('accountsModels'), " \u00B7 ", preferences.poolModels === undefined
122
+ ? t('accountsModelsAll') : t('accountsModelsCount', { count: preferences.poolModels.length })] }), _jsxs("div", { style: { ...stack, marginTop: 12 }, children: [_jsx("p", { className: "dsh-subscription-hint", children: t('accountsModelsHint') }), _jsxs("label", { children: [_jsx("input", { type: "checkbox", checked: preferences.poolModels === undefined, onChange: event => {
123
+ const next = { ...preferences };
124
+ if (event.target.checked)
125
+ delete next.poolModels;
126
+ else
127
+ next.poolModels = account.models.map(model => model.id);
128
+ edit(account.key, next);
129
+ } }), " ", t('accountsModelsAutomatic')] }), _jsxs("div", { style: actions, children: [_jsx("button", { type: "button", onClick: () => edit(account.key, { ...preferences, poolModels: models.map(model => model.id) }), children: t('modelsSelectAll') }), _jsx("button", { type: "button", onClick: () => edit(account.key, { ...preferences, poolModels: [] }), children: t('modelsSelectNone') })] }), _jsxs("div", { style: { ...stack, gap: 8, maxHeight: 240, overflowY: 'auto' }, children: [models.map(model => _jsxs("label", { style: { overflowWrap: 'anywhere' }, children: [_jsx("input", { type: "checkbox", checked: selected.has(model.id), onChange: event => {
130
+ const next = new Set(selected);
131
+ if (event.target.checked)
132
+ next.add(model.id);
133
+ else
134
+ next.delete(model.id);
135
+ edit(account.key, { ...preferences, poolModels: [...next] });
136
+ } }), " ", model.name, model.unavailable && ` (${t('modelsUnavailable')})`] }, model.id)), models.length === 0 && _jsx("p", { className: "dsh-subscription-hint", children: t('accountsNoModels') })] })] })] })] }, account.key);
137
+ })] }), (provider !== 'cursor-subscription' || (catalog?.accounts.length ?? 0) > 0) &&
138
+ _jsx(ProviderModelEditor, { ref: editor, provider: provider, rpc: rpc, t: t, disabled: saving, onDirtyChange: setModelsDirty }), _jsxs("footer", { style: { ...stack, gap: 8, borderTop: border, padding: '14px 0 8px',
139
+ position: 'sticky', bottom: 0, background: 'var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-layer-1))' }, children: [saveError && _jsx("p", { role: "alert", children: saveError }), _jsxs("div", { style: { ...actions, justifyContent: 'flex-end' }, children: [_jsx("button", { type: "button", disabled: saving, onClick: onClose, children: t('cancel') }), _jsx("button", { type: "button", className: "dsh-subscription-primary", disabled: loading || saving || (!Object.keys(changes).length && !modelsDirty), onClick: () => { void save(); }, children: saving ? t('modelDefaultsSaving') : t('modelsSave') })] })] })] })] });
147
140
  }
@@ -1,11 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
3
3
  import { callSubscriptionsAuth } from './subscriptions-rpc.js';
4
- const border = '1px solid var(--dsw-alias-border-l2, #ddd)';
5
- const control = {
6
- font: 'inherit', color: 'inherit', background: 'transparent', border,
7
- borderRadius: 8, padding: '6px 10px', minWidth: 0,
8
- };
4
+ const border = '0.5px solid var(--dsw-alias-border-l2, #ddd)';
9
5
  const actions = { display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'center' };
10
6
  /**
11
7
  * Embedded section of the account manager dialog. It owns the model draft
@@ -83,25 +79,25 @@ export const ProviderModelEditor = forwardRef(function ProviderModelEditor({ pro
83
79
  const missing = (draft.visibleModels ?? []).filter(id => !known.has(id)).map(id => ({ id, name: id }));
84
80
  const models = [...allModels, ...missing].filter(model => `${model.name} ${model.id}`.toLowerCase().includes(query.trim().toLowerCase()));
85
81
  const selected = new Set(draft.visibleModels ?? allModels.map(model => model.id));
86
- return _jsxs("div", { style: { borderTop: border, marginTop: 12, paddingTop: 12 }, children: [_jsx("h3", { style: { margin: 0, fontSize: 15 }, children: t('modelsEdit') }), _jsxs("div", { style: { display: 'grid', gap: 12, marginTop: 12 }, children: [_jsx("p", { style: { margin: 0 }, children: t(provider === 'cursor-subscription' ? 'cursorManageHint' : 'modelsHint') }), error && _jsx("p", { role: "alert", style: { margin: 0, color: 'var(--dsw-alias-state-error-primary, #b42318)' }, children: error }), _jsxs("div", { style: actions, children: [_jsx("button", { type: "button", style: control, disabled: busy || disabled || dirty, onClick: () => { void load(true); }, children: t('usageRefresh') }), busy && _jsx("span", { role: "status", children: t('modelDefaultsLoading') })] }), catalog && _jsxs("fieldset", { disabled: busy || disabled, style: { border: 0, padding: 0, margin: 0, minWidth: 0, display: 'grid', gap: 12 }, children: [_jsxs("label", { children: [_jsx("input", { type: "checkbox", checked: draft.visibleModels === undefined, onChange: event => {
82
+ return _jsxs("div", { style: { borderTop: border, marginTop: 12, paddingTop: 12 }, children: [_jsx("h3", { style: { margin: 0 }, children: t('modelsEdit') }), _jsxs("div", { style: { display: 'grid', gap: 12, marginTop: 12 }, children: [_jsx("p", { className: "dsh-subscription-hint", children: t(provider === 'cursor-subscription' ? 'cursorManageHint' : 'modelsHint') }), error && _jsx("p", { role: "alert", children: error }), _jsxs("div", { style: actions, children: [_jsx("button", { type: "button", className: "dsh-subscription-quiet", disabled: busy || disabled || dirty, onClick: () => { void load(true); }, children: t('usageRefresh') }), busy && _jsx("span", { role: "status", children: t('modelDefaultsLoading') })] }), catalog && _jsxs("fieldset", { disabled: busy || disabled, style: { border: 0, padding: 0, margin: 0, minWidth: 0, display: 'grid', gap: 12 }, children: [_jsxs("label", { children: [_jsx("input", { type: "checkbox", checked: draft.visibleModels === undefined, onChange: event => {
87
83
  const next = { ...draft };
88
84
  if (event.target.checked)
89
85
  delete next.visibleModels;
90
86
  else
91
87
  next.visibleModels = allModels.map(model => model.id);
92
88
  edit(next);
93
- } }), " ", t('modelsAutomatic')] }), _jsxs("div", { style: actions, children: [_jsx("input", { style: { ...control, flex: '1 1 180px' }, value: query, onChange: event => setQuery(event.target.value), placeholder: t('modelDefaultsFilterPlaceholder'), "aria-label": t('modelDefaultsFilterPlaceholder') }), _jsx("button", { type: "button", style: control, onClick: () => edit({ ...draft, visibleModels: allModels.map(model => model.id) }), children: t('modelsSelectAll') }), _jsx("button", { type: "button", style: control, onClick: () => edit({ ...draft, visibleModels: [] }), children: t('modelsSelectNone') })] }), _jsxs("div", { style: { maxHeight: 360, overflowY: 'auto', display: 'grid', gap: 8 }, children: [models.map(model => _jsxs("div", { style: { borderBottom: border, padding: '8px 2px', display: 'grid', gap: 8 }, children: [_jsxs("label", { style: { overflowWrap: 'anywhere' }, children: [_jsx("input", { type: "checkbox", checked: selected.has(model.id), onChange: event => {
89
+ } }), " ", t('modelsAutomatic')] }), _jsxs("div", { style: actions, children: [_jsx("input", { style: { flex: '1 1 180px' }, value: query, onChange: event => setQuery(event.target.value), placeholder: t('modelDefaultsFilterPlaceholder'), "aria-label": t('modelDefaultsFilterPlaceholder') }), _jsx("button", { type: "button", onClick: () => edit({ ...draft, visibleModels: allModels.map(model => model.id) }), children: t('modelsSelectAll') }), _jsx("button", { type: "button", onClick: () => edit({ ...draft, visibleModels: [] }), children: t('modelsSelectNone') })] }), _jsxs("div", { style: { maxHeight: 360, overflowY: 'auto', display: 'grid', gap: 8 }, children: [models.map(model => _jsxs("div", { style: { borderBottom: border, padding: '8px 2px', display: 'grid', gap: 8 }, children: [_jsxs("label", { style: { overflowWrap: 'anywhere' }, children: [_jsx("input", { type: "checkbox", checked: selected.has(model.id), onChange: event => {
94
90
  const next = new Set(selected);
95
91
  if (event.target.checked)
96
92
  next.add(model.id);
97
93
  else
98
94
  next.delete(model.id);
99
95
  edit({ ...draft, visibleModels: [...next] });
100
- } }), " ", model.name, !known.has(model.id) && ` (${t('modelsUnavailable')})`] }), _jsxs("div", { style: actions, children: [(model.efforts?.length ?? 0) > 0 && _jsxs("label", { style: actions, children: [t('modelDefaultsTitle'), _jsxs("select", { style: control, "aria-label": `${model.name} ${t('modelDefaultsTitle')}`, value: efforts[model.id] ?? '', onChange: event => {
96
+ } }), " ", model.name, !known.has(model.id) && ` (${t('modelsUnavailable')})`] }), _jsxs("div", { style: actions, children: [(model.efforts?.length ?? 0) > 0 && _jsxs("label", { style: actions, children: [t('modelDefaultsTitle'), _jsxs("select", { "aria-label": `${model.name} ${t('modelDefaultsTitle')}`, value: efforts[model.id] ?? '', onChange: event => {
101
97
  setEfforts({ ...efforts, [model.id]: event.target.value });
102
98
  setDirty(true);
103
99
  }, children: [_jsx("option", { value: "", children: t('modelDefaultsFollowProvider') }), model.configured && !model.efforts?.some(effort => effort.id === model.configured) &&
104
- _jsxs("option", { value: model.configured, disabled: true, children: [model.configured, " (", t('modelsUnavailable'), ")"] }), model.efforts?.map(effort => _jsx("option", { value: effort.id, children: effort.name }, effort.id))] })] }), provider === 'codex' && model.maxContextWindow !== undefined && _jsxs("div", { style: actions, children: [_jsxs("label", { style: actions, children: [t('modelsContext'), _jsx("input", { style: { ...control, width: 140 }, inputMode: "numeric", value: contexts[model.id] ?? '', "aria-label": `${model.name} ${t('modelsContext')}`, placeholder: String(model.defaultContextWindow), onChange: event => { setContexts({ ...contexts, [model.id]: event.target.value }); setDirty(true); } })] }), _jsx("small", { children: t('modelsContextBounds', { default: model.defaultContextWindow, max: model.maxContextWindow }) })] })] })] }, model.id)), models.length === 0 && _jsx("span", { children: t('modelDefaultsFilterEmpty', { query }) })] }), provider === 'codex' && _jsx("small", { children: t('modelsContextHint') }), catalog.tools.length > 0 && _jsxs("div", { style: { display: 'grid', gap: 8 }, children: [_jsx("strong", { children: t('modelsTools') }), _jsx("small", { children: t('modelsToolsHint') }), catalog.tools.map(tool => _jsxs("label", { children: [_jsx("input", { type: "checkbox", checked: draft.tools?.[tool] !== false, onChange: event => edit({
100
+ _jsxs("option", { value: model.configured, disabled: true, children: [model.configured, " (", t('modelsUnavailable'), ")"] }), model.efforts?.map(effort => _jsx("option", { value: effort.id, children: effort.name }, effort.id))] })] }), provider === 'codex' && model.maxContextWindow !== undefined && _jsxs("div", { style: actions, children: [_jsxs("label", { style: actions, children: [t('modelsContext'), _jsx("input", { style: { width: 140 }, inputMode: "numeric", value: contexts[model.id] ?? '', "aria-label": `${model.name} ${t('modelsContext')}`, placeholder: String(model.defaultContextWindow), onChange: event => { setContexts({ ...contexts, [model.id]: event.target.value }); setDirty(true); } })] }), _jsx("small", { children: t('modelsContextBounds', { default: model.defaultContextWindow, max: model.maxContextWindow }) })] })] })] }, model.id)), models.length === 0 && _jsx("span", { children: t('modelDefaultsFilterEmpty', { query }) })] }), provider === 'codex' && _jsx("small", { children: t('modelsContextHint') }), catalog.tools.length > 0 && _jsxs("div", { style: { display: 'grid', gap: 8 }, children: [_jsx("strong", { children: t('modelsTools') }), _jsx("small", { children: t('modelsToolsHint') }), catalog.tools.map(tool => _jsxs("label", { children: [_jsx("input", { type: "checkbox", checked: draft.tools?.[tool] !== false, onChange: event => edit({
105
101
  ...draft, tools: { ...draft.tools, [tool]: event.target.checked },
106
102
  }) }), " ", t(tool === 'image_generate' ? 'modelsImage' : tool === 'video_generate' ? 'modelsVideo' : tool === 'web_search' ? 'modelsWebSearch' : 'modelsSearch')] }, tool))] })] })] })] });
107
103
  });
@@ -14,6 +14,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
14
14
  */
15
15
  import { useEffect, useRef, useState } from 'react';
16
16
  import { callSubscriptionsAuth } from './SubscriptionsSection.js';
17
+ import { subscriptionChromeCss } from './provider-settings-styles.js';
17
18
  import { en } from './locales.js';
18
19
  /**
19
20
  * The `loadSpeed` half of the inject face: the plugin's own speed state plus
@@ -136,12 +137,12 @@ export function SpeedSelect({ loadSpeed, setSpeed, t }) {
136
137
  const tierName = (tier) => translate(tier === 'fast' ? 'speedFast' : 'speedStandard');
137
138
  const tierDescription = (tier) => translate(tier === 'fast' ? 'speedFastDescription' : 'speedStandardDescription');
138
139
  const triggerLabel = `${translate('speed')} · ${tierName(state.tier)}`;
139
- return (_jsxs("div", { ref: rootRef, style: styles.root, onKeyDown: (event) => {
140
+ return (_jsxs("div", { ref: rootRef, className: "dsh-subscription-speed", style: styles.root, onKeyDown: (event) => {
140
141
  if (event.key === 'Escape' && open) {
141
142
  event.preventDefault();
142
143
  setOpen(false);
143
144
  }
144
- }, children: [open && (_jsx("div", { style: styles.menu, role: "menu", "aria-label": translate('speed'), children: TIERS.map(tier => (_jsxs("button", { type: "button", role: "menuitemradio", "aria-checked": tier === state.tier, style: styles.item, disabled: busy, onClick: () => { choose(tier); }, children: [_jsx("span", { style: styles.itemCheck, children: tier === state.tier ? '✓' : '' }), _jsxs("span", { style: styles.itemText, children: [_jsx("span", { style: styles.itemName, children: tierName(tier) }), _jsx("span", { style: styles.itemDescription, children: tierDescription(tier) })] })] }, tier))) })), _jsx("button", { type: "button", style: styles.trigger, "aria-haspopup": "menu", "aria-expanded": open, title: triggerLabel, disabled: busy, onClick: () => {
145
+ }, children: [_jsx("style", { children: subscriptionChromeCss }), open && (_jsx("div", { style: styles.menu, role: "menu", "aria-label": translate('speed'), children: TIERS.map(tier => (_jsxs("button", { type: "button", role: "menuitemradio", "aria-checked": tier === state.tier, style: styles.item, disabled: busy, onClick: () => { choose(tier); }, children: [_jsx("span", { style: styles.itemCheck, children: tier === state.tier ? '✓' : '' }), _jsxs("span", { style: styles.itemText, children: [_jsx("span", { style: styles.itemName, children: tierName(tier) }), _jsx("span", { style: styles.itemDescription, children: tierDescription(tier) })] })] }, tier))) })), _jsx("button", { type: "button", style: styles.trigger, "aria-haspopup": "menu", "aria-expanded": open, title: triggerLabel, disabled: busy, onClick: () => {
145
146
  if (open)
146
147
  setOpen(false);
147
148
  else
@@ -151,20 +152,20 @@ export function SpeedSelect({ loadSpeed, setSpeed, t }) {
151
152
  const styles = {
152
153
  root: { position: 'relative', display: 'inline-flex' },
153
154
  trigger: {
154
- border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8,
155
- background: 'transparent', color: 'var(--dsw-alias-label-secondary)',
155
+ border: '0.5px solid var(--dsw-alias-border-l3)', borderRadius: 'var(--dsw-radius-sm, 8px)',
156
+ background: 'var(--dsh-subscription-speed-hover, transparent)', color: 'var(--dsw-alias-label-secondary)',
156
157
  font: 'inherit', fontSize: 12, lineHeight: '18px',
157
158
  padding: '2px 8px', cursor: 'pointer', whiteSpace: 'nowrap',
158
159
  },
159
160
  menu: {
160
161
  position: 'absolute', bottom: '100%', right: 0, marginBottom: 4,
161
162
  minWidth: 180, padding: 4, zIndex: 20,
162
- background: 'var(--dsw-alias-bg-layer-1)', border: '1px solid var(--dsw-alias-border-l2)',
163
- borderRadius: 8, display: 'flex', flexDirection: 'column', gap: 2,
163
+ background: 'var(--dsw-specific-menu, var(--dsw-alias-bg-layer-2))', border: 0,
164
+ boxShadow: 'var(--dsw-elevation-prominent)', borderRadius: 'var(--dsw-radius-lg, 16px)', display: 'flex', flexDirection: 'column', gap: 2,
164
165
  },
165
166
  item: {
166
167
  display: 'flex', alignItems: 'flex-start', gap: 6, width: '100%',
167
- border: 'none', borderRadius: 6, background: 'transparent',
168
+ border: 'none', borderRadius: 'var(--dsw-radius-sm, 8px)', background: 'var(--dsh-subscription-speed-hover, transparent)',
168
169
  padding: '6px 8px', cursor: 'pointer', font: 'inherit', textAlign: 'left',
169
170
  },
170
171
  itemCheck: {
@@ -20,7 +20,11 @@ export interface SubscriptionUsageBadgeInjected {
20
20
  } | undefined>;
21
21
  }
22
22
  /** Props delivered by the slot outlet + inject + the locale seat. */
23
- export type SubscriptionUsageBadgeProps = PropsRuntime<'conversation.composer.dock'> & Partial<SubscriptionUsageBadgeInjected> & Partial<PropsLocale<'settings.subscriptions'>>;
23
+ export type SubscriptionUsageBadgeProps = PropsRuntime<'conversation.composer.dock'> & Partial<SubscriptionUsageBadgeInjected> & Partial<PropsLocale<'settings.subscriptions'>>
24
+ /** Override the rotation cadence (tests only; the slot never passes it). */
25
+ & {
26
+ rotationMs?: number;
27
+ };
24
28
  /** One logged-in account's usage windows, as listed in the expanded dialog. */
25
29
  export interface AccountUsageDisplay {
26
30
  /** Account key (the `usage` endpoint's `account` argument). */
@@ -98,6 +102,19 @@ type ModelSelection = {
98
102
  export declare function retainSubscriptionSelection(previous: ModelSelection | undefined, current: ModelSelection | undefined): ModelSelection | undefined;
99
103
  /** At most one provider; no known selection or no usage means no badge. */
100
104
  export declare function collapsedDisplays(displays: readonly ProviderUsageDisplay[], current: string | undefined): readonly ProviderUsageDisplay[];
105
+ /**
106
+ * `always` mode: the current model's provider while it reports usage,
107
+ * otherwise the rotation steps through the providers that do, in display
108
+ * order, wrapping around. The index is taken modulo the current provider
109
+ * count, so providers appearing or dropping out stay inside range; the
110
+ * sequence itself may shift when the roster changes. Unlike `recent`, the
111
+ * retained subscription does not pin the pill — that retention is exactly
112
+ * what this mode exists to escape.
113
+ * @param displays - providers with usage, in display order.
114
+ * @param current - the provider of the session's current model.
115
+ * @param step - rotation tick; consecutive ticks advance one provider.
116
+ */
117
+ export declare function rotatingDisplay(displays: readonly ProviderUsageDisplay[], current: string | undefined, step: number): readonly ProviderUsageDisplay[];
101
118
  /** Order for the expanded dialog: the current provider first, the rest in poll order. */
102
119
  export declare function expandedDisplays(displays: readonly ProviderUsageDisplay[], current: string | undefined): readonly ProviderUsageDisplay[];
103
120
  type Translate = (key: SubscriptionsKey, params?: Record<string, unknown>) => string;
@@ -107,7 +124,7 @@ type Translate = (key: SubscriptionsKey, params?: Record<string, unknown>) => st
107
124
  * every provider's accounts and their windows. Returns null when no data is
108
125
  * available.
109
126
  */
110
- export declare function SubscriptionUsageBadge({ rpc, currentModel, t }: SubscriptionUsageBadgeProps): import("react").JSX.Element;
127
+ export declare function SubscriptionUsageBadge({ rpc, currentModel, t, rotationMs }: SubscriptionUsageBadgeProps): import("react").JSX.Element;
111
128
  /** Preview each account independently; all remaining quotas stay accessible. */
112
129
  export declare function AccountWindows({ windows, model, provider, translate, observedAt, stale }: {
113
130
  windows: readonly UsageWindow[];