dsh-subscriptions 0.1.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.
Files changed (150) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +129 -0
  3. package/cordis.patch.yml +12 -0
  4. package/lib/auth/claude-code-creds.d.ts +23 -0
  5. package/lib/auth/claude-code-creds.js +172 -0
  6. package/lib/auth/device-flow.d.ts +55 -0
  7. package/lib/auth/device-flow.js +177 -0
  8. package/lib/auth/jwt.d.ts +10 -0
  9. package/lib/auth/jwt.js +25 -0
  10. package/lib/auth/oauth-flow.d.ts +91 -0
  11. package/lib/auth/oauth-flow.js +227 -0
  12. package/lib/auth/pkce.d.ts +31 -0
  13. package/lib/auth/pkce.js +35 -0
  14. package/lib/auth/rpc.d.ts +209 -0
  15. package/lib/auth/rpc.js +386 -0
  16. package/lib/auth/store.d.ts +196 -0
  17. package/lib/auth/store.js +468 -0
  18. package/lib/client/CursorCard.d.ts +9 -0
  19. package/lib/client/CursorCard.js +113 -0
  20. package/lib/client/ExternalUsageCards.d.ts +9 -0
  21. package/lib/client/ExternalUsageCards.js +72 -0
  22. package/lib/client/ImageGallery.d.ts +54 -0
  23. package/lib/client/ImageGallery.js +112 -0
  24. package/lib/client/ImageGenerateToolview.d.ts +48 -0
  25. package/lib/client/ImageGenerateToolview.js +133 -0
  26. package/lib/client/ProviderAccountManager.d.ts +13 -0
  27. package/lib/client/ProviderAccountManager.js +147 -0
  28. package/lib/client/ProviderModelEditor.d.ts +39 -0
  29. package/lib/client/ProviderModelEditor.js +107 -0
  30. package/lib/client/SpeedSelect.d.ts +71 -0
  31. package/lib/client/SpeedSelect.js +177 -0
  32. package/lib/client/SubscriptionUsageBadge.d.ts +116 -0
  33. package/lib/client/SubscriptionUsageBadge.js +543 -0
  34. package/lib/client/SubscriptionsSection.d.ts +140 -0
  35. package/lib/client/SubscriptionsSection.js +544 -0
  36. package/lib/client/UsageBadgeDisplaySetting.d.ts +7 -0
  37. package/lib/client/UsageBadgeDisplaySetting.js +36 -0
  38. package/lib/client/VideoGenerateToolview.d.ts +42 -0
  39. package/lib/client/VideoGenerateToolview.js +175 -0
  40. package/lib/client/account-preferences.d.ts +24 -0
  41. package/lib/client/account-preferences.js +21 -0
  42. package/lib/client/fast-command.d.ts +2 -0
  43. package/lib/client/fast-command.js +4 -0
  44. package/lib/client/host-contract.d.ts +90 -0
  45. package/lib/client/host-contract.js +101 -0
  46. package/lib/client/host-icons.d.ts +24 -0
  47. package/lib/client/host-icons.js +21 -0
  48. package/lib/client/index.d.ts +27 -0
  49. package/lib/client/index.js +136 -0
  50. package/lib/client/locales.d.ts +297 -0
  51. package/lib/client/locales.js +295 -0
  52. package/lib/client/subscription-card-styles.d.ts +3 -0
  53. package/lib/client/subscription-card-styles.js +53 -0
  54. package/lib/client/subscriptions-rpc.d.ts +14 -0
  55. package/lib/client/subscriptions-rpc.js +34 -0
  56. package/lib/client/usage-badge-preferences.d.ts +11 -0
  57. package/lib/client/usage-badge-preferences.js +41 -0
  58. package/lib/client.js +4472 -0
  59. package/lib/client.js.map +1 -0
  60. package/lib/compat.d.ts +36 -0
  61. package/lib/compat.js +20 -0
  62. package/lib/http.d.ts +6 -0
  63. package/lib/http.js +15 -0
  64. package/lib/index.d.ts +80 -0
  65. package/lib/index.js +14018 -0
  66. package/lib/model-defaults.d.ts +23 -0
  67. package/lib/model-defaults.js +237 -0
  68. package/lib/provider-settings.d.ts +39 -0
  69. package/lib/provider-settings.js +151 -0
  70. package/lib/providers/account-preferences.d.ts +41 -0
  71. package/lib/providers/account-preferences.js +153 -0
  72. package/lib/providers/accounts.d.ts +112 -0
  73. package/lib/providers/accounts.js +142 -0
  74. package/lib/providers/antigravity-oauth-client.d.ts +7 -0
  75. package/lib/providers/antigravity-oauth-client.js +7 -0
  76. package/lib/providers/antigravity.d.ts +110 -0
  77. package/lib/providers/antigravity.js +516 -0
  78. package/lib/providers/catalog-store.d.ts +48 -0
  79. package/lib/providers/catalog-store.js +243 -0
  80. package/lib/providers/claude-cli-version.d.ts +15 -0
  81. package/lib/providers/claude-cli-version.js +24 -0
  82. package/lib/providers/claude.d.ts +181 -0
  83. package/lib/providers/claude.js +680 -0
  84. package/lib/providers/codex-client-version.d.ts +8 -0
  85. package/lib/providers/codex-client-version.js +17 -0
  86. package/lib/providers/codex-search.d.ts +29 -0
  87. package/lib/providers/codex-search.js +198 -0
  88. package/lib/providers/codex.d.ts +229 -0
  89. package/lib/providers/codex.js +821 -0
  90. package/lib/providers/common.d.ts +389 -0
  91. package/lib/providers/common.js +656 -0
  92. package/lib/providers/copilot.d.ts +344 -0
  93. package/lib/providers/copilot.js +888 -0
  94. package/lib/providers/cursor-adapter.d.ts +15 -0
  95. package/lib/providers/cursor-adapter.js +39 -0
  96. package/lib/providers/cursor-auth.d.ts +43 -0
  97. package/lib/providers/cursor-auth.js +169 -0
  98. package/lib/providers/cursor-model-settings.d.ts +15 -0
  99. package/lib/providers/cursor-model-settings.js +51 -0
  100. package/lib/providers/cursor-usage.d.ts +9 -0
  101. package/lib/providers/cursor-usage.js +112 -0
  102. package/lib/providers/external-usage-controller.d.ts +19 -0
  103. package/lib/providers/external-usage-controller.js +32 -0
  104. package/lib/providers/external-usage.d.ts +8 -0
  105. package/lib/providers/external-usage.js +82 -0
  106. package/lib/providers/grok.d.ts +205 -0
  107. package/lib/providers/grok.js +713 -0
  108. package/lib/providers/image-pool.d.ts +30 -0
  109. package/lib/providers/image-pool.js +89 -0
  110. package/lib/providers/npm-cli-version.d.ts +56 -0
  111. package/lib/providers/npm-cli-version.js +98 -0
  112. package/lib/providers/pool-family.d.ts +56 -0
  113. package/lib/providers/pool-family.js +45 -0
  114. package/lib/providers/pool-health.d.ts +74 -0
  115. package/lib/providers/pool-health.js +150 -0
  116. package/lib/providers/pool-usage.d.ts +86 -0
  117. package/lib/providers/pool-usage.js +223 -0
  118. package/lib/providers/pool.d.ts +108 -0
  119. package/lib/providers/pool.js +368 -0
  120. package/lib/providers/rate-limit.d.ts +192 -0
  121. package/lib/providers/rate-limit.js +338 -0
  122. package/lib/tools/image-generate.d.ts +128 -0
  123. package/lib/tools/image-generate.js +438 -0
  124. package/lib/tools/registration.d.ts +15 -0
  125. package/lib/tools/registration.js +24 -0
  126. package/lib/tools/video-generate.d.ts +89 -0
  127. package/lib/tools/video-generate.js +257 -0
  128. package/lib/tools/x-search.d.ts +58 -0
  129. package/lib/tools/x-search.js +197 -0
  130. package/lib/translate/anthropic.d.ts +161 -0
  131. package/lib/translate/anthropic.js +515 -0
  132. package/lib/translate/antigravity-schema.d.ts +4 -0
  133. package/lib/translate/antigravity-schema.js +145 -0
  134. package/lib/translate/antigravity-thinking.d.ts +5 -0
  135. package/lib/translate/antigravity-thinking.js +44 -0
  136. package/lib/translate/antigravity.d.ts +114 -0
  137. package/lib/translate/antigravity.js +366 -0
  138. package/lib/translate/chat-completions.d.ts +120 -0
  139. package/lib/translate/chat-completions.js +382 -0
  140. package/lib/translate/resolved.d.ts +80 -0
  141. package/lib/translate/resolved.js +118 -0
  142. package/lib/translate/responses.d.ts +179 -0
  143. package/lib/translate/responses.js +408 -0
  144. package/lib/translate/sse.d.ts +21 -0
  145. package/lib/translate/sse.js +56 -0
  146. package/package.json +114 -0
  147. package/vendor/cursor/LICENSE +21 -0
  148. package/vendor/cursor/index.d.ts +18 -0
  149. package/vendor/cursor/index.js +3574 -0
  150. package/vendor/cursor/proto.js +254 -0
