dsh-coding-subscription-oauth 0.5.8 → 0.6.2

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 (148) hide show
  1. package/CHANGELOG.md +217 -188
  2. package/CONTRIBUTING.md +120 -120
  3. package/INSTALL.md +241 -209
  4. package/LICENSE +19 -19
  5. package/NOTICE +11 -11
  6. package/README.de.md +298 -294
  7. package/README.es.md +299 -295
  8. package/README.fr.md +299 -295
  9. package/README.ja.md +299 -295
  10. package/README.ko.md +299 -295
  11. package/README.md +313 -307
  12. package/README.pt-BR.md +299 -295
  13. package/README.ru.md +299 -295
  14. package/README.zh-CN.md +311 -305
  15. package/compatibility/dsh-bom.json +30 -0
  16. package/cordis.patch.yml +13 -13
  17. package/docs/00-project-rules.md +195 -195
  18. package/docs/02-architecture.md +134 -129
  19. package/docs/02-architecture.zh-CN.md +134 -129
  20. package/lib/adapter.d.ts.map +1 -1
  21. package/lib/alias-adapter.d.ts +3 -1
  22. package/lib/alias-adapter.d.ts.map +1 -1
  23. package/lib/auth-routes.d.ts +8 -15
  24. package/lib/auth-routes.d.ts.map +1 -1
  25. package/lib/bin.js +746 -1402
  26. package/lib/bin.js.map +4 -4
  27. package/lib/capability-routes.d.ts +3 -3
  28. package/lib/capability-routes.d.ts.map +1 -1
  29. package/lib/capability-settings.d.ts +5 -4
  30. package/lib/capability-settings.d.ts.map +1 -1
  31. package/lib/client.js +27 -8
  32. package/lib/client.js.map +4 -4
  33. package/lib/compatibility.d.ts +46 -0
  34. package/lib/compatibility.d.ts.map +1 -0
  35. package/lib/dsh-host-adapter.d.ts +18 -0
  36. package/lib/dsh-host-adapter.d.ts.map +1 -0
  37. package/lib/gateway-auth.d.ts +1 -2
  38. package/lib/gateway-auth.d.ts.map +1 -1
  39. package/lib/gateway-routes.d.ts +3 -4
  40. package/lib/gateway-routes.d.ts.map +1 -1
  41. package/lib/gateway.d.ts +4 -0
  42. package/lib/gateway.d.ts.map +1 -1
  43. package/lib/ids.d.ts +3 -32
  44. package/lib/ids.d.ts.map +1 -1
  45. package/lib/imagine-routes.d.ts +2 -0
  46. package/lib/imagine-routes.d.ts.map +1 -1
  47. package/lib/index.d.ts +19 -4
  48. package/lib/index.d.ts.map +1 -1
  49. package/lib/index.js +26627 -1047
  50. package/lib/index.js.map +4 -4
  51. package/lib/invariant.js.map +1 -1
  52. package/lib/oauth-import-routes.d.ts +3 -4
  53. package/lib/oauth-import-routes.d.ts.map +1 -1
  54. package/lib/proxy.d.ts +3 -16
  55. package/lib/proxy.d.ts.map +1 -1
  56. package/lib/web-origin.d.ts +28 -4
  57. package/lib/web-origin.d.ts.map +1 -1
  58. package/lib/web-routes.d.ts.map +1 -1
  59. package/media/en/settings_accounts.png +0 -0
  60. package/media/en/settings_capabilities.png +0 -0
  61. package/media/en/settings_gateway.png +0 -0
  62. package/media/settings_accounts.png +0 -0
  63. package/media/settings_capabilities.png +0 -0
  64. package/media/settings_gateway.png +0 -0
  65. package/media/settings_overview.png +0 -0
  66. package/media/zh-CN/settings_accounts.png +0 -0
  67. package/media/zh-CN/settings_capabilities.png +0 -0
  68. package/media/zh-CN/settings_gateway.png +0 -0
  69. package/package.json +211 -174
  70. package/patches/dsh-agy@0.1.2.patch +25 -25
  71. package/scripts/release.mjs +186 -170
  72. package/scripts/smoke-deployed-routes.mjs +146 -146
  73. package/scripts/verify-deployed-catalog.mjs +87 -87
  74. package/src/adapter.ts +329 -268
  75. package/src/alias-adapter.ts +141 -147
  76. package/src/auth-routes.ts +921 -871
  77. package/src/auth.ts +67 -67
  78. package/src/bin.ts +350 -350
  79. package/src/capability-routes.ts +279 -275
  80. package/src/capability-runtime.ts +313 -313
  81. package/src/capability-settings.ts +658 -657
  82. package/src/capability-tools.ts +666 -666
  83. package/src/catalog.ts +271 -271
  84. package/src/client/GrokBuildSettings.tsx +770 -718
  85. package/src/client/api.ts +88 -88
  86. package/src/client/components/AboutTab.tsx +30 -30
  87. package/src/client/components/AccountsTab.tsx +241 -224
  88. package/src/client/components/Badge.tsx +33 -33
  89. package/src/client/components/CapabilitiesTab.tsx +265 -227
  90. package/src/client/components/CliPullPreview.tsx +116 -107
  91. package/src/client/components/CopyButton.tsx +57 -57
  92. package/src/client/components/GatewayTab.tsx +469 -409
  93. package/src/client/components/NoticeBanner.tsx +46 -46
  94. package/src/client/components/ProgressBar.tsx +53 -53
  95. package/src/client/components/ProviderCard.tsx +606 -502
  96. package/src/client/components/SettingsTabs.tsx +75 -75
  97. package/src/client/components/ToggleSwitch.tsx +71 -62
  98. package/src/client/constants.ts +224 -206
  99. package/src/client/display.ts +61 -61
  100. package/src/client/dshClientAdapter.ts +127 -0
  101. package/src/client/gatewaySnippets.ts +37 -36
  102. package/src/client/index.tsx +156 -37
  103. package/src/client/locales.ts +535 -491
  104. package/src/client/microStyles.ts +52 -33
  105. package/src/client/parsers.ts +396 -386
  106. package/src/client/styles.ts +325 -307
  107. package/src/client/types.ts +197 -185
  108. package/src/codex-http.ts +447 -447
  109. package/src/codex-images.ts +485 -485
  110. package/src/codex-model-capabilities.ts +320 -320
  111. package/src/codex-search.ts +245 -245
  112. package/src/codex-usage.ts +263 -263
  113. package/src/compatibility.ts +55 -0
  114. package/src/dsh-host-adapter.ts +173 -0
  115. package/src/gateway-anthropic-messages.ts +84 -84
  116. package/src/gateway-auth.ts +102 -100
  117. package/src/gateway-backend.ts +274 -274
  118. package/src/gateway-body.ts +49 -49
  119. package/src/gateway-config.ts +76 -76
  120. package/src/gateway-http.ts +104 -104
  121. package/src/gateway-openai-chat.ts +124 -124
  122. package/src/gateway-openai-responses.ts +53 -53
  123. package/src/gateway-parse.ts +224 -224
  124. package/src/gateway-protocol.ts +52 -52
  125. package/src/gateway-routes.ts +158 -152
  126. package/src/gateway.ts +258 -242
  127. package/src/grok-errors.ts +24 -24
  128. package/src/grok-imagine.ts +1627 -1627
  129. package/src/grok-import.ts +151 -151
  130. package/src/http-json.ts +82 -82
  131. package/src/ids.ts +59 -45
  132. package/src/imagine-routes.ts +463 -461
  133. package/src/index.ts +729 -592
  134. package/src/invariant.ts +17 -17
  135. package/src/kimi-errors.ts +26 -26
  136. package/src/media-store.ts +927 -927
  137. package/src/oauth-import-routes.ts +324 -314
  138. package/src/oauth-providers.ts +152 -152
  139. package/src/oauth-session.ts +183 -183
  140. package/src/oauth-sources.ts +1104 -1104
  141. package/src/oauth.ts +620 -620
  142. package/src/provider.ts +128 -128
  143. package/src/proxy.ts +11 -99
  144. package/src/redact.ts +72 -72
  145. package/src/session.ts +218 -218
  146. package/src/store.ts +217 -217
  147. package/src/web-origin.ts +296 -60
  148. package/src/web-routes.ts +38 -75