@@ -0,0 +1,544 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /**
3
+ * Subscriptions settings section: one card per subscription provider with an
4
+ * OAuth login/logout flow driven by the node half's `/subscriptions-auth` RPC
5
+ * channel. Login state lives server-side; the page polls `status` only while
6
+ * a login attempt is busy, so an idle page never polls. All state is local
7
+ * React state — the page has no store.
8
+ *
9
+ * Every color resolves through a `--dsw-alias-*` design token (the ui-theme
10
+ * design-platform.css values flip under `body[data-ds-dark-theme]`), and
11
+ * every user-visible string goes through the locale-bound `t` of the
12
+ * 'settings.subscriptions' namespace. Buttons and inputs take the
13
+ * ModelsSection vocabulary minus hover rules, which inline styles cannot
14
+ * express.
15
+ */
16
+ import { useCallback, useEffect, useRef, useState } from 'react';
17
+ import { en } from './locales.js';
18
+ import { ProviderAccountManager } from './ProviderAccountManager.js';
19
+ import { ExternalUsageCards } from './ExternalUsageCards.js';
20
+ import { CursorCard } from './CursorCard.js';
21
+ import { UsageBadgeDisplaySetting } from './UsageBadgeDisplaySetting.js';
22
+ import { USAGE_BADGE_REFRESH_EVENT } from './usage-badge-preferences.js';
23
+ import { subscriptionCardStyles as cardStyles } from './subscription-card-styles.js';
24
+ import { callSubscriptionsAuth, SubscriptionsAuthError } from './subscriptions-rpc.js';
25
+ export { callSubscriptionsAuth } from './subscriptions-rpc.js';
26
+ /** Poll cadence while a provider login attempt is busy. */
27
+ const POLL_INTERVAL_MS = 2000;
28
+ /**
29
+ * Model count above which the expanded default-effort list also offers a name
30
+ * filter; below it the list is short enough to scan.
31
+ */
32
+ const MODEL_FILTER_THRESHOLD = 8;
33
+ const CLIENT_VERSION_SOURCES = {
34
+ npm: 'clientVersionNpm',
35
+ local: 'clientVersionLocal',
36
+ fallback: 'clientVersionFallback',
37
+ config: 'clientVersionConfig',
38
+ };
39
+ /** Card display metadata, in page order (names are brand names, not translated). */
40
+ const PROVIDERS = [
41
+ { id: 'codex', name: 'Codex (ChatGPT)' },
42
+ { id: 'claude', name: 'Claude' },
43
+ { id: 'grok', name: 'Grok (X Premium)' },
44
+ { id: 'copilot', name: 'GitHub Copilot' },
45
+ { id: 'antigravity', name: 'Google Antigravity' },
46
+ ];
47
+ /** Human text of an action failure, SubscriptionsAuthError or not. */
48
+ function messageOf(error) {
49
+ return error instanceof Error ? error.message : String(error);
50
+ }
51
+ /** Copy a keyed map without the entries whose key is not in `live`. */
52
+ function dropStale(map, live) {
53
+ const stale = Object.keys(map).filter(key => !live.has(key));
54
+ if (stale.length === 0)
55
+ return map;
56
+ const next = { ...map };
57
+ for (const key of stale)
58
+ delete next[key];
59
+ return next;
60
+ }
61
+ /**
62
+ * English-dictionary fallback for a missing inject `t` (standalone renders);
63
+ * the slot inject always supplies the locale-bound one.
64
+ * @param key - dictionary key.
65
+ * @param params - `{name}` template params.
66
+ * @returns the template with params substituted.
67
+ */
68
+ function fallbackTranslate(key, params) {
69
+ let text = en[key];
70
+ for (const [name, value] of Object.entries(params ?? {})) {
71
+ text = text.replaceAll(`{${name}}`, String(value));
72
+ }
73
+ return text;
74
+ }
75
+ const styles = {
76
+ section: {
77
+ display: 'flex', flexDirection: 'column', gap: 12, maxWidth: 560,
78
+ color: 'var(--dsw-alias-label-primary)',
79
+ },
80
+ intro: { margin: 0, color: 'var(--dsw-alias-label-tertiary)', fontSize: 14, lineHeight: '22px' },
81
+ card: cardStyles.card,
82
+ cardHeader: cardStyles.header,
83
+ dot: cardStyles.dot,
84
+ name: cardStyles.name,
85
+ statusLine: cardStyles.status,
86
+ clientVersion: { fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-label-tertiary)', fontVariantNumeric: 'tabular-nums' },
87
+ errorLine: cardStyles.error,
88
+ actions: cardStyles.actions,
89
+ button: cardStyles.button,
90
+ usage: cardStyles.usage,
91
+ usageHeader: cardStyles.usageHeader,
92
+ usageTitle: cardStyles.usageTitle,
93
+ usagePlan: cardStyles.usagePlan,
94
+ usageRefresh: cardStyles.usageRefresh,
95
+ usageRow: cardStyles.usageRow,
96
+ usageMeta: cardStyles.usageMeta,
97
+ accountRow: cardStyles.account,
98
+ accountHeader: cardStyles.accountHeader,
99
+ accountName: cardStyles.accountName,
100
+ starButton: {
101
+ border: 'none', background: 'transparent', padding: 0,
102
+ font: 'inherit', fontSize: 14, lineHeight: '20px', cursor: 'pointer',
103
+ color: 'var(--dsw-alias-state-warn-label)',
104
+ },
105
+ usageTrack: cardStyles.usageTrack,
106
+ usageFill: cardStyles.usageFill,
107
+ deviceCode: {
108
+ marginTop: 4, display: 'flex', flexDirection: 'column', gap: 6,
109
+ border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8,
110
+ padding: '10px 12px', background: 'var(--dsw-alias-bg-layer-1)',
111
+ },
112
+ deviceCodeText: {
113
+ fontFamily: 'monospace', fontSize: 18, lineHeight: '24px', letterSpacing: 2,
114
+ color: 'var(--dsw-alias-label-primary)', userSelect: 'all',
115
+ },
116
+ modalOverlay: {
117
+ position: 'fixed', inset: 0, zIndex: 1000,
118
+ display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16,
119
+ background: 'rgba(0, 0, 0, 0.45)',
120
+ },
121
+ modal: {
122
+ width: 460, maxWidth: '100%', maxHeight: '90vh', overflowY: 'auto',
123
+ boxSizing: 'border-box', display: 'flex', flexDirection: 'column', gap: 12,
124
+ padding: '16px 18px', borderRadius: 12,
125
+ background: 'var(--dsw-alias-bg-layer-1)', border: '1px solid var(--dsw-alias-border-l2)',
126
+ },
127
+ modalHeader: { display: 'flex', alignItems: 'center', gap: 8 },
128
+ modalTitle: { fontWeight: 600, fontSize: 15, lineHeight: '22px', color: 'var(--dsw-alias-label-primary)' },
129
+ };
130
+ /** Status dot color for one provider state. */
131
+ function dotColor(status) {
132
+ if (status?.busy === true)
133
+ return 'var(--dsw-alias-state-warn-label)';
134
+ if ((status?.accounts.length ?? 0) > 0)
135
+ return 'var(--dsw-alias-state-success-primary)';
136
+ return 'var(--dsw-alias-label-dimmed)';
137
+ }
138
+ /**
139
+ * Whether a provider has at least one connected account. Multi-account made
140
+ * "logged in" a property of the account list rather than a flag, so every
141
+ * logged-in test goes through this one predicate.
142
+ * @param status - the provider's last reported state, possibly absent.
143
+ * @returns true when the provider serves at least one account.
144
+ */
145
+ function hasAccount(status) {
146
+ return (status?.accounts.length ?? 0) > 0;
147
+ }
148
+ /**
149
+ * One-line status text for one provider state.
150
+ * @param t - section translate.
151
+ * @param status - the provider's last reported state.
152
+ * @returns the localized status line.
153
+ */
154
+ function statusText(t, status) {
155
+ if (status === undefined)
156
+ return t('checking');
157
+ if (status.busy)
158
+ return t('loginInProgress');
159
+ if (status.accounts.length > 0)
160
+ return t('loggedInCount', { count: status.accounts.length });
161
+ return t('notLoggedIn');
162
+ }
163
+ /**
164
+ * The CLI version a route presents, beside the provider name; nothing for
165
+ * routes without one. The source word (npm / local / built-in / configured)
166
+ * is the fix for upstream issue #108: a failed npm lookup must read as a
167
+ * fallback, never as a plan limit.
168
+ */
169
+ export function clientVersionText(t, clientVersion) {
170
+ if (clientVersion === undefined)
171
+ return '';
172
+ return t('clientVersion', { version: clientVersion.version, source: t(CLIENT_VERSION_SOURCES[clientVersion.source]) });
173
+ }
174
+ /**
175
+ * Localized label of one usage window (kind, plus the model scope when named).
176
+ * @param t - section translate.
177
+ * @param window - the reported window.
178
+ * @returns e.g. "5-hour window" or "Weekly · Opus".
179
+ */
180
+ function usageWindowLabel(t, window) {
181
+ const base = window.kind === 'session'
182
+ ? t('usageSession')
183
+ : window.kind === 'weekly' ? t('usageWeekly') : t('usageWindow');
184
+ return window.scope !== undefined && window.scope !== '' ? `${base} · ${window.scope}` : base;
185
+ }
186
+ /** Bar fill color: success normally, warn from 80%, error from 95%. Shared with the composer badge. */
187
+ export function usageBarColor(usedPercent) {
188
+ if (usedPercent >= 95)
189
+ return 'var(--dsw-alias-state-error-primary)';
190
+ if (usedPercent >= 80)
191
+ return 'var(--dsw-alias-state-warn-label)';
192
+ return 'var(--dsw-alias-state-success-primary)';
193
+ }
194
+ /**
195
+ * Derive one provider's default-effort section from its catalog and filter.
196
+ * Pure so the collapsed-header counts and the filter stay testable without a
197
+ * DOM: rows come only from models that advertise levels, the count of the rest
198
+ * rides as one line, and the filter matches display name or model id.
199
+ * @param models - the provider's catalog models, or undefined while loading.
200
+ * @param filter - the raw filter input (trimmed and lowercased here).
201
+ * @returns the section's rows and header counts.
202
+ */
203
+ export function deriveModelDefaultsView(models, filter) {
204
+ const all = models ?? [];
205
+ const withEfforts = all.filter(model => model.efforts.length > 0);
206
+ const query = filter.trim().toLowerCase();
207
+ const shown = query === ''
208
+ ? withEfforts
209
+ : withEfforts.filter(model => model.name.toLowerCase().includes(query)
210
+ || model.id.toLowerCase().includes(query));
211
+ return {
212
+ shown,
213
+ total: withEfforts.length,
214
+ overridden: withEfforts.filter(model => model.configured !== undefined).length,
215
+ withoutEfforts: all.length - withEfforts.length,
216
+ showFilter: withEfforts.length > MODEL_FILTER_THRESHOLD,
217
+ };
218
+ }
219
+ /**
220
+ * Whether the default-effort catalog needs (re)fetching.
221
+ *
222
+ * Fetching is gated on an *attempt* signature rather than on the payload
223
+ * being empty: an empty answer is a legitimate result (a narrowed
224
+ * `config.providers`, or a catalog that is momentarily unavailable), and
225
+ * treating it as "not loaded yet" re-ran this effect forever. The signature
226
+ * also covers the accounts, so logging a second provider in refetches
227
+ * instead of leaving that card on the previous answer.
228
+ * @param input - the decision inputs.
229
+ * @returns true when the caller should start a fetch.
230
+ */
231
+ export function shouldFetchModelDefaults(input) {
232
+ if (input.failed)
233
+ return false;
234
+ if (input.loggedIn.length === 0)
235
+ return false;
236
+ // Only an open disclosure pays for the per-model live resolve.
237
+ if (!input.open.some(provider => input.loggedIn.includes(provider)))
238
+ return false;
239
+ return input.loadedFor !== input.signature;
240
+ }
241
+ /**
242
+ * Stable signature of the accounts a catalog answer depends on. A change
243
+ * means a previous answer is stale (an account arrived or left), so the next
244
+ * open disclosure refetches.
245
+ * @param statuses - the per-provider status snapshot.
246
+ * @returns a signature string, stable across renders with equal accounts.
247
+ */
248
+ export function modelDefaultsSignature(statuses) {
249
+ return PROVIDERS
250
+ .map(({ id }) => `${id}:${(statuses[id]?.accounts ?? []).map(account => account.key).sort().join(',')}`)
251
+ .join('|');
252
+ }
253
+ /**
254
+ * The Subscriptions settings page component.
255
+ * @param props - the slot inject face ({@link SubscriptionsSectionInjected}).
256
+ * @returns the section body, or a notice while the RPC face is absent.
257
+ */
258
+ export function SubscriptionsSection(props) {
259
+ const { rpc } = props;
260
+ const t = props.t ?? fallbackTranslate;
261
+ const [statuses, setStatuses] = useState({});
262
+ const [errors, setErrors] = useState({});
263
+ const [manualDrafts, setManualDrafts] = useState({
264
+ codex: '', claude: '', grok: '', copilot: '', antigravity: '',
265
+ });
266
+ /** Pending device-flow codes (copilot), shown while the attempt polls. */
267
+ const [deviceCodes, setDeviceCodes] = useState({});
268
+ const [copiedCode, setCopiedCode] = useState(undefined);
269
+ /** Usage snapshots keyed `${provider}:${accountKey}` — every account tracks its own windows. */
270
+ const [usages, setUsages] = useState({});
271
+ const [usageErrors, setUsageErrors] = useState({});
272
+ const [usageLoading, setUsageLoading] = useState({});
273
+ const mountedRef = useRef(true);
274
+ const pollersRef = useRef(new Map());
275
+ /** Accounts with a `usage` call in flight; guards the auto-fetch effect against re-entry. */
276
+ const usageInflightRef = useRef(new Set());
277
+ const usageRosterSignatureRef = useRef();
278
+ const [managedProvider, setManagedProvider] = useState();
279
+ const setProviderError = useCallback((provider, message) => {
280
+ if (!mountedRef.current)
281
+ return;
282
+ setErrors((prev) => {
283
+ const next = { ...prev };
284
+ if (message === undefined)
285
+ delete next[provider];
286
+ else
287
+ next[provider] = message;
288
+ return next;
289
+ });
290
+ }, []);
291
+ const stopPolling = useCallback((provider) => {
292
+ const poller = pollersRef.current.get(provider);
293
+ if (poller !== undefined) {
294
+ clearInterval(poller);
295
+ pollersRef.current.delete(provider);
296
+ }
297
+ }, []);
298
+ /** Refetch every provider's status; stop a provider's poller once its attempt settles. */
299
+ const refresh = useCallback(async () => {
300
+ if (rpc === undefined)
301
+ return;
302
+ let response;
303
+ try {
304
+ response = await callSubscriptionsAuth(rpc, 'status', {});
305
+ }
306
+ catch (error) {
307
+ // A failed poll must not kill the page; busy providers keep polling and
308
+ // the action paths report their own errors. But staying silent turns a
309
+ // persistent failure into an endless "Checking…" — show it instead.
310
+ const message = error instanceof Error ? error.message : String(error);
311
+ for (const { id } of PROVIDERS)
312
+ setProviderError(id, message);
313
+ return;
314
+ }
315
+ if (!mountedRef.current)
316
+ return;
317
+ setStatuses(response.providers);
318
+ const signature = PROVIDERS.map(({ id }) => `${id}:${response.providers[id].accounts
319
+ .map(account => `${account.key}:${account.isDefault}`).join(',')}`).join(';');
320
+ if (signature !== usageRosterSignatureRef.current) {
321
+ usageRosterSignatureRef.current = signature;
322
+ window.dispatchEvent(new Event(USAGE_BADGE_REFRESH_EVENT));
323
+ }
324
+ // The poll recovered: drop any error line a previous failed poll left.
325
+ for (const { id } of PROVIDERS)
326
+ setProviderError(id, undefined);
327
+ for (const { id } of PROVIDERS) {
328
+ const status = response.providers[id];
329
+ if (status.accounts.length > 0 || !status.busy) {
330
+ stopPolling(id);
331
+ // The attempt settled (success, timeout, or cancel): drop the code card.
332
+ setDeviceCodes((prev) => {
333
+ if (prev[id] === undefined)
334
+ return prev;
335
+ const next = { ...prev };
336
+ delete next[id];
337
+ return next;
338
+ });
339
+ }
340
+ }
341
+ }, [rpc, stopPolling, setProviderError]);
342
+ const startPolling = useCallback((provider) => {
343
+ if (pollersRef.current.has(provider))
344
+ return;
345
+ pollersRef.current.set(provider, setInterval(() => { void refresh(); }, POLL_INTERVAL_MS));
346
+ }, [refresh]);
347
+ // Initial load; every busy provider (e.g. an attempt started before a page
348
+ // reload) resumes polling. Teardown clears pollers and the mounted guard.
349
+ useEffect(() => {
350
+ mountedRef.current = true;
351
+ void refresh().then(() => {
352
+ if (!mountedRef.current)
353
+ return;
354
+ setStatuses((current) => {
355
+ for (const { id } of PROVIDERS) {
356
+ if (current[id]?.busy === true)
357
+ startPolling(id);
358
+ }
359
+ return current;
360
+ });
361
+ });
362
+ return () => {
363
+ mountedRef.current = false;
364
+ for (const poller of pollersRef.current.values())
365
+ clearInterval(poller);
366
+ pollersRef.current.clear();
367
+ };
368
+ }, [refresh, startPolling]);
369
+ const loadUsage = useCallback(async (provider, account, force = false) => {
370
+ const key = `${provider}:${account}`;
371
+ if (rpc === undefined || usageInflightRef.current.has(key))
372
+ return;
373
+ usageInflightRef.current.add(key);
374
+ setUsageLoading(prev => ({ ...prev, [key]: true }));
375
+ try {
376
+ const usage = await callSubscriptionsAuth(rpc, 'usage', { provider, account, ...force ? { force: true } : {} });
377
+ if (!mountedRef.current)
378
+ return;
379
+ setUsages(prev => ({ ...prev, [key]: usage }));
380
+ window.dispatchEvent(new Event(USAGE_BADGE_REFRESH_EVENT));
381
+ setUsageErrors((prev) => {
382
+ const next = { ...prev };
383
+ delete next[key];
384
+ return next;
385
+ });
386
+ }
387
+ catch (error) {
388
+ if (mountedRef.current)
389
+ setUsageErrors(prev => ({ ...prev, [key]: messageOf(error) }));
390
+ }
391
+ finally {
392
+ usageInflightRef.current.delete(key);
393
+ if (mountedRef.current)
394
+ setUsageLoading(prev => ({ ...prev, [key]: false }));
395
+ }
396
+ }, [rpc]);
397
+ // Fetch usage once an account is logged in; drop the snapshots of accounts
398
+ // that vanished so a re-login refetches. A failed lookup does not auto-retry
399
+ // — the per-account Refresh button is the retry path.
400
+ useEffect(() => {
401
+ const live = new Set();
402
+ for (const { id } of PROVIDERS) {
403
+ for (const account of statuses[id]?.accounts ?? []) {
404
+ const key = `${id}:${account.key}`;
405
+ live.add(key);
406
+ if (usages[key] === undefined && usageErrors[key] === undefined)
407
+ void loadUsage(id, account.key);
408
+ }
409
+ }
410
+ setUsages(prev => dropStale(prev, live));
411
+ setUsageErrors(prev => dropStale(prev, live));
412
+ }, [statuses, usages, usageErrors, loadUsage]);
413
+ const login = useCallback(async (provider, method) => {
414
+ if (rpc === undefined)
415
+ return;
416
+ setProviderError(provider, undefined);
417
+ try {
418
+ const response = await callSubscriptionsAuth(rpc, 'login', {
419
+ provider,
420
+ ...method === undefined ? {} : { method },
421
+ });
422
+ if (typeof response.authorizeUrl === 'string' && response.authorizeUrl === '') {
423
+ // Instant login (e.g. imported from Claude Code credentials)
424
+ await refresh();
425
+ return;
426
+ }
427
+ if (typeof response.authorizeUrl !== 'string') {
428
+ throw new SubscriptionsAuthError(t('loginMissingUrl'));
429
+ }
430
+ if (!mountedRef.current)
431
+ return;
432
+ // Optimistic busy so Cancel and the manual fallback appear before the first poll tick.
433
+ setStatuses(prev => ({
434
+ ...prev,
435
+ [provider]: { accounts: prev[provider]?.accounts ?? [], ...prev[provider], busy: true },
436
+ }));
437
+ if (typeof response.userCode === 'string' && response.userCode.length > 0) {
438
+ // Device flow: show the code card instead of opening the page blind —
439
+ // the user copies the code first, then opens the verification page.
440
+ setDeviceCodes(prev => ({ ...prev, [provider]: { userCode: response.userCode, verificationUrl: response.authorizeUrl } }));
441
+ }
442
+ else {
443
+ window.open(response.authorizeUrl, '_blank', 'noopener');
444
+ }
445
+ startPolling(provider);
446
+ }
447
+ catch (error) {
448
+ setProviderError(provider, messageOf(error));
449
+ }
450
+ }, [rpc, t, setProviderError, startPolling]);
451
+ const cancel = useCallback(async (provider) => {
452
+ if (rpc === undefined)
453
+ return;
454
+ stopPolling(provider);
455
+ try {
456
+ await callSubscriptionsAuth(rpc, 'cancel', { provider });
457
+ }
458
+ catch (error) {
459
+ setProviderError(provider, messageOf(error));
460
+ }
461
+ await refresh();
462
+ }, [rpc, stopPolling, setProviderError, refresh]);
463
+ const submitManual = useCallback(async (provider) => {
464
+ if (rpc === undefined)
465
+ return;
466
+ const input = manualDrafts[provider].trim();
467
+ if (input === '')
468
+ return;
469
+ setProviderError(provider, undefined);
470
+ try {
471
+ await callSubscriptionsAuth(rpc, 'manual', { provider, input });
472
+ if (mountedRef.current)
473
+ setManualDrafts(prev => ({ ...prev, [provider]: '' }));
474
+ }
475
+ catch (error) {
476
+ setProviderError(provider, messageOf(error));
477
+ }
478
+ await refresh();
479
+ }, [rpc, manualDrafts, setProviderError, refresh]);
480
+ const logout = useCallback(async (provider, account, display, name) => {
481
+ if (rpc === undefined)
482
+ return;
483
+ if (!window.confirm(t('logoutAccountConfirm', { provider: name, account: display })))
484
+ return;
485
+ setProviderError(provider, undefined);
486
+ try {
487
+ await callSubscriptionsAuth(rpc, 'logout', { provider, account });
488
+ }
489
+ catch (error) {
490
+ setProviderError(provider, messageOf(error));
491
+ }
492
+ await refresh();
493
+ }, [rpc, t, setProviderError, refresh]);
494
+ const setDefault = useCallback(async (provider, account) => {
495
+ if (rpc === undefined)
496
+ return;
497
+ setProviderError(provider, undefined);
498
+ try {
499
+ await callSubscriptionsAuth(rpc, 'setDefault', { provider, account });
500
+ }
501
+ catch (error) {
502
+ setProviderError(provider, messageOf(error));
503
+ }
504
+ await refresh();
505
+ }, [rpc, setProviderError, refresh]);
506
+ const copyDeviceCode = useCallback((provider, userCode) => {
507
+ void navigator.clipboard?.writeText(userCode).then(() => {
508
+ if (!mountedRef.current)
509
+ return;
510
+ setCopiedCode(provider);
511
+ setTimeout(() => {
512
+ if (mountedRef.current) {
513
+ setCopiedCode(current => current === provider ? undefined : current);
514
+ }
515
+ }, 1500);
516
+ }).catch(() => undefined);
517
+ }, []);
518
+ if (rpc === undefined) {
519
+ return _jsx("p", { style: styles.intro, children: t('unavailable') });
520
+ }
521
+ return (_jsxs("div", { style: styles.section, children: [_jsx("p", { style: styles.intro, children: t('intro') }), _jsx(UsageBadgeDisplaySetting, { t: t }), PROVIDERS.map(({ id, name }) => {
522
+ const status = statuses[id];
523
+ const busy = status?.busy === true;
524
+ const deviceCode = deviceCodes[id];
525
+ const accounts = status?.accounts ?? [];
526
+ return (_jsxs("div", { style: styles.card, children: [_jsxs("div", { style: styles.cardHeader, children: [_jsx("span", { style: { ...styles.dot, background: dotColor(status) } }), _jsx("span", { style: styles.name, children: name }), status?.clientVersion !== undefined && (_jsx("span", { style: styles.clientVersion, title: t('clientVersionHint'), children: clientVersionText(t, status.clientVersion) }))] }), _jsx("p", { style: styles.statusLine, children: statusText(t, status) }), status?.detail !== undefined && status.detail !== '' && (_jsx("p", { style: styles.statusLine, children: status.detail })), errors[id] !== undefined && _jsx("p", { style: styles.errorLine, children: errors[id] }), accounts.map((account) => {
527
+ const usageKey = `${id}:${account.key}`;
528
+ const usage = usages[usageKey];
529
+ const usageError = usageErrors[usageKey];
530
+ const display = account.account ?? account.key;
531
+ // Providers without a usage endpoint answer supported:false — no block.
532
+ const showUsage = usage?.supported !== false
533
+ && (usage !== undefined || usageError !== undefined || usageLoading[usageKey] === true);
534
+ return (_jsxs("div", { style: styles.accountRow, children: [_jsxs("div", { style: styles.accountHeader, children: [_jsx("button", { type: "button", style: styles.starButton, title: account.isDefault ? t('defaultBadge') : t('setDefault'), onClick: () => {
535
+ if (!account.isDefault)
536
+ void setDefault(id, account.key);
537
+ }, children: account.isDefault ? '★' : '☆' }), _jsx("span", { style: styles.accountName, children: display }), account.plan !== undefined && (_jsx("span", { style: styles.usagePlan, children: account.plan })), account.expiresAt !== undefined && (_jsx("span", { style: styles.statusLine, children: t('accountExpires', { date: new Date(account.expiresAt).toLocaleString() }) })), _jsx("button", { type: "button", style: { ...styles.button, marginLeft: 'auto', flexShrink: 0 }, onClick: () => { void logout(id, account.key, display, name); }, children: t('logout') })] }), showUsage && (_jsxs("div", { style: styles.usage, children: [_jsxs("div", { style: styles.usageHeader, children: [_jsx("span", { style: styles.usageTitle, children: t('usageTitle') }), usage?.plan !== undefined && (_jsx("span", { style: styles.usagePlan, children: t('usagePlan', { plan: usage.plan }) })), _jsx("button", { type: "button", style: { ...styles.usageRefresh, ...usageLoading[usageKey] === true ? { opacity: 0.5, cursor: 'default' } : {} }, disabled: usageLoading[usageKey] === true, onClick: () => { void loadUsage(id, account.key, true); }, children: t('usageRefresh') })] }), usage === undefined && usageError === undefined && (_jsx("p", { style: styles.statusLine, children: t('usageLoading') })), usageError !== undefined && (_jsx("p", { style: styles.errorLine, children: t('usageError', { message: usageError }) })), usage?.windows !== undefined && usage.windows.length === 0 && (_jsx("p", { style: styles.statusLine, children: t('usageEmpty') })), (usage?.windows ?? []).map((window, index) => {
538
+ const percent = Math.min(100, Math.max(0, window.usedPercent));
539
+ return (_jsxs("div", { style: styles.usageRow, children: [_jsxs("div", { style: styles.usageMeta, children: [_jsx("span", { children: usageWindowLabel(t, window) }), _jsxs("span", { children: [`${String(Math.round(percent))}%`, window.resetsAt !== undefined
540
+ && ` · ${t('usageResets', { date: new Date(window.resetsAt).toLocaleString() })}`] })] }), _jsx("div", { style: styles.usageTrack, children: _jsx("div", { style: { ...styles.usageFill, width: `${String(percent)}%`, background: usageBarColor(percent) } }) })] }, index));
541
+ })] }))] }, account.key));
542
+ }), _jsxs("div", { style: styles.actions, children: [!busy && accounts.length === 0 && (_jsx("button", { type: "button", style: styles.button, onClick: () => { void login(id); }, children: t('login') })), !busy && accounts.length > 0 && id === 'claude' && (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", style: styles.button, onClick: () => { void login(id, 'oauth'); }, children: t('addAccountOAuth') }), _jsx("button", { type: "button", style: styles.button, onClick: () => { void login(id, 'keychain'); }, children: t('addAccountKeychain') })] })), !busy && accounts.length > 0 && id !== 'claude' && (_jsx("button", { type: "button", style: styles.button, onClick: () => { void login(id); }, children: t('addAccount') })), _jsx("button", { type: "button", style: styles.button, "aria-haspopup": "dialog", onClick: () => setManagedProvider({ id, name }), children: t('accountsManage') }), busy && (_jsx("button", { type: "button", style: styles.button, onClick: () => { void cancel(id); }, children: t('cancel') }))] }), !busy && accounts.length > 0 && (_jsx("p", { style: styles.statusLine, children: t('addAccountHint') })), busy && deviceCode !== undefined && (_jsxs("div", { style: styles.deviceCode, children: [_jsx("span", { style: styles.statusLine, children: t('deviceCodePrompt') }), _jsx("span", { style: styles.deviceCodeText, children: deviceCode.userCode }), _jsxs("div", { style: styles.actions, children: [_jsx("button", { type: "button", style: styles.button, onClick: () => { copyDeviceCode(id, deviceCode.userCode); }, children: copiedCode === id ? t('deviceCodeCopied') : t('deviceCodeCopy') }), _jsx("button", { type: "button", style: styles.button, onClick: () => { window.open(deviceCode.verificationUrl, '_blank', 'noopener'); }, children: t('deviceCodeOpenPage') })] })] })), busy && deviceCode === undefined && (_jsxs("details", { style: styles.manual, children: [_jsx("summary", { children: t('manualSummary') }), _jsxs("div", { style: styles.manualRow, children: [_jsx("input", { style: styles.manualInput, value: manualDrafts[id], placeholder: t('manualPlaceholder'), onChange: event => setManualDrafts(prev => ({ ...prev, [id]: event.target.value })) }), _jsx("button", { type: "button", style: styles.button, onClick: () => { void submitManual(id); }, children: t('submit') })] })] }))] }, id));
543
+ }), _jsx(CursorCard, { rpc: rpc, t: t }), _jsx(ExternalUsageCards, { rpc: rpc, t: t }), managedProvider && _jsx(ProviderAccountManager, { provider: managedProvider.id, name: managedProvider.name, rpc: rpc, t: t, onClose: () => setManagedProvider(undefined) })] }));
544
+ }
@@ -0,0 +1,7 @@
1
+ import type { SubscriptionsKey } from './locales.js';
2
+ type Translate = (key: SubscriptionsKey, params?: Record<string, unknown>) => string;
3
+ /** Browser-local display preference; never changes provider or account settings. */
4
+ export declare function UsageBadgeDisplaySetting({ t }: {
5
+ t: Translate;
6
+ }): import("react").JSX.Element;
7
+ export {};
@@ -0,0 +1,36 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { setUsageBadgeMode, useUsageBadgeMode } from './usage-badge-preferences.js';
4
+ /** Browser-local display preference; never changes provider or account settings. */
5
+ export function UsageBadgeDisplaySetting({ t }) {
6
+ const mode = useUsageBadgeMode();
7
+ const [failed, setFailed] = useState(false);
8
+ return (_jsxs("div", { style: styles.card, children: [_jsxs("label", { style: styles.field, children: [_jsx("span", { children: t('usageBadgeDisplay') }), _jsxs("select", { style: styles.select, "aria-label": t('usageBadgeDisplay'), value: mode, onChange: event => {
9
+ const value = event.currentTarget.value;
10
+ if (value !== 'recent' && value !== 'hidden')
11
+ return;
12
+ try {
13
+ setUsageBadgeMode(value);
14
+ setFailed(false);
15
+ }
16
+ catch (error) {
17
+ if (!(error instanceof DOMException) || !['SecurityError', 'QuotaExceededError'].includes(error.name))
18
+ throw error;
19
+ setFailed(true);
20
+ }
21
+ }, children: [_jsx("option", { value: "recent", children: t('usageBadgeDisplayRecent') }), _jsx("option", { value: "hidden", children: t('usageBadgeDisplayHidden') })] })] }), _jsx("p", { style: styles.hint, children: t('usageBadgeDisplayHint') }), failed && _jsx("p", { role: "alert", style: { ...styles.hint, color: 'var(--dsw-alias-state-error-primary)' }, children: t('usageBadgeDisplaySaveFailed') })] }));
22
+ }
23
+ const styles = {
24
+ card: {
25
+ border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 12,
26
+ padding: '12px 14px', display: 'flex', flexDirection: 'column', gap: 6,
27
+ },
28
+ field: { display: 'flex', flexDirection: 'column', gap: 4, fontSize: 14, lineHeight: '22px' },
29
+ select: {
30
+ height: 32, width: '100%', boxSizing: 'border-box',
31
+ border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8,
32
+ padding: '0 10px', font: 'inherit',
33
+ background: 'var(--dsw-alias-bg-layer-1)', color: 'var(--dsw-alias-label-primary)',
34
+ },
35
+ hint: { margin: 0, fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-label-tertiary)' },
36
+ };
@@ -0,0 +1,42 @@
1
+ import type { ConnectionHandle } from '@deepseek-ai/dsh-api-remotes/client';
2
+ import type { ToolCallBlock } from '@deepseek-ai/dsh-client-ui-conversation/client';
3
+ import type { SubscriptionsKey } from './locales.js';
4
+ /** Mirror of ui-tool's ToolCallOwnerProps (see ImageGenerateToolview). */
5
+ interface ToolCallOwnerProps {
6
+ callId: string;
7
+ toolName: string;
8
+ block: ToolCallBlock;
9
+ cwd?: string | undefined;
10
+ openFile: (path: string) => void;
11
+ inspect?: (() => void) | undefined;
12
+ }
13
+ /** Decoded video bytes as the node half's `video` endpoint answers them. */
14
+ export interface VideoBytes {
15
+ mediaType: string;
16
+ dataBase64: string;
17
+ }
18
+ /** Injected dependencies of {@link VideoGenerateToolview} (slot `inject`). */
19
+ export interface VideoGenerateToolviewInjected {
20
+ /** Loads one generated video's bytes by bare file name. */
21
+ loadVideo: (name: string) => Promise<VideoBytes>;
22
+ }
23
+ /**
24
+ * Props delivered by the toolview outlet: the owner share plus the inject
25
+ * face and the framework locale seat, spread flat.
26
+ */
27
+ export type VideoGenerateToolviewProps = Partial<ToolCallOwnerProps> & Partial<VideoGenerateToolviewInjected> & {
28
+ t?: ((key: SubscriptionsKey, params?: Record<string, unknown>) => string) | undefined;
29
+ };
30
+ /**
31
+ * Build the video loader over the `/subscriptions-auth` `video` endpoint.
32
+ * @param rpc - Connection RPC caller.
33
+ * @returns loader resolving a bare file name to the decoded bytes.
34
+ */
35
+ export declare function createVideoLoader(rpc: ConnectionHandle['rpc']): (name: string) => Promise<VideoBytes>;
36
+ /**
37
+ * The `video_generate` keyed toolview component.
38
+ * @param props - owner share, inject face, and locale seat (spread flat).
39
+ * @returns the call row plus, once settled, the player / text / error body.
40
+ */
41
+ export declare function VideoGenerateToolview(props: VideoGenerateToolviewProps): import("react").JSX.Element | null;
42
+ export {};