@@ -1,502 +1,606 @@
1
- /** Single provider account card for the Accounts tab. */
2
-
3
- import { useMemo, useState } from "react";
4
- import { SOURCE_REASON_KEY } from "../constants.ts";
5
- import { methodLabel, orderedLoginMethods, shouldShowPerCardSourceReason } from "../display.ts";
6
- import { formatEpoch, modelFields, usageHasVisibleFields } from "../parsers.ts";
7
- import {
8
- bodyStyle,
9
- buttonStyle,
10
- cardStyle,
11
- checkRowStyle,
12
- codeStyle,
13
- compactButtonStyle,
14
- hintStyle,
15
- inputStyle,
16
- listStyle,
17
- monoStyle,
18
- nestedStyle,
19
- primaryButtonStyle,
20
- rowStyle,
21
- stepActiveStyle,
22
- stepNumberActiveStyle,
23
- stepNumberStyle,
24
- stepRowStyle,
25
- titleStyle,
26
- } from "../styles.ts";
27
- import type {
28
- GrokBuildSettingsInjected,
29
- GrokStatus,
30
- LoginMethod,
31
- ProviderCardDefinition,
32
- ProviderStatus,
33
- SourceStatus,
34
- UsageView,
35
- } from "../types.ts";
36
- import { Badge } from "./Badge.tsx";
37
- import { CopyButton } from "./CopyButton.tsx";
38
- import { ProgressBar } from "./ProgressBar.tsx";
39
-
40
- export interface ProviderCardProps {
41
- t: GrokBuildSettingsInjected["t"];
42
- definition: ProviderCardDefinition;
43
- providerStatus: ProviderStatus;
44
- busy: boolean;
45
- sourcesBusy: boolean;
46
- remote: boolean;
47
- codeInput: string;
48
- popupBlocked: boolean;
49
- expanded: boolean;
50
- source: SourceStatus | undefined;
51
- showUsage: boolean;
52
- usage: UsageView | undefined;
53
- usageError: string | undefined;
54
- usageLoading: boolean;
55
- onSignIn: (method: LoginMethod) => void;
56
- onSignOut: () => void;
57
- onCancelLogin: () => void;
58
- onSubmitCode: () => void;
59
- onCodeChange: (value: string) => void;
60
- onToggleExpanded: () => void;
61
- onPreviewSource: () => void;
62
- onSaveModels: (selected: string[]) => void;
63
- }
64
-
65
- function SignInSteps({
66
- t,
67
- activeMethod,
68
- userCode,
69
- url,
70
- popupBlocked,
71
- }: {
72
- t: GrokBuildSettingsInjected["t"];
73
- activeMethod: LoginMethod;
74
- userCode: string | undefined;
75
- url: string | undefined;
76
- popupBlocked: boolean;
77
- }) {
78
- const hasCode = userCode !== undefined && userCode.length > 0;
79
- const hasUrl = url !== undefined && url.length > 0;
80
- const needsPaste = activeMethod !== "device";
81
-
82
- return (
83
- <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
84
- <div style={hasUrl ? stepActiveStyle : stepRowStyle}>
85
- <span style={hasUrl ? stepNumberActiveStyle : stepNumberStyle} aria-hidden="true">
86
- 1
87
- </span>
88
- <span>{t("signInStepOpen")}</span>
89
- </div>
90
- {hasUrl ? (
91
- <div style={{ display: "flex", flexWrap: "wrap", gap: 8, paddingLeft: 32 }}>
92
- <a href={url} target="_blank" rel="noreferrer" style={primaryButtonStyle}>
93
- {t("openAuthUrl")}
94
- </a>
95
- <CopyButton text={url} idleLabel={t("copy")} copiedLabel={t("copied")} failedLabel={t("copyFailed")} />
96
- </div>
97
- ) : null}
98
- {popupBlocked && hasUrl ? <p style={hintStyle}>{t("popupBlocked")}</p> : null}
99
- {hasCode ? (
100
- <>
101
- <div style={stepActiveStyle}>
102
- <span style={stepNumberActiveStyle} aria-hidden="true">
103
- 2
104
- </span>
105
- <span>{t("signInStepCode")}</span>
106
- </div>
107
- <div style={{ display: "flex", flexWrap: "wrap", alignItems: "center", gap: 8, paddingLeft: 32 }}>
108
- <span style={codeStyle}>{userCode}</span>
109
- <CopyButton
110
- text={userCode}
111
- idleLabel={t("copyUserCode")}
112
- copiedLabel={t("copied")}
113
- failedLabel={t("copyFailed")}
114
- primary
115
- />
116
- </div>
117
- </>
118
- ) : null}
119
- <div style={stepActiveStyle}>
120
- <span style={stepNumberActiveStyle} aria-hidden="true">
121
- {hasCode ? 3 : 2}
122
- </span>
123
- <span style={{ display: "inline-flex", alignItems: "center", gap: 8 }}>
124
- <span
125
- aria-hidden="true"
126
- style={{
127
- width: 14,
128
- height: 14,
129
- border: "2px solid var(--dsw-alias-brand-primary, #1677ff)",
130
- borderTopColor: "transparent",
131
- borderRadius: "50%",
132
- animation: "dsh-coding-oauth-spin 0.8s linear infinite",
133
- }}
134
- />
135
- {t("signInStepWait")}
136
- </span>
137
- </div>
138
- {needsPaste ? (
139
- <div style={{ display: "flex", flexDirection: "column", gap: 8, paddingLeft: 32 }}>
140
- <p style={bodyStyle}>{t(activeMethod === "browser" ? "pasteBrowserCodeHint" : "pasteCodeHint")}</p>
141
- </div>
142
- ) : null}
143
- </div>
144
- );
145
- }
146
-
147
- export function ProviderCard({
148
- t,
149
- definition,
150
- providerStatus,
151
- busy,
152
- sourcesBusy,
153
- remote,
154
- codeInput,
155
- popupBlocked,
156
- expanded,
157
- source,
158
- showUsage,
159
- usage,
160
- usageError,
161
- usageLoading,
162
- onSignIn,
163
- onSignOut,
164
- onCancelLogin,
165
- onSubmitCode,
166
- onCodeChange,
167
- onToggleExpanded,
168
- onPreviewSource,
169
- onSaveModels,
170
- }: ProviderCardProps) {
171
- const [showAltMethods, setShowAltMethods] = useState(false);
172
- const [modelFilter, setModelFilter] = useState("");
173
-
174
- const ordered = orderedLoginMethods(definition, remote);
175
- const primaryMethod: LoginMethod = ordered[0] ?? definition.recommended;
176
- const altMethods = ordered.filter((method) => method !== primaryMethod);
177
-
178
- const statusLabel =
179
- providerStatus.status === "signed-in"
180
- ? t("signedIn")
181
- : providerStatus.status === "signing-in"
182
- ? t("signingIn")
183
- : providerStatus.status === "error"
184
- ? t("requestFailed")
185
- : t("signedOut");
186
- const activeMethod = providerStatus.status === "signing-in" ? providerStatus.method : primaryMethod;
187
- const { available, selected } = modelFields(providerStatus);
188
- const grokProviderStatus = definition.slug === "grok" ? (providerStatus as GrokStatus) : undefined;
189
- const showSourceReason = shouldShowPerCardSourceReason(source);
190
-
191
- const filteredModels = useMemo(() => {
192
- const query = modelFilter.trim().toLowerCase();
193
- if (query.length === 0) return available;
194
- return available.filter((id) => id.toLowerCase().includes(query));
195
- }, [available, modelFilter]);
196
-
197
- const usagePercent =
198
- definition.slug === "codex" && showUsage
199
- ? usage?.individualRemainingPercent === undefined
200
- ? usage?.rateLimits[0]?.windows[0]?.usedPercent
201
- : 100 - usage.individualRemainingPercent
202
- : undefined;
203
- const fetchedAt = formatEpoch(usage?.fetchedAt);
204
-
205
- return (
206
- <div style={cardStyle}>
207
- <div style={rowStyle}>
208
- <div>
209
- <h3 style={{ ...titleStyle, fontSize: 16 }}>{t(definition.titleKey)}</h3>
210
- {providerStatus.status === "signed-in" && !expanded ? (
211
- <p style={{ ...hintStyle, marginTop: 4 }}>
212
- {t("modelsSummary", { selected: selected.length, total: available.length })}
213
- {usagePercent === undefined ? "" : ` · ${t("usageUsedShort", { value: `${String(usagePercent)}%` })}`}
214
- </p>
215
- ) : (
216
- <>
217
- <p style={{ ...bodyStyle, marginTop: 4 }}>{t(definition.descriptionKey)}</p>
218
- <p style={{ ...bodyStyle, marginTop: 4 }}>
219
- <span style={monoStyle}>{definition.route}</span>
220
- </p>
221
- </>
222
- )}
223
- </div>
224
- <Badge label={statusLabel} providerStatus={providerStatus.status} />
225
- </div>
226
- <div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
227
- {providerStatus.status === "signed-in" ? (
228
- <>
229
- <button type="button" style={buttonStyle} disabled={busy} onClick={onSignOut}>
230
- {busy ? t("working") : t("logout")}
231
- </button>
232
- <button type="button" style={buttonStyle} onClick={onToggleExpanded}>
233
- {expanded ? t("collapseModels") : t("expandModels")}
234
- </button>
235
- {source?.available === true ? (
236
- <button type="button" style={buttonStyle} disabled={sourcesBusy} onClick={onPreviewSource}>
237
- {t("sourcesPullCopy")}
238
- </button>
239
- ) : null}
240
- </>
241
- ) : providerStatus.status === "signing-in" ? (
242
- <>
243
- <button type="button" style={buttonStyle} disabled={busy} onClick={onCancelLogin}>
244
- {t("cancelLogin")}
245
- </button>
246
- {altMethods.length > 0 ? (
247
- <button
248
- type="button"
249
- style={compactButtonStyle}
250
- disabled={busy}
251
- onClick={() => {
252
- setShowAltMethods((current) => !current);
253
- }}
254
- >
255
- {showAltMethods ? t("hideOtherLoginMethods") : t("otherLoginMethods")}
256
- </button>
257
- ) : null}
258
- {showAltMethods
259
- ? altMethods.map((method) => (
260
- <button
261
- key={method}
262
- type="button"
263
- style={compactButtonStyle}
264
- disabled={busy}
265
- onClick={() => {
266
- onSignIn(method);
267
- }}
268
- >
269
- {methodLabel(method, t, { remote, primary: false })}
270
- </button>
271
- ))
272
- : null}
273
- </>
274
- ) : (
275
- <>
276
- <button
277
- type="button"
278
- style={primaryButtonStyle}
279
- disabled={busy}
280
- onClick={() => {
281
- onSignIn(primaryMethod);
282
- }}
283
- >
284
- {busy ? t("working") : methodLabel(primaryMethod, t, { remote, primary: true })}
285
- </button>
286
- {altMethods.length > 0 ? (
287
- <button
288
- type="button"
289
- style={compactButtonStyle}
290
- disabled={busy}
291
- onClick={() => {
292
- setShowAltMethods((current) => !current);
293
- }}
294
- >
295
- {showAltMethods ? t("hideOtherLoginMethods") : t("otherLoginMethods")}
296
- </button>
297
- ) : null}
298
- {showAltMethods
299
- ? altMethods.map((method) => (
300
- <button
301
- key={method}
302
- type="button"
303
- style={compactButtonStyle}
304
- disabled={busy}
305
- onClick={() => {
306
- onSignIn(method);
307
- }}
308
- >
309
- {methodLabel(method, t, { remote, primary: false })}
310
- </button>
311
- ))
312
- : null}
313
- {source?.available === true ? (
314
- <button type="button" style={buttonStyle} disabled={sourcesBusy} onClick={onPreviewSource}>
315
- {t("sourcesPullCopy")}
316
- </button>
317
- ) : showSourceReason && source?.reason !== undefined ? (
318
- <span style={hintStyle}>{t(SOURCE_REASON_KEY[source.reason])}</span>
319
- ) : null}
320
- </>
321
- )}
322
- </div>
323
- {providerStatus.status === "error" ? (
324
- <p style={{ ...bodyStyle, color: "var(--dsw-alias-state-error-primary)" }}>{providerStatus.message}</p>
325
- ) : null}
326
- {providerStatus.status === "signing-in" ? (
327
- <SignInSteps
328
- t={t}
329
- activeMethod={activeMethod}
330
- userCode={providerStatus.userCode}
331
- url={providerStatus.url}
332
- popupBlocked={popupBlocked}
333
- />
334
- ) : null}
335
- {providerStatus.status === "signing-in" && activeMethod !== "device" ? (
336
- <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
337
- <div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
338
- <input
339
- style={{ ...inputStyle, flex: "1 1 360px" }}
340
- value={codeInput}
341
- placeholder={t("pasteCodePlaceholder")}
342
- disabled={busy}
343
- onChange={(event) => {
344
- onCodeChange(event.target.value);
345
- }}
346
- onKeyDown={(event) => {
347
- if (event.key === "Enter") {
348
- event.preventDefault();
349
- onSubmitCode();
350
- }
351
- }}
352
- />
353
- <button
354
- type="button"
355
- style={primaryButtonStyle}
356
- disabled={busy || codeInput.trim().length === 0}
357
- onClick={onSubmitCode}
358
- >
359
- {t("submitCode")}
360
- </button>
361
- </div>
362
- </div>
363
- ) : null}
364
- {providerStatus.status === "signed-in" && expanded ? (
365
- <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
366
- <div style={rowStyle}>
367
- <h4 style={{ ...titleStyle, fontSize: 14 }}>{t("models")}</h4>
368
- <div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
369
- <button
370
- type="button"
371
- style={compactButtonStyle}
372
- disabled={busy}
373
- onClick={() => {
374
- onSaveModels([]);
375
- }}
376
- >
377
- {t("deselectAll")}
378
- </button>
379
- <button
380
- type="button"
381
- style={compactButtonStyle}
382
- disabled={busy}
383
- onClick={() => {
384
- onSaveModels([...available]);
385
- }}
386
- >
387
- {t("selectAll")}
388
- </button>
389
- <button
390
- type="button"
391
- style={compactButtonStyle}
392
- disabled={busy}
393
- onClick={() => {
394
- onSaveModels(available.length > 0 ? [available[0]!] : []);
395
- }}
396
- >
397
- {t("resetModelsDefault")}
398
- </button>
399
- </div>
400
- </div>
401
- <input
402
- type="search"
403
- style={inputStyle}
404
- value={modelFilter}
405
- placeholder={t("modelFilterPlaceholder")}
406
- disabled={busy}
407
- onChange={(event) => {
408
- setModelFilter(event.target.value);
409
- }}
410
- />
411
- {grokProviderStatus?.status === "signed-in" ? (
412
- <p style={bodyStyle}>
413
- {grokProviderStatus.catalogSource === "live"
414
- ? t("catalogLive")
415
- : grokProviderStatus.catalogSource === "cache"
416
- ? t("catalogCache")
417
- : t("catalogFallback")}
418
- </p>
419
- ) : null}
420
- <p style={bodyStyle}>
421
- {t("modelHint")} <span style={monoStyle}>{definition.route}/&lt;id&gt;</span>
422
- </p>
423
- <ul style={listStyle}>
424
- {filteredModels.map((id) => {
425
- const checked = selected.includes(id);
426
- return (
427
- <li key={id}>
428
- <label style={checkRowStyle}>
429
- <input
430
- type="checkbox"
431
- checked={checked}
432
- disabled={busy}
433
- onChange={() => {
434
- const current = new Set(selected);
435
- if (checked) current.delete(id);
436
- else current.add(id);
437
- onSaveModels([...current]);
438
- }}
439
- />
440
- <span style={monoStyle}>{id}</span>
441
- </label>
442
- </li>
443
- );
444
- })}
445
- </ul>
446
- {filteredModels.length === 0 ? <p style={hintStyle}>{t("modelFilterPlaceholder")}</p> : null}
447
- {grokProviderStatus?.status === "signed-in" && grokProviderStatus.catalogError !== undefined ? (
448
- <p style={{ ...bodyStyle, color: "var(--dsw-alias-state-error-primary)" }}>{t("catalogError")}</p>
449
- ) : null}
450
- {definition.slug === "codex" && showUsage ? (
451
- <div style={nestedStyle}>
452
- <p style={{ ...bodyStyle, color: "var(--dsw-alias-label-primary)" }}>{t("usageTitle")}</p>
453
- {usageError === undefined ? null : (
454
- <p style={{ ...bodyStyle, color: "var(--dsw-alias-state-error-primary)" }} role="alert">
455
- {usageError}
456
- </p>
457
- )}
458
- {usageLoading && usage === undefined ? (
459
- <p style={hintStyle}>{t("usageLoading")}</p>
460
- ) : usage === undefined || !usageHasVisibleFields(usage) ? (
461
- <p style={hintStyle}>{t("usageEmpty")}</p>
462
- ) : (
463
- <>
464
- {fetchedAt === undefined ? null : <p style={hintStyle}>{t("usageFetchedAt", { time: fetchedAt })}</p>}
465
- {usagePercent !== undefined ? (
466
- <ProgressBar
467
- value={usagePercent}
468
- label={t("usageRateLimit")}
469
- meta={t("usageUsed", { value: `${String(usagePercent)}%` })}
470
- />
471
- ) : null}
472
- {usage.rateLimits.map((limit) => {
473
- const window = limit.windows[0];
474
- const used = window?.usedPercent;
475
- const resetsAt = formatEpoch(window?.resetsAt);
476
- return (
477
- <div key={limit.id} style={{ display: "flex", flexDirection: "column", gap: 4 }}>
478
- {used === undefined ? (
479
- <p style={hintStyle}>{limit.name ?? t("usageRateLimit")}</p>
480
- ) : (
481
- <ProgressBar
482
- value={used}
483
- label={limit.name ?? t("usageRateLimit")}
484
- meta={
485
- resetsAt === undefined
486
- ? t("usageUsed", { value: `${String(used)}%` })
487
- : `${t("usageUsed", { value: `${String(used)}%` })} · ${t("usageResets", { time: resetsAt })}`
488
- }
489
- />
490
- )}
491
- </div>
492
- );
493
- })}
494
- </>
495
- )}
496
- </div>
497
- ) : null}
498
- </div>
499
- ) : null}
500
- </div>
501
- );
502
- }
1
+ /** Single provider account card for the Accounts tab. */
2
+
3
+ import { useEffect, useMemo, useRef, useState } from "react";
4
+ import { SOURCE_REASON_KEY } from "../constants.ts";
5
+ import { methodLabel, orderedLoginMethods, shouldShowPerCardSourceReason } from "../display.ts";
6
+ import { formatEpoch, modelFields, usageHasVisibleFields } from "../parsers.ts";
7
+ import {
8
+ bodyStyle,
9
+ buttonStyle,
10
+ cardStyle,
11
+ checkRowStyle,
12
+ codeStyle,
13
+ compactButtonStyle,
14
+ hintStyle,
15
+ inputStyle,
16
+ listStyle,
17
+ monoStyle,
18
+ nestedStyle,
19
+ primaryButtonStyle,
20
+ rowStyle,
21
+ stepActiveStyle,
22
+ stepNumberActiveStyle,
23
+ stepNumberStyle,
24
+ stepRowStyle,
25
+ titleStyle,
26
+ visuallyHiddenStyle,
27
+ } from "../styles.ts";
28
+ import type {
29
+ GrokBuildSettingsInjected,
30
+ GrokStatus,
31
+ LoginMethod,
32
+ ProviderCardDefinition,
33
+ ProviderStatus,
34
+ SourceStatus,
35
+ UsageView,
36
+ } from "../types.ts";
37
+ import { Badge } from "./Badge.tsx";
38
+ import { CopyButton } from "./CopyButton.tsx";
39
+ import { ProgressBar } from "./ProgressBar.tsx";
40
+
41
+ export interface ProviderCardProps {
42
+ t: GrokBuildSettingsInjected["t"];
43
+ definition: ProviderCardDefinition;
44
+ providerStatus: ProviderStatus;
45
+ busy: boolean;
46
+ sourcesBusy: boolean;
47
+ remote: boolean;
48
+ codeInput: string;
49
+ popupBlocked: boolean;
50
+ expanded: boolean;
51
+ source: SourceStatus | undefined;
52
+ showUsage: boolean;
53
+ usage: UsageView | undefined;
54
+ usageError: string | undefined;
55
+ usageLoading: boolean;
56
+ onSignIn: (method: LoginMethod) => void;
57
+ onSignOut: () => void;
58
+ onCancelLogin: () => void;
59
+ onSubmitCode: () => void;
60
+ onCodeChange: (value: string) => void;
61
+ onToggleExpanded: () => void;
62
+ onPreviewSource: () => void;
63
+ onSaveModels: (selected: string[]) => void;
64
+ }
65
+
66
+ function SignInSteps({
67
+ t,
68
+ activeMethod,
69
+ userCode,
70
+ url,
71
+ popupBlocked,
72
+ }: {
73
+ t: GrokBuildSettingsInjected["t"];
74
+ activeMethod: LoginMethod;
75
+ userCode: string | undefined;
76
+ url: string | undefined;
77
+ popupBlocked: boolean;
78
+ }) {
79
+ const hasCode = userCode !== undefined && userCode.length > 0;
80
+ const hasUrl = url !== undefined && url.length > 0;
81
+ const needsPaste = activeMethod !== "device";
82
+
83
+ return (
84
+ <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
85
+ <div style={hasUrl ? stepActiveStyle : stepRowStyle}>
86
+ <span style={hasUrl ? stepNumberActiveStyle : stepNumberStyle} aria-hidden="true">
87
+ 1
88
+ </span>
89
+ <span>{t("signInStepOpen")}</span>
90
+ </div>
91
+ {hasUrl ? (
92
+ <div style={{ display: "flex", flexWrap: "wrap", gap: 8, paddingLeft: 32 }}>
93
+ <a href={url} target="_blank" rel="noreferrer" style={primaryButtonStyle}>
94
+ {t("openAuthUrl")}
95
+ </a>
96
+ <CopyButton text={url} idleLabel={t("copy")} copiedLabel={t("copied")} failedLabel={t("copyFailed")} />
97
+ </div>
98
+ ) : null}
99
+ {popupBlocked && hasUrl ? <p style={hintStyle}>{t("popupBlocked")}</p> : null}
100
+ {hasCode ? (
101
+ <>
102
+ <div style={stepActiveStyle}>
103
+ <span style={stepNumberActiveStyle} aria-hidden="true">
104
+ 2
105
+ </span>
106
+ <span>{t("signInStepCode")}</span>
107
+ </div>
108
+ <div style={{ display: "flex", flexWrap: "wrap", alignItems: "center", gap: 8, paddingLeft: 32 }}>
109
+ <span style={codeStyle}>{userCode}</span>
110
+ <CopyButton
111
+ text={userCode}
112
+ idleLabel={t("copyUserCode")}
113
+ copiedLabel={t("copied")}
114
+ failedLabel={t("copyFailed")}
115
+ primary
116
+ />
117
+ </div>
118
+ </>
119
+ ) : null}
120
+ <div style={stepActiveStyle}>
121
+ <span style={stepNumberActiveStyle} aria-hidden="true">
122
+ {hasCode ? 3 : 2}
123
+ </span>
124
+ <span style={{ display: "inline-flex", alignItems: "center", gap: 8 }}>
125
+ <span
126
+ aria-hidden="true"
127
+ style={{
128
+ width: 14,
129
+ height: 14,
130
+ border: "2px solid var(--dsw-alias-brand-primary, #1677ff)",
131
+ borderTopColor: "transparent",
132
+ borderRadius: "50%",
133
+ animation: "dsh-coding-oauth-spin 0.8s linear infinite",
134
+ }}
135
+ />
136
+ {t("signInStepWait")}
137
+ </span>
138
+ </div>
139
+ {needsPaste ? (
140
+ <div style={{ display: "flex", flexDirection: "column", gap: 8, paddingLeft: 32 }}>
141
+ <p style={bodyStyle}>{t(activeMethod === "browser" ? "pasteBrowserCodeHint" : "pasteCodeHint")}</p>
142
+ </div>
143
+ ) : null}
144
+ </div>
145
+ );
146
+ }
147
+
148
+ export function ProviderCard({
149
+ t,
150
+ definition,
151
+ providerStatus,
152
+ busy,
153
+ sourcesBusy,
154
+ remote,
155
+ codeInput,
156
+ popupBlocked,
157
+ expanded,
158
+ source,
159
+ showUsage,
160
+ usage,
161
+ usageError,
162
+ usageLoading,
163
+ onSignIn,
164
+ onSignOut,
165
+ onCancelLogin,
166
+ onSubmitCode,
167
+ onCodeChange,
168
+ onToggleExpanded,
169
+ onPreviewSource,
170
+ onSaveModels,
171
+ }: ProviderCardProps) {
172
+ const [showAltMethods, setShowAltMethods] = useState(false);
173
+ const [modelFilter, setModelFilter] = useState("");
174
+ const [logoutConfirm, setLogoutConfirm] = useState(false);
175
+ const logoutTrigger = useRef<HTMLButtonElement>(null);
176
+ const logoutConfirmAction = useRef<HTMLButtonElement>(null);
177
+ const restoreLogoutFocus = useRef(false);
178
+
179
+ const ordered = orderedLoginMethods(definition, remote);
180
+ const primaryMethod: LoginMethod = ordered[0] ?? definition.recommended;
181
+ const altMethods = ordered.filter((method) => method !== primaryMethod);
182
+
183
+ const statusLabel =
184
+ providerStatus.status === "signed-in"
185
+ ? t("signedIn")
186
+ : providerStatus.status === "signing-in"
187
+ ? t("signingIn")
188
+ : providerStatus.status === "error"
189
+ ? t("requestFailed")
190
+ : t("signedOut");
191
+ const activeMethod = providerStatus.status === "signing-in" ? providerStatus.method : primaryMethod;
192
+ const { available, selected } = useMemo(() => modelFields(providerStatus), [providerStatus]);
193
+ const [modelDraft, setModelDraft] = useState<string[]>(selected);
194
+ useEffect(() => {
195
+ setModelDraft(selected);
196
+ }, [selected]);
197
+ useEffect(() => {
198
+ if (providerStatus.status !== "signed-in") setLogoutConfirm(false);
199
+ }, [providerStatus.status]);
200
+ useEffect(() => {
201
+ if (logoutConfirm) {
202
+ logoutConfirmAction.current?.focus();
203
+ return;
204
+ }
205
+ if (!restoreLogoutFocus.current) return;
206
+ restoreLogoutFocus.current = false;
207
+ if (providerStatus.status === "signed-in") {
208
+ logoutTrigger.current?.focus();
209
+ return;
210
+ }
211
+ document.getElementById(`coding-oauth-login-${definition.slug}`)?.focus();
212
+ }, [definition.slug, logoutConfirm, providerStatus.status]);
213
+ const modelDraftDirty =
214
+ modelDraft.length !== selected.length || modelDraft.some((model, index) => model !== selected[index]);
215
+ const grokProviderStatus = definition.slug === "grok" ? (providerStatus as GrokStatus) : undefined;
216
+ const showSourceReason = shouldShowPerCardSourceReason(source);
217
+
218
+ const filteredModels = useMemo(() => {
219
+ const query = modelFilter.trim().toLowerCase();
220
+ if (query.length === 0) return available;
221
+ return available.filter((id) => id.toLowerCase().includes(query));
222
+ }, [available, modelFilter]);
223
+
224
+ const usagePercent =
225
+ definition.slug === "codex" && showUsage
226
+ ? usage?.individualRemainingPercent === undefined
227
+ ? usage?.rateLimits[0]?.windows[0]?.usedPercent
228
+ : 100 - usage.individualRemainingPercent
229
+ : undefined;
230
+ const fetchedAt = formatEpoch(usage?.fetchedAt);
231
+
232
+ return (
233
+ <div style={cardStyle}>
234
+ <div style={rowStyle}>
235
+ <div>
236
+ <h3 style={{ ...titleStyle, fontSize: 16 }}>{t(definition.titleKey)}</h3>
237
+ {providerStatus.status === "signed-in" && !expanded ? (
238
+ <p style={{ ...hintStyle, marginTop: 4 }}>
239
+ {t("modelsSummary", { selected: selected.length, total: available.length })}
240
+ {usagePercent === undefined ? "" : ` · ${t("usageUsedShort", { value: `${String(usagePercent)}%` })}`}
241
+ </p>
242
+ ) : (
243
+ <>
244
+ <p style={{ ...bodyStyle, marginTop: 4 }}>{t(definition.descriptionKey)}</p>
245
+ <p style={{ ...bodyStyle, marginTop: 4 }}>
246
+ <span style={monoStyle}>{definition.route}</span>
247
+ </p>
248
+ </>
249
+ )}
250
+ </div>
251
+ <Badge label={statusLabel} providerStatus={providerStatus.status} />
252
+ </div>
253
+ <div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
254
+ {providerStatus.status === "signed-in" ? (
255
+ <>
256
+ {logoutConfirm ? (
257
+ <fieldset style={{ ...nestedStyle, margin: 0, minWidth: 0 }}>
258
+ <legend style={visuallyHiddenStyle}>{t("logoutConfirmTitle")}</legend>
259
+ <p style={bodyStyle}>{t("logoutConfirmHint")}</p>
260
+ <div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
261
+ <button
262
+ ref={logoutConfirmAction}
263
+ type="button"
264
+ style={primaryButtonStyle}
265
+ disabled={busy}
266
+ onClick={() => {
267
+ restoreLogoutFocus.current = true;
268
+ onSignOut();
269
+ }}
270
+ >
271
+ {busy ? t("working") : t("logoutConfirmAction")}
272
+ </button>
273
+ <button
274
+ type="button"
275
+ style={buttonStyle}
276
+ disabled={busy}
277
+ onClick={() => {
278
+ restoreLogoutFocus.current = true;
279
+ setLogoutConfirm(false);
280
+ }}
281
+ >
282
+ {t("cancel")}
283
+ </button>
284
+ </div>
285
+ </fieldset>
286
+ ) : (
287
+ <button
288
+ ref={logoutTrigger}
289
+ type="button"
290
+ style={buttonStyle}
291
+ disabled={busy}
292
+ onClick={() => setLogoutConfirm(true)}
293
+ >
294
+ {t("logout")}
295
+ </button>
296
+ )}
297
+ <button
298
+ id={`coding-oauth-models-toggle-${definition.slug}`}
299
+ type="button"
300
+ style={buttonStyle}
301
+ aria-expanded={expanded}
302
+ aria-controls={expanded ? `coding-oauth-models-${definition.slug}` : undefined}
303
+ onClick={onToggleExpanded}
304
+ >
305
+ {expanded ? t("collapseModels") : t("expandModels")}
306
+ </button>
307
+ {source?.available === true ? (
308
+ <button
309
+ id={`coding-oauth-source-pull-${definition.slug}`}
310
+ type="button"
311
+ style={buttonStyle}
312
+ disabled={sourcesBusy}
313
+ onClick={onPreviewSource}
314
+ >
315
+ {t("sourcesPullCopy")}
316
+ </button>
317
+ ) : null}
318
+ </>
319
+ ) : providerStatus.status === "signing-in" ? (
320
+ <>
321
+ <button type="button" style={buttonStyle} disabled={busy} onClick={onCancelLogin}>
322
+ {t("cancelLogin")}
323
+ </button>
324
+ {altMethods.length > 0 ? (
325
+ <button
326
+ type="button"
327
+ style={compactButtonStyle}
328
+ disabled={busy}
329
+ onClick={() => {
330
+ setShowAltMethods((current) => !current);
331
+ }}
332
+ >
333
+ {showAltMethods ? t("hideOtherLoginMethods") : t("otherLoginMethods")}
334
+ </button>
335
+ ) : null}
336
+ {showAltMethods
337
+ ? altMethods.map((method) => (
338
+ <button
339
+ key={method}
340
+ type="button"
341
+ style={compactButtonStyle}
342
+ disabled={busy}
343
+ onClick={() => {
344
+ onSignIn(method);
345
+ }}
346
+ >
347
+ {methodLabel(method, t, { remote, primary: false })}
348
+ </button>
349
+ ))
350
+ : null}
351
+ </>
352
+ ) : (
353
+ <>
354
+ <button
355
+ id={`coding-oauth-login-${definition.slug}`}
356
+ type="button"
357
+ style={primaryButtonStyle}
358
+ disabled={busy}
359
+ onClick={() => {
360
+ onSignIn(primaryMethod);
361
+ }}
362
+ >
363
+ {busy ? t("working") : methodLabel(primaryMethod, t, { remote, primary: true })}
364
+ </button>
365
+ {altMethods.length > 0 ? (
366
+ <button
367
+ type="button"
368
+ style={compactButtonStyle}
369
+ disabled={busy}
370
+ onClick={() => {
371
+ setShowAltMethods((current) => !current);
372
+ }}
373
+ >
374
+ {showAltMethods ? t("hideOtherLoginMethods") : t("otherLoginMethods")}
375
+ </button>
376
+ ) : null}
377
+ {showAltMethods
378
+ ? altMethods.map((method) => (
379
+ <button
380
+ key={method}
381
+ type="button"
382
+ style={compactButtonStyle}
383
+ disabled={busy}
384
+ onClick={() => {
385
+ onSignIn(method);
386
+ }}
387
+ >
388
+ {methodLabel(method, t, { remote, primary: false })}
389
+ </button>
390
+ ))
391
+ : null}
392
+ {source?.available === true ? (
393
+ <button
394
+ id={`coding-oauth-source-pull-${definition.slug}`}
395
+ type="button"
396
+ style={buttonStyle}
397
+ disabled={sourcesBusy}
398
+ onClick={onPreviewSource}
399
+ >
400
+ {t("sourcesPullCopy")}
401
+ </button>
402
+ ) : showSourceReason && source?.reason !== undefined ? (
403
+ <span style={hintStyle}>{t(SOURCE_REASON_KEY[source.reason])}</span>
404
+ ) : null}
405
+ </>
406
+ )}
407
+ </div>
408
+ {providerStatus.status === "error" ? (
409
+ <p style={{ ...bodyStyle, color: "var(--dsw-alias-state-error-primary)" }}>{providerStatus.message}</p>
410
+ ) : null}
411
+ {providerStatus.status === "signing-in" ? (
412
+ <SignInSteps
413
+ t={t}
414
+ activeMethod={activeMethod}
415
+ userCode={providerStatus.userCode}
416
+ url={providerStatus.url}
417
+ popupBlocked={popupBlocked}
418
+ />
419
+ ) : null}
420
+ {providerStatus.status === "signing-in" && activeMethod !== "device" ? (
421
+ <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
422
+ <div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
423
+ <label htmlFor={`coding-oauth-code-${definition.slug}`} style={visuallyHiddenStyle}>
424
+ {t("pasteCodeLabel")}
425
+ </label>
426
+ <input
427
+ id={`coding-oauth-code-${definition.slug}`}
428
+ style={{ ...inputStyle, flex: "1 1 360px" }}
429
+ value={codeInput}
430
+ placeholder={t("pasteCodePlaceholder")}
431
+ disabled={busy}
432
+ onChange={(event) => {
433
+ onCodeChange(event.target.value);
434
+ }}
435
+ onKeyDown={(event) => {
436
+ if (event.key === "Enter") {
437
+ event.preventDefault();
438
+ onSubmitCode();
439
+ }
440
+ }}
441
+ />
442
+ <button
443
+ type="button"
444
+ style={primaryButtonStyle}
445
+ disabled={busy || codeInput.trim().length === 0}
446
+ onClick={onSubmitCode}
447
+ >
448
+ {t("submitCode")}
449
+ </button>
450
+ </div>
451
+ </div>
452
+ ) : null}
453
+ {providerStatus.status === "signed-in" && expanded ? (
454
+ <div id={`coding-oauth-models-${definition.slug}`} style={{ display: "flex", flexDirection: "column", gap: 8 }}>
455
+ <div style={rowStyle}>
456
+ <h4 style={{ ...titleStyle, fontSize: 14 }}>{t("models")}</h4>
457
+ <div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
458
+ <button
459
+ type="button"
460
+ style={compactButtonStyle}
461
+ disabled={busy}
462
+ onClick={() => {
463
+ setModelDraft([]);
464
+ }}
465
+ >
466
+ {t("deselectAll")}
467
+ </button>
468
+ <button
469
+ type="button"
470
+ style={compactButtonStyle}
471
+ disabled={busy}
472
+ onClick={() => {
473
+ setModelDraft([...available]);
474
+ }}
475
+ >
476
+ {t("selectAll")}
477
+ </button>
478
+ <button
479
+ type="button"
480
+ style={compactButtonStyle}
481
+ disabled={busy}
482
+ onClick={() => {
483
+ setModelDraft(available.length > 0 ? [available[0]!] : []);
484
+ }}
485
+ >
486
+ {t("resetModelsDefault")}
487
+ </button>
488
+ </div>
489
+ </div>
490
+ <label htmlFor={`coding-oauth-model-filter-${definition.slug}`} style={visuallyHiddenStyle}>
491
+ {t("modelFilterLabel", { provider: t(definition.titleKey) })}
492
+ </label>
493
+ <input
494
+ id={`coding-oauth-model-filter-${definition.slug}`}
495
+ type="search"
496
+ style={inputStyle}
497
+ value={modelFilter}
498
+ placeholder={t("modelFilterPlaceholder")}
499
+ disabled={busy}
500
+ onChange={(event) => {
501
+ setModelFilter(event.target.value);
502
+ }}
503
+ />
504
+ {grokProviderStatus?.status === "signed-in" ? (
505
+ <p style={bodyStyle}>
506
+ {grokProviderStatus.catalogSource === "live"
507
+ ? t("catalogLive")
508
+ : grokProviderStatus.catalogSource === "cache"
509
+ ? t("catalogCache")
510
+ : t("catalogFallback")}
511
+ </p>
512
+ ) : null}
513
+ <p style={bodyStyle}>
514
+ {t("modelHint")} <span style={monoStyle}>{definition.route}/&lt;id&gt;</span>
515
+ </p>
516
+ <ul style={listStyle}>
517
+ {filteredModels.map((id) => {
518
+ const checked = modelDraft.includes(id);
519
+ return (
520
+ <li key={id}>
521
+ <label style={checkRowStyle}>
522
+ <input
523
+ type="checkbox"
524
+ checked={checked}
525
+ disabled={busy}
526
+ onChange={() => {
527
+ const current = new Set(modelDraft);
528
+ if (checked) current.delete(id);
529
+ else current.add(id);
530
+ setModelDraft([...available].filter((model) => current.has(model)));
531
+ }}
532
+ />
533
+ <span style={monoStyle}>{id}</span>
534
+ </label>
535
+ </li>
536
+ );
537
+ })}
538
+ </ul>
539
+ <div style={{ display: "flex", flexWrap: "wrap", alignItems: "center", gap: 8 }}>
540
+ <button
541
+ type="button"
542
+ style={primaryButtonStyle}
543
+ disabled={busy || !modelDraftDirty}
544
+ onClick={() => onSaveModels(modelDraft)}
545
+ >
546
+ {busy ? t("working") : t("applyModelDraft")}
547
+ </button>
548
+ {modelDraftDirty ? <span style={hintStyle}>{t("modelDraftPending")}</span> : null}
549
+ </div>
550
+ {filteredModels.length === 0 ? <p style={hintStyle}>{t("modelFilterPlaceholder")}</p> : null}
551
+ {grokProviderStatus?.status === "signed-in" && grokProviderStatus.catalogError !== undefined ? (
552
+ <p style={{ ...bodyStyle, color: "var(--dsw-alias-state-error-primary)" }}>{t("catalogError")}</p>
553
+ ) : null}
554
+ {definition.slug === "codex" && showUsage ? (
555
+ <div style={nestedStyle}>
556
+ <p style={{ ...bodyStyle, color: "var(--dsw-alias-label-primary)" }}>{t("usageTitle")}</p>
557
+ {usageError === undefined ? null : (
558
+ <p style={{ ...bodyStyle, color: "var(--dsw-alias-state-error-primary)" }} role="alert">
559
+ {usageError}
560
+ </p>
561
+ )}
562
+ {usageLoading && usage === undefined ? (
563
+ <p style={hintStyle}>{t("usageLoading")}</p>
564
+ ) : usage === undefined || !usageHasVisibleFields(usage) ? (
565
+ <p style={hintStyle}>{t("usageEmpty")}</p>
566
+ ) : (
567
+ <>
568
+ {fetchedAt === undefined ? null : <p style={hintStyle}>{t("usageFetchedAt", { time: fetchedAt })}</p>}
569
+ {usagePercent !== undefined ? (
570
+ <ProgressBar
571
+ value={usagePercent}
572
+ label={t("usageRateLimit")}
573
+ meta={t("usageUsed", { value: `${String(usagePercent)}%` })}
574
+ />
575
+ ) : null}
576
+ {usage.rateLimits.map((limit) => {
577
+ const window = limit.windows[0];
578
+ const used = window?.usedPercent;
579
+ const resetsAt = formatEpoch(window?.resetsAt);
580
+ return (
581
+ <div key={limit.id} style={{ display: "flex", flexDirection: "column", gap: 4 }}>
582
+ {used === undefined ? (
583
+ <p style={hintStyle}>{limit.name ?? t("usageRateLimit")}</p>
584
+ ) : (
585
+ <ProgressBar
586
+ value={used}
587
+ label={limit.name ?? t("usageRateLimit")}
588
+ meta={
589
+ resetsAt === undefined
590
+ ? t("usageUsed", { value: `${String(used)}%` })
591
+ : `${t("usageUsed", { value: `${String(used)}%` })} · ${t("usageResets", { time: resetsAt })}`
592
+ }
593
+ />
594
+ )}
595
+ </div>
596
+ );
597
+ })}
598
+ </>
599
+ )}
600
+ </div>
601
+ ) : null}
602
+ </div>
603
+ ) : null}
604
+ </div>
605
+ );
606
+ }