dsh-coding-subscription-oauth 0.5.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 (178) hide show
  1. package/CHANGELOG.md +136 -0
  2. package/CONTRIBUTING.md +109 -0
  3. package/INSTALL.md +220 -0
  4. package/LICENSE +19 -0
  5. package/NOTICE +11 -0
  6. package/README.de.md +254 -0
  7. package/README.es.md +254 -0
  8. package/README.fr.md +254 -0
  9. package/README.ja.md +254 -0
  10. package/README.ko.md +254 -0
  11. package/README.md +286 -0
  12. package/README.pt-BR.md +254 -0
  13. package/README.ru.md +254 -0
  14. package/README.zh-CN.md +271 -0
  15. package/cordis.patch.yml +13 -0
  16. package/docs/00-project-rules.md +195 -0
  17. package/docs/02-architecture.md +132 -0
  18. package/docs/02-architecture.zh-CN.md +128 -0
  19. package/lib/adapter.d.ts +24 -0
  20. package/lib/adapter.d.ts.map +1 -0
  21. package/lib/alias-adapter.d.ts +43 -0
  22. package/lib/alias-adapter.d.ts.map +1 -0
  23. package/lib/auth-routes.d.ts +155 -0
  24. package/lib/auth-routes.d.ts.map +1 -0
  25. package/lib/auth.d.ts +29 -0
  26. package/lib/auth.d.ts.map +1 -0
  27. package/lib/bin.d.ts +6 -0
  28. package/lib/bin.d.ts.map +1 -0
  29. package/lib/bin.js +27591 -0
  30. package/lib/bin.js.map +7 -0
  31. package/lib/capability-routes.d.ts +43 -0
  32. package/lib/capability-routes.d.ts.map +1 -0
  33. package/lib/capability-runtime.d.ts +56 -0
  34. package/lib/capability-runtime.d.ts.map +1 -0
  35. package/lib/capability-settings.d.ts +263 -0
  36. package/lib/capability-settings.d.ts.map +1 -0
  37. package/lib/capability-tools.d.ts +50 -0
  38. package/lib/capability-tools.d.ts.map +1 -0
  39. package/lib/catalog.d.ts +53 -0
  40. package/lib/catalog.d.ts.map +1 -0
  41. package/lib/client.js +3 -0
  42. package/lib/client.js.map +7 -0
  43. package/lib/codex-http.d.ts +77 -0
  44. package/lib/codex-http.d.ts.map +1 -0
  45. package/lib/codex-images.d.ts +109 -0
  46. package/lib/codex-images.d.ts.map +1 -0
  47. package/lib/codex-model-capabilities.d.ts +112 -0
  48. package/lib/codex-model-capabilities.d.ts.map +1 -0
  49. package/lib/codex-search.d.ts +96 -0
  50. package/lib/codex-search.d.ts.map +1 -0
  51. package/lib/codex-usage.d.ts +79 -0
  52. package/lib/codex-usage.d.ts.map +1 -0
  53. package/lib/gateway-anthropic-messages.d.ts +8 -0
  54. package/lib/gateway-anthropic-messages.d.ts.map +1 -0
  55. package/lib/gateway-auth.d.ts +22 -0
  56. package/lib/gateway-auth.d.ts.map +1 -0
  57. package/lib/gateway-backend.d.ts +62 -0
  58. package/lib/gateway-backend.d.ts.map +1 -0
  59. package/lib/gateway-body.d.ts +9 -0
  60. package/lib/gateway-body.d.ts.map +1 -0
  61. package/lib/gateway-config.d.ts +24 -0
  62. package/lib/gateway-config.d.ts.map +1 -0
  63. package/lib/gateway-http.d.ts +16 -0
  64. package/lib/gateway-http.d.ts.map +1 -0
  65. package/lib/gateway-openai-chat.d.ts +8 -0
  66. package/lib/gateway-openai-chat.d.ts.map +1 -0
  67. package/lib/gateway-openai-responses.d.ts +8 -0
  68. package/lib/gateway-openai-responses.d.ts.map +1 -0
  69. package/lib/gateway-parse.d.ts +10 -0
  70. package/lib/gateway-parse.d.ts.map +1 -0
  71. package/lib/gateway-protocol.d.ts +47 -0
  72. package/lib/gateway-protocol.d.ts.map +1 -0
  73. package/lib/gateway-routes.d.ts +21 -0
  74. package/lib/gateway-routes.d.ts.map +1 -0
  75. package/lib/gateway.d.ts +48 -0
  76. package/lib/gateway.d.ts.map +1 -0
  77. package/lib/grok-imagine.d.ts +271 -0
  78. package/lib/grok-imagine.d.ts.map +1 -0
  79. package/lib/grok-import.d.ts +21 -0
  80. package/lib/grok-import.d.ts.map +1 -0
  81. package/lib/http-json.d.ts +10 -0
  82. package/lib/http-json.d.ts.map +1 -0
  83. package/lib/ids.d.ts +33 -0
  84. package/lib/ids.d.ts.map +1 -0
  85. package/lib/imagine-routes.d.ts +59 -0
  86. package/lib/imagine-routes.d.ts.map +1 -0
  87. package/lib/index.d.ts +69 -0
  88. package/lib/index.d.ts.map +1 -0
  89. package/lib/index.js +35355 -0
  90. package/lib/index.js.map +7 -0
  91. package/lib/invariant.d.ts +9 -0
  92. package/lib/invariant.d.ts.map +1 -0
  93. package/lib/invariant.js +14 -0
  94. package/lib/invariant.js.map +7 -0
  95. package/lib/kimi-errors.d.ts +13 -0
  96. package/lib/kimi-errors.d.ts.map +1 -0
  97. package/lib/media-store.d.ts +130 -0
  98. package/lib/media-store.d.ts.map +1 -0
  99. package/lib/oauth-import-routes.d.ts +52 -0
  100. package/lib/oauth-import-routes.d.ts.map +1 -0
  101. package/lib/oauth-providers.d.ts +26 -0
  102. package/lib/oauth-providers.d.ts.map +1 -0
  103. package/lib/oauth-session.d.ts +40 -0
  104. package/lib/oauth-session.d.ts.map +1 -0
  105. package/lib/oauth-sources.d.ts +205 -0
  106. package/lib/oauth-sources.d.ts.map +1 -0
  107. package/lib/oauth.d.ts +79 -0
  108. package/lib/oauth.d.ts.map +1 -0
  109. package/lib/provider.d.ts +38 -0
  110. package/lib/provider.d.ts.map +1 -0
  111. package/lib/proxy.d.ts +17 -0
  112. package/lib/proxy.d.ts.map +1 -0
  113. package/lib/redact.d.ts +5 -0
  114. package/lib/redact.d.ts.map +1 -0
  115. package/lib/session.d.ts +40 -0
  116. package/lib/session.d.ts.map +1 -0
  117. package/lib/store.d.ts +46 -0
  118. package/lib/store.d.ts.map +1 -0
  119. package/lib/web-origin.d.ts +10 -0
  120. package/lib/web-origin.d.ts.map +1 -0
  121. package/lib/web-routes.d.ts +20 -0
  122. package/lib/web-routes.d.ts.map +1 -0
  123. package/package.json +185 -0
  124. package/patches/dsh-agy@0.1.2.patch +25 -0
  125. package/scripts/release.mjs +166 -0
  126. package/scripts/smoke-deployed-routes.mjs +146 -0
  127. package/scripts/verify-deployed-catalog.mjs +87 -0
  128. package/src/adapter.ts +282 -0
  129. package/src/alias-adapter.ts +152 -0
  130. package/src/auth-routes.ts +871 -0
  131. package/src/auth.ts +67 -0
  132. package/src/bin.ts +350 -0
  133. package/src/capability-routes.ts +275 -0
  134. package/src/capability-runtime.ts +313 -0
  135. package/src/capability-settings.ts +657 -0
  136. package/src/capability-tools.ts +666 -0
  137. package/src/catalog.ts +271 -0
  138. package/src/client/GrokBuildSettings.tsx +2221 -0
  139. package/src/client/index.tsx +37 -0
  140. package/src/client/locales.ts +421 -0
  141. package/src/codex-http.ts +447 -0
  142. package/src/codex-images.ts +485 -0
  143. package/src/codex-model-capabilities.ts +320 -0
  144. package/src/codex-search.ts +245 -0
  145. package/src/codex-usage.ts +263 -0
  146. package/src/gateway-anthropic-messages.ts +84 -0
  147. package/src/gateway-auth.ts +100 -0
  148. package/src/gateway-backend.ts +274 -0
  149. package/src/gateway-body.ts +49 -0
  150. package/src/gateway-config.ts +76 -0
  151. package/src/gateway-http.ts +104 -0
  152. package/src/gateway-openai-chat.ts +124 -0
  153. package/src/gateway-openai-responses.ts +53 -0
  154. package/src/gateway-parse.ts +224 -0
  155. package/src/gateway-protocol.ts +52 -0
  156. package/src/gateway-routes.ts +152 -0
  157. package/src/gateway.ts +242 -0
  158. package/src/grok-imagine.ts +1627 -0
  159. package/src/grok-import.ts +151 -0
  160. package/src/http-json.ts +82 -0
  161. package/src/ids.ts +45 -0
  162. package/src/imagine-routes.ts +461 -0
  163. package/src/index.ts +598 -0
  164. package/src/invariant.ts +17 -0
  165. package/src/kimi-errors.ts +26 -0
  166. package/src/media-store.ts +927 -0
  167. package/src/oauth-import-routes.ts +314 -0
  168. package/src/oauth-providers.ts +152 -0
  169. package/src/oauth-session.ts +183 -0
  170. package/src/oauth-sources.ts +1104 -0
  171. package/src/oauth.ts +620 -0
  172. package/src/provider.ts +128 -0
  173. package/src/proxy.ts +99 -0
  174. package/src/redact.ts +72 -0
  175. package/src/session.ts +218 -0
  176. package/src/store.ts +217 -0
  177. package/src/web-origin.ts +60 -0
  178. package/src/web-routes.ts +75 -0
@@ -0,0 +1,2221 @@
1
+ /** Plugin-owned coding subscription account section inside the dsh Settings shell. */
2
+
3
+ import type { CSSProperties } from "react";
4
+ import { useCallback, useEffect, useRef, useState } from "react";
5
+ import type { GrokBuildSettingsKey } from "./locales.ts";
6
+
7
+ const STATUS_PATH = "/plugins/dsh-grok-build/oauth/status";
8
+ const LOGIN_PATH = "/plugins/dsh-grok-build/oauth/login";
9
+ const LOGIN_CODE_PATH = "/plugins/dsh-grok-build/oauth/code";
10
+ const LOGIN_CANCEL_PATH = "/plugins/dsh-grok-build/oauth/cancel";
11
+ const LOGOUT_PATH = "/plugins/dsh-grok-build/oauth/logout";
12
+ const MODELS_PATH = "/plugins/dsh-grok-build/oauth/models";
13
+ const SOURCES_PATH = "/plugins/dsh-grok-build/oauth/sources";
14
+ const SOURCES_PREVIEW_PATH = "/plugins/dsh-grok-build/oauth/sources/preview";
15
+ const SOURCES_COMMIT_PATH = "/plugins/dsh-grok-build/oauth/sources/commit";
16
+ const SOURCES_CANCEL_PATH = "/plugins/dsh-grok-build/oauth/sources/cancel";
17
+ const CAPABILITIES_PATH = "/plugins/dsh-grok-build/capabilities";
18
+ const CODEX_USAGE_PATH = "/plugins/dsh-grok-build/codex/usage";
19
+ const IMAGINE_CREDENTIAL_PATH = "/plugins/dsh-grok-build/imagine/credential-status";
20
+ const GATEWAY_PATH = "/plugins/dsh-grok-build/gateway";
21
+ const GATEWAY_REVEAL_PATH = "/plugins/dsh-grok-build/gateway/reveal";
22
+ const GATEWAY_ROTATE_PATH = "/plugins/dsh-grok-build/gateway/rotate";
23
+ const POLL_INTERVAL_MS = 1_000;
24
+
25
+ type ProviderSlug = "grok" | "codex" | "kimi" | "claude";
26
+ type LoginMethod = "pkce" | "device" | "browser";
27
+ type CatalogSource = "live" | "cache" | "fallback";
28
+ type SourceKind = ProviderSlug;
29
+ type SourceReason = "missing" | "unsafe" | "invalid" | "too_large";
30
+ type SourceConflict =
31
+ | "none"
32
+ | "same_credential"
33
+ | "same_account"
34
+ | "different_account"
35
+ | "unknown_account"
36
+ | "unreadable_destination"
37
+ | "unsafe_destination";
38
+ type SourcePreviewAction = "import" | "reuse" | "overwrite" | "blocked";
39
+ type SourceCommitAction = "imported" | "unchanged" | "overwritten";
40
+ type CapabilityFlagKey =
41
+ | "codexSearch"
42
+ | "codexImages"
43
+ | "codexImageEdits"
44
+ | "codexUsage"
45
+ | "codexFast"
46
+ | "grokImagineImage"
47
+ | "grokImagineVideo";
48
+ type CapabilityLimitKey = "searchResults" | "imageCount" | "videoArtifactTtlMs";
49
+ type CapabilitySettingKey = CapabilityFlagKey | CapabilityLimitKey;
50
+
51
+ type GrokStatus =
52
+ | { status: "signed-out"; grokImportAvailable: boolean }
53
+ | { status: "signing-in"; method: "pkce" | "device"; url?: string; userCode?: string; grokImportAvailable: boolean }
54
+ | {
55
+ status: "signed-in";
56
+ models: string[];
57
+ available: string[];
58
+ selected: string[];
59
+ catalogSource: CatalogSource;
60
+ catalogError?: string;
61
+ grokImportAvailable: boolean;
62
+ }
63
+ | { status: "error"; message: string; grokImportAvailable: boolean };
64
+
65
+ type SubscriptionStatus = {
66
+ provider: Exclude<ProviderSlug, "grok">;
67
+ route: string;
68
+ displayName: string;
69
+ loginMethods: readonly ("browser" | "device")[];
70
+ recommendedLoginMethod: "browser" | "device";
71
+ models: string[];
72
+ available: string[];
73
+ selected: string[];
74
+ } & (
75
+ | { status: "signed-out" }
76
+ | { status: "signing-in"; method: "browser" | "device"; url?: string; userCode?: string }
77
+ | { status: "signed-in"; expiresAt?: number }
78
+ | { status: "error"; message: string }
79
+ );
80
+
81
+ type ProviderStatus = GrokStatus | SubscriptionStatus;
82
+
83
+ interface CodingOAuthStatus {
84
+ providers: {
85
+ grok: GrokStatus;
86
+ codex: SubscriptionStatus;
87
+ kimi: SubscriptionStatus;
88
+ claude: SubscriptionStatus;
89
+ };
90
+ antigravity: { installed: boolean; route: "agy"; management: "cli" };
91
+ }
92
+
93
+ interface LoginChallenge {
94
+ method: LoginMethod;
95
+ url: string;
96
+ userCode?: string;
97
+ }
98
+
99
+ interface ProviderCardDefinition {
100
+ slug: ProviderSlug;
101
+ route: string;
102
+ titleKey: GrokBuildSettingsKey;
103
+ descriptionKey: GrokBuildSettingsKey;
104
+ methods: readonly LoginMethod[];
105
+ recommended: LoginMethod;
106
+ }
107
+
108
+ interface SourceStatus {
109
+ kind: SourceKind;
110
+ displayPath: string;
111
+ available: boolean;
112
+ expiresAt?: number;
113
+ reason?: SourceReason;
114
+ }
115
+
116
+ interface SourcePreview {
117
+ previewId: string;
118
+ kind: SourceKind;
119
+ displayPath: string;
120
+ expiresAt?: number;
121
+ ticketExpiresAt?: number;
122
+ conflict?: SourceConflict;
123
+ action?: SourcePreviewAction;
124
+ warnings: string[];
125
+ confirmOverwriteRequired: boolean;
126
+ }
127
+
128
+ interface CapabilityFlags {
129
+ codexSearch: boolean;
130
+ codexImages: boolean;
131
+ codexImageEdits: boolean;
132
+ codexUsage: boolean;
133
+ codexFast: boolean;
134
+ grokImagineImage: boolean;
135
+ grokImagineVideo: boolean;
136
+ }
137
+
138
+ interface CapabilitySettingsView extends CapabilityFlags {
139
+ searchResults: number;
140
+ imageCount: number;
141
+ videoArtifactTtlMs: number;
142
+ }
143
+
144
+ interface CapabilitySnapshot {
145
+ value: CapabilitySettingsView;
146
+ revision: number;
147
+ writable: boolean;
148
+ }
149
+
150
+ interface UsageWindowView {
151
+ usedPercent?: number;
152
+ remainingPercent?: number;
153
+ windowSeconds?: number;
154
+ resetsAt?: number;
155
+ }
156
+
157
+ interface UsageLimitView {
158
+ id: string;
159
+ name?: string;
160
+ windows: UsageWindowView[];
161
+ }
162
+
163
+ interface UsageView {
164
+ rateLimits: UsageLimitView[];
165
+ creditsUnlimited?: boolean;
166
+ creditsBalance?: string;
167
+ individualLimit?: string;
168
+ individualUsed?: string;
169
+ individualRemaining?: string;
170
+ individualRemainingPercent?: number;
171
+ individualResetsAt?: number;
172
+ spendControlReached?: boolean;
173
+ resetCredits?: number;
174
+ fetchedAt?: number;
175
+ }
176
+
177
+ interface ImagineCredentialView {
178
+ configured: boolean;
179
+ source?: string;
180
+ writable?: boolean;
181
+ }
182
+
183
+ interface PluginRequestError extends Error {
184
+ status: number;
185
+ code?: string;
186
+ }
187
+
188
+ const SOURCE_KINDS: readonly SourceKind[] = ["grok", "codex", "kimi", "claude"];
189
+ const SOURCE_REASONS: readonly SourceReason[] = ["missing", "unsafe", "invalid", "too_large"];
190
+ const SOURCE_CONFLICTS: readonly SourceConflict[] = [
191
+ "none",
192
+ "same_credential",
193
+ "same_account",
194
+ "different_account",
195
+ "unknown_account",
196
+ "unreadable_destination",
197
+ "unsafe_destination",
198
+ ];
199
+ const SOURCE_PREVIEW_ACTIONS: readonly SourcePreviewAction[] = ["import", "reuse", "overwrite", "blocked"];
200
+ const SOURCE_COMMIT_ACTIONS: readonly SourceCommitAction[] = ["imported", "unchanged", "overwritten"];
201
+ const SOURCE_DEFAULT_PATH: { readonly [K in SourceKind]: string } = {
202
+ grok: "~/.grok/auth.json",
203
+ codex: "~/.codex/auth.json",
204
+ kimi: "~/.kimi/credentials/kimi-code.json",
205
+ claude: "~/.claude/.credentials.json",
206
+ };
207
+ const SOURCE_KIND_KEY: { readonly [K in SourceKind]: GrokBuildSettingsKey } = {
208
+ grok: "sourceKindGrok",
209
+ codex: "sourceKindCodex",
210
+ kimi: "sourceKindKimi",
211
+ claude: "sourceKindClaude",
212
+ };
213
+ const SOURCE_REASON_KEY: { readonly [K in SourceReason]: GrokBuildSettingsKey } = {
214
+ missing: "sourceReasonMissing",
215
+ unsafe: "sourceReasonUnsafe",
216
+ invalid: "sourceReasonInvalid",
217
+ too_large: "sourceReasonTooLarge",
218
+ };
219
+ const SOURCE_CONFLICT_KEY: { readonly [K in SourceConflict]: GrokBuildSettingsKey } = {
220
+ none: "sourceConflictNone",
221
+ same_credential: "sourceConflictSameCredential",
222
+ same_account: "sourceConflictSameAccount",
223
+ different_account: "sourceConflictDifferentAccount",
224
+ unknown_account: "sourceConflictUnknownAccount",
225
+ unreadable_destination: "sourceConflictUnreadableDestination",
226
+ unsafe_destination: "sourceConflictUnsafeDestination",
227
+ };
228
+ const SOURCE_PREVIEW_ACTION_KEY: { readonly [K in SourcePreviewAction]: GrokBuildSettingsKey } = {
229
+ import: "sourceActionImport",
230
+ reuse: "sourceActionReuse",
231
+ overwrite: "sourceActionOverwrite",
232
+ blocked: "sourceActionBlocked",
233
+ };
234
+ const SOURCE_COMMIT_ACTION_KEY: { readonly [K in SourceCommitAction]: GrokBuildSettingsKey } = {
235
+ imported: "sourceCommitImported",
236
+ unchanged: "sourceCommitUnchanged",
237
+ overwritten: "sourceCommitOverwritten",
238
+ };
239
+ const CAPABILITY_TOGGLES: readonly {
240
+ key: CapabilityFlagKey;
241
+ label: GrokBuildSettingsKey;
242
+ hint: GrokBuildSettingsKey;
243
+ requiresImages?: true;
244
+ }[] = [
245
+ { key: "codexSearch", label: "capCodexSearch", hint: "capCodexSearchHint" },
246
+ { key: "codexImages", label: "capCodexImages", hint: "capCodexImagesHint" },
247
+ { key: "codexImageEdits", label: "capCodexImageEdits", hint: "capCodexImageEditsHint", requiresImages: true },
248
+ { key: "codexUsage", label: "capCodexUsage", hint: "capCodexUsageHint" },
249
+ { key: "codexFast", label: "capCodexFast", hint: "capCodexFastHint" },
250
+ { key: "grokImagineImage", label: "capGrokImagineImage", hint: "capGrokImagineImageHint" },
251
+ { key: "grokImagineVideo", label: "capGrokImagineVideo", hint: "capGrokImagineVideoHint" },
252
+ ];
253
+ const HOUR_MS = 60 * 60 * 1000;
254
+ const CAPABILITY_LIMITS: readonly {
255
+ key: CapabilityLimitKey;
256
+ label: GrokBuildSettingsKey;
257
+ hint: GrokBuildSettingsKey;
258
+ min: number;
259
+ max: number;
260
+ scale: number;
261
+ }[] = [
262
+ { key: "searchResults", label: "capSearchResults", hint: "capSearchResultsHint", min: 1, max: 20, scale: 1 },
263
+ { key: "imageCount", label: "capImageCount", hint: "capImageCountHint", min: 1, max: 4, scale: 1 },
264
+ {
265
+ key: "videoArtifactTtlMs",
266
+ label: "capVideoTtlHours",
267
+ hint: "capVideoTtlHoursHint",
268
+ min: 1,
269
+ max: 168,
270
+ scale: HOUR_MS,
271
+ },
272
+ ];
273
+ const IMAGINE_SOURCE_KEY: { readonly [source: string]: GrokBuildSettingsKey } = {
274
+ none: "imagineSourceNone",
275
+ env: "imagineSourceEnv",
276
+ environment: "imagineSourceEnv",
277
+ "xai-api-key": "imagineSourceEnv",
278
+ xai_api_key: "imagineSourceEnv",
279
+ "api-key": "imagineSourceApiKey",
280
+ api_key: "imagineSourceApiKey",
281
+ apikey: "imagineSourceApiKey",
282
+ key: "imagineSourceApiKey",
283
+ settings: "imagineSourceApiKey",
284
+ oauth: "imagineSourceOAuth",
285
+ "oauth-access": "imagineSourceOAuth",
286
+ "grok-cli-key": "imagineSourceCliKey",
287
+ "cli-key": "imagineSourceCliKey",
288
+ };
289
+
290
+ const PROVIDERS: readonly ProviderCardDefinition[] = [
291
+ {
292
+ slug: "grok",
293
+ route: "grok-build",
294
+ titleKey: "grokTitle",
295
+ descriptionKey: "grokDescription",
296
+ methods: ["pkce", "device"],
297
+ recommended: "pkce",
298
+ },
299
+ {
300
+ slug: "codex",
301
+ route: "codex-oauth",
302
+ titleKey: "codexTitle",
303
+ descriptionKey: "codexDescription",
304
+ methods: ["device", "browser"],
305
+ recommended: "device",
306
+ },
307
+ {
308
+ slug: "kimi",
309
+ route: "kimi-code-oauth",
310
+ titleKey: "kimiTitle",
311
+ descriptionKey: "kimiDescription",
312
+ methods: ["device"],
313
+ recommended: "device",
314
+ },
315
+ {
316
+ slug: "claude",
317
+ route: "claude-code-oauth",
318
+ titleKey: "claudeTitle",
319
+ descriptionKey: "claudeDescription",
320
+ methods: ["browser"],
321
+ recommended: "browser",
322
+ },
323
+ ];
324
+
325
+ type SettingsTabId = "accounts" | "capabilities" | "gateway" | "about";
326
+ type CopyField = "openai" | "anthropic" | "key";
327
+
328
+ const SETTINGS_TABS: readonly { id: SettingsTabId; label: GrokBuildSettingsKey }[] = [
329
+ { id: "accounts", label: "tabAccounts" },
330
+ { id: "gateway", label: "tabGateway" },
331
+ { id: "capabilities", label: "tabCapabilities" },
332
+ { id: "about", label: "tabAbout" },
333
+ ];
334
+
335
+ export interface GrokBuildSettingsInjected {
336
+ t: (key: GrokBuildSettingsKey, params?: Record<string, unknown>) => string;
337
+ }
338
+
339
+ export type GrokBuildSettingsProps = Partial<GrokBuildSettingsInjected>;
340
+
341
+ const pageStyle: CSSProperties = { display: "flex", flexDirection: "column", gap: 16, maxWidth: 780 };
342
+ const titleStyle: CSSProperties = {
343
+ margin: 0,
344
+ fontSize: 20,
345
+ lineHeight: "28px",
346
+ fontWeight: 600,
347
+ color: "var(--dsw-alias-label-primary)",
348
+ };
349
+ const bodyStyle: CSSProperties = {
350
+ margin: 0,
351
+ fontSize: 14,
352
+ lineHeight: "22px",
353
+ color: "var(--dsw-alias-label-secondary)",
354
+ };
355
+ const cardStyle: CSSProperties = {
356
+ display: "flex",
357
+ flexDirection: "column",
358
+ gap: 14,
359
+ padding: "18px 20px",
360
+ border: "1px solid var(--dsw-alias-border-l2)",
361
+ borderRadius: 12,
362
+ background: "var(--dsw-alias-bg-module-platform)",
363
+ };
364
+ const rowStyle: CSSProperties = {
365
+ display: "flex",
366
+ alignItems: "center",
367
+ justifyContent: "space-between",
368
+ flexWrap: "wrap",
369
+ gap: 12,
370
+ };
371
+ const statusStyle: CSSProperties = {
372
+ display: "flex",
373
+ alignItems: "center",
374
+ gap: 9,
375
+ fontSize: 14,
376
+ fontWeight: 500,
377
+ color: "var(--dsw-alias-label-primary)",
378
+ };
379
+ const buttonStyle: CSSProperties = {
380
+ boxSizing: "border-box",
381
+ minHeight: 34,
382
+ padding: "6px 14px",
383
+ border: "1px solid var(--dsw-alias-border-l4, rgba(127, 127, 127, 0.4))",
384
+ borderRadius: 18,
385
+ background: "var(--dsw-alias-button-elevated-fill, var(--dsw-alias-bg-layer-1))",
386
+ color: "var(--dsw-alias-label-primary)",
387
+ boxShadow: "0 1px 2px rgba(0, 0, 0, 0.18)",
388
+ font: "inherit",
389
+ fontSize: 14,
390
+ fontWeight: 500,
391
+ cursor: "pointer",
392
+ };
393
+ const primaryButtonStyle: CSSProperties = {
394
+ ...buttonStyle,
395
+ borderColor: "#315fc7",
396
+ background: "#315fc7",
397
+ color: "#ffffff",
398
+ boxShadow: "0 1px 3px rgba(0, 0, 0, 0.28)",
399
+ fontWeight: 600,
400
+ };
401
+ const errorStyle: CSSProperties = { ...bodyStyle, color: "var(--dsw-alias-state-error-primary)" };
402
+ const warningStyle: CSSProperties = {
403
+ ...bodyStyle,
404
+ padding: "10px 12px",
405
+ borderRadius: 8,
406
+ background: "var(--dsw-alias-bg-layer-1)",
407
+ };
408
+ const codeStyle: CSSProperties = {
409
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
410
+ fontSize: 20,
411
+ letterSpacing: "0.08em",
412
+ fontWeight: 600,
413
+ color: "var(--dsw-alias-label-primary)",
414
+ };
415
+ const monoStyle: CSSProperties = { fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" };
416
+ const linkStyle: CSSProperties = { color: "var(--dsw-alias-brand-primary)", wordBreak: "break-all" };
417
+ const listStyle: CSSProperties = {
418
+ display: "flex",
419
+ flexDirection: "column",
420
+ gap: 8,
421
+ margin: 0,
422
+ padding: 0,
423
+ listStyle: "none",
424
+ };
425
+ const checkRowStyle: CSSProperties = {
426
+ display: "flex",
427
+ alignItems: "flex-start",
428
+ gap: 8,
429
+ fontSize: 14,
430
+ color: "var(--dsw-alias-label-primary)",
431
+ };
432
+ const inputStyle: CSSProperties = {
433
+ boxSizing: "border-box",
434
+ width: "100%",
435
+ minHeight: 34,
436
+ padding: "6px 12px",
437
+ border: "1px solid var(--dsw-alias-border-l2)",
438
+ borderRadius: 8,
439
+ background: "var(--dsw-alias-bg-layer-1)",
440
+ color: "var(--dsw-alias-label-primary)",
441
+ font: "inherit",
442
+ fontSize: 13,
443
+ };
444
+ const nestedStyle: CSSProperties = {
445
+ display: "flex",
446
+ flexDirection: "column",
447
+ gap: 8,
448
+ padding: "12px 14px",
449
+ border: "1px solid var(--dsw-alias-border-l2)",
450
+ borderRadius: 8,
451
+ background: "var(--dsw-alias-bg-layer-1)",
452
+ };
453
+ const hintStyle: CSSProperties = { ...bodyStyle, fontSize: 13 };
454
+ const tabNavStyle: CSSProperties = {
455
+ display: "flex",
456
+ flexWrap: "wrap",
457
+ gap: 8,
458
+ };
459
+ const tabButtonStyle: CSSProperties = {
460
+ ...buttonStyle,
461
+ borderRadius: 10,
462
+ };
463
+ const tabButtonActiveStyle: CSSProperties = {
464
+ ...primaryButtonStyle,
465
+ borderRadius: 10,
466
+ };
467
+ const panelStyle: CSSProperties = {
468
+ display: "flex",
469
+ flexDirection: "column",
470
+ gap: 14,
471
+ minWidth: 0,
472
+ };
473
+ const accountGridStyle: CSSProperties = {
474
+ display: "grid",
475
+ gridTemplateColumns: "repeat(auto-fill, minmax(320px, 1fr))",
476
+ gap: 14,
477
+ };
478
+ const copyRowStyle: CSSProperties = {
479
+ display: "flex",
480
+ alignItems: "center",
481
+ justifyContent: "space-between",
482
+ flexWrap: "wrap",
483
+ gap: 8,
484
+ };
485
+
486
+ function dotStyle(
487
+ status: ProviderStatus["status"] | "loading" | "available" | "unavailable",
488
+ installed = true,
489
+ ): CSSProperties {
490
+ const color = !installed
491
+ ? "var(--dsw-alias-label-dimmed, #9aa0a6)"
492
+ : status === "signed-in" || status === "available"
493
+ ? "var(--dsw-alias-state-success-primary, #22a06b)"
494
+ : status === "error"
495
+ ? "var(--dsw-alias-state-error-primary, #d92d20)"
496
+ : status === "signing-in" || status === "loading"
497
+ ? "var(--dsw-alias-brand-primary, #1677ff)"
498
+ : "var(--dsw-alias-label-dimmed, #9aa0a6)";
499
+ return { width: 9, height: 9, borderRadius: "50%", flex: "0 0 auto", background: color };
500
+ }
501
+
502
+ function isRecord(value: unknown): value is Record<string, unknown> {
503
+ return typeof value === "object" && value !== null && !Array.isArray(value);
504
+ }
505
+
506
+ function optionalString(value: unknown): string | undefined {
507
+ return typeof value === "string" && value.length > 0 && value.length < 500 ? value : undefined;
508
+ }
509
+
510
+ function optionalFiniteNumber(value: unknown): number | undefined {
511
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
512
+ }
513
+
514
+ function optionalBoolean(value: unknown): boolean | undefined {
515
+ return typeof value === "boolean" ? value : undefined;
516
+ }
517
+
518
+ function optionalPercent(value: unknown): number | undefined {
519
+ const numeric = optionalFiniteNumber(value);
520
+ return numeric !== undefined && numeric >= 0 && numeric <= 100 ? numeric : undefined;
521
+ }
522
+
523
+ function isSourceKind(value: string): value is SourceKind {
524
+ return (SOURCE_KINDS as readonly string[]).includes(value);
525
+ }
526
+
527
+ function isSourceReason(value: string): value is SourceReason {
528
+ return (SOURCE_REASONS as readonly string[]).includes(value);
529
+ }
530
+
531
+ function isSourceConflict(value: string): value is SourceConflict {
532
+ return (SOURCE_CONFLICTS as readonly string[]).includes(value);
533
+ }
534
+
535
+ function isSourcePreviewAction(value: string): value is SourcePreviewAction {
536
+ return (SOURCE_PREVIEW_ACTIONS as readonly string[]).includes(value);
537
+ }
538
+
539
+ function isSourceCommitAction(value: string): value is SourceCommitAction {
540
+ return (SOURCE_COMMIT_ACTIONS as readonly string[]).includes(value);
541
+ }
542
+
543
+ function looksSecret(value: string): boolean {
544
+ return /eyJ[A-Za-z0-9_-]+\.|sk-[A-Za-z0-9_-]{8,}|Bearer\s+\S+/u.test(value);
545
+ }
546
+
547
+ function safeDisplayPath(value: unknown, kind: SourceKind): string {
548
+ const text = optionalString(value);
549
+ if (text === undefined || looksSecret(text) || text.length > 180) return SOURCE_DEFAULT_PATH[kind];
550
+ return text;
551
+ }
552
+
553
+ function safeWarning(value: unknown): string | undefined {
554
+ const text = optionalString(value);
555
+ if (text === undefined || looksSecret(text)) return undefined;
556
+ return text;
557
+ }
558
+
559
+ function formatEpoch(value: number | undefined): string | undefined {
560
+ if (value === undefined || !Number.isFinite(value) || value <= 0) return undefined;
561
+ const ms = value > 1e12 ? value : value > 1e9 ? value * 1000 : undefined;
562
+ if (ms === undefined) return undefined;
563
+ const formatted = new Date(ms).toLocaleString();
564
+ return formatted.length > 0 ? formatted : undefined;
565
+ }
566
+
567
+ function isPluginRequestError(error: unknown): error is PluginRequestError {
568
+ return error instanceof Error && error.name === "PluginRequestError" && "status" in error;
569
+ }
570
+
571
+ function isConflictError(error: unknown): boolean {
572
+ if (!isPluginRequestError(error)) {
573
+ return (
574
+ error instanceof Error && /SETTINGS_CONFLICT|settings-conflict|changed since it was read/iu.test(error.message)
575
+ );
576
+ }
577
+ return error.status === 409 || error.code === "SETTINGS_CONFLICT" || /conflict/iu.test(error.message);
578
+ }
579
+
580
+ const CONSUMED_PREVIEW_CODES = new Set([
581
+ "preview_invalid",
582
+ "preview_expired",
583
+ "source_changed",
584
+ "destination_changed",
585
+ "confirm_required",
586
+ "unsafe_destination",
587
+ ]);
588
+
589
+ function isConsumedPreviewError(error: unknown): boolean {
590
+ if (!isPluginRequestError(error)) return false;
591
+ if (error.code !== undefined && CONSUMED_PREVIEW_CODES.has(error.code)) return true;
592
+ return error.status === 404 || error.status === 410;
593
+ }
594
+
595
+ function cancelPreviewTicket(previewId: string, keepalive = false): void {
596
+ void fetch(SOURCES_CANCEL_PATH, {
597
+ method: "POST",
598
+ headers: { accept: "application/json", "content-type": "application/json" },
599
+ credentials: "same-origin",
600
+ body: JSON.stringify({ previewId }),
601
+ ...(keepalive ? { keepalive: true } : {}),
602
+ }).catch(() => undefined);
603
+ }
604
+
605
+ async function jsonRequest<T>(path: string, method = "GET", body?: unknown): Promise<T> {
606
+ const response = await fetch(path, {
607
+ method,
608
+ headers: { accept: "application/json", ...(body === undefined ? {} : { "content-type": "application/json" }) },
609
+ credentials: "same-origin",
610
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }),
611
+ });
612
+ const value: unknown = await response.json().catch(() => undefined);
613
+ if (!response.ok) {
614
+ const record = isRecord(value) ? value : undefined;
615
+ const message =
616
+ record !== undefined && typeof record["error"] === "string"
617
+ ? record["error"]
618
+ : record !== undefined && typeof record["message"] === "string"
619
+ ? record["message"]
620
+ : `HTTP ${response.status}`;
621
+ const code = record !== undefined && typeof record["code"] === "string" ? record["code"] : undefined;
622
+ const error = new Error(message) as PluginRequestError;
623
+ error.name = "PluginRequestError";
624
+ error.status = response.status;
625
+ if (code !== undefined) error.code = code;
626
+ throw error;
627
+ }
628
+ return value as T;
629
+ }
630
+
631
+ function parseSource(value: unknown): SourceStatus | undefined {
632
+ if (!isRecord(value) || typeof value["kind"] !== "string" || !isSourceKind(value["kind"])) return undefined;
633
+ const kind = value["kind"];
634
+ const reasonRaw = optionalString(value["reason"]);
635
+ const expiresAt = optionalFiniteNumber(value["expiresAt"]);
636
+ return {
637
+ kind,
638
+ displayPath: safeDisplayPath(value["displayPath"], kind),
639
+ available: value["available"] === true,
640
+ ...(expiresAt === undefined ? {} : { expiresAt }),
641
+ ...(reasonRaw !== undefined && isSourceReason(reasonRaw) ? { reason: reasonRaw } : {}),
642
+ };
643
+ }
644
+
645
+ function mergeSources(discovered: readonly SourceStatus[]): SourceStatus[] {
646
+ return SOURCE_KINDS.map((kind) => {
647
+ const found = discovered.find((entry) => entry.kind === kind);
648
+ return found ?? { kind, displayPath: SOURCE_DEFAULT_PATH[kind], available: false, reason: "missing" };
649
+ });
650
+ }
651
+
652
+ function parseSources(value: unknown): SourceStatus[] {
653
+ const rows = Array.isArray(value)
654
+ ? value
655
+ : isRecord(value) && Array.isArray(value["sources"])
656
+ ? value["sources"]
657
+ : [];
658
+ return mergeSources(rows.map(parseSource).filter((entry): entry is SourceStatus => entry !== undefined));
659
+ }
660
+
661
+ function parsePreview(value: unknown): SourcePreview | undefined {
662
+ if (!isRecord(value)) return undefined;
663
+ const previewId = optionalString(value["previewId"]);
664
+ const kindRaw = optionalString(value["kind"]);
665
+ if (previewId === undefined || kindRaw === undefined || !isSourceKind(kindRaw)) return undefined;
666
+ const conflictRaw = optionalString(value["conflict"]);
667
+ const actionRaw = optionalString(value["action"]);
668
+ const expiresAt = optionalFiniteNumber(value["expiresAt"]);
669
+ const ticketExpiresAt = optionalFiniteNumber(value["ticketExpiresAt"]);
670
+ const warnings = Array.isArray(value["warnings"])
671
+ ? value["warnings"].map(safeWarning).filter((entry): entry is string => entry !== undefined)
672
+ : [];
673
+ return {
674
+ previewId,
675
+ kind: kindRaw,
676
+ displayPath: safeDisplayPath(value["displayPath"], kindRaw),
677
+ confirmOverwriteRequired: value["confirmOverwriteRequired"] === true,
678
+ warnings,
679
+ ...(expiresAt === undefined ? {} : { expiresAt }),
680
+ ...(ticketExpiresAt === undefined ? {} : { ticketExpiresAt }),
681
+ ...(conflictRaw !== undefined && isSourceConflict(conflictRaw) ? { conflict: conflictRaw } : {}),
682
+ ...(actionRaw !== undefined && isSourcePreviewAction(actionRaw) ? { action: actionRaw } : {}),
683
+ };
684
+ }
685
+
686
+ function parseCommitAction(value: unknown): SourceCommitAction | undefined {
687
+ if (!isRecord(value)) return undefined;
688
+ const action = optionalString(value["action"]);
689
+ return action !== undefined && isSourceCommitAction(action) ? action : undefined;
690
+ }
691
+
692
+ function boundedInteger(value: unknown, min: number, max: number, fallback: number): number {
693
+ const numeric = optionalFiniteNumber(value);
694
+ return numeric !== undefined && Number.isInteger(numeric) && numeric >= min && numeric <= max ? numeric : fallback;
695
+ }
696
+
697
+ function emptyCapabilitySettings(): CapabilitySettingsView {
698
+ return {
699
+ codexSearch: false,
700
+ codexImages: false,
701
+ codexImageEdits: false,
702
+ codexUsage: false,
703
+ codexFast: false,
704
+ grokImagineImage: false,
705
+ grokImagineVideo: false,
706
+ searchResults: 5,
707
+ imageCount: 1,
708
+ videoArtifactTtlMs: 7 * 24 * HOUR_MS,
709
+ };
710
+ }
711
+
712
+ function parseCapabilitySettings(value: unknown): CapabilitySettingsView {
713
+ const source = isRecord(value) ? value : {};
714
+ return {
715
+ codexSearch: source["codexSearch"] === true,
716
+ codexImages: source["codexImages"] === true,
717
+ codexImageEdits: source["codexImageEdits"] === true,
718
+ codexUsage: source["codexUsage"] === true,
719
+ codexFast: source["codexFast"] === true,
720
+ grokImagineImage: source["grokImagineImage"] === true,
721
+ grokImagineVideo: source["grokImagineVideo"] === true,
722
+ searchResults: boundedInteger(source["searchResults"], 1, 20, 5),
723
+ imageCount: boundedInteger(source["imageCount"], 1, 4, 1),
724
+ videoArtifactTtlMs: boundedInteger(source["videoArtifactTtlMs"], HOUR_MS, 7 * 24 * HOUR_MS, 7 * 24 * HOUR_MS),
725
+ };
726
+ }
727
+
728
+ function parseCapabilities(value: unknown): CapabilitySnapshot | undefined {
729
+ if (!isRecord(value)) return undefined;
730
+ const nested = isRecord(value["value"]) ? value["value"] : value;
731
+ const revision = optionalFiniteNumber(value["revision"]);
732
+ if (revision === undefined && !isRecord(value["value"]) && value["writable"] === undefined) return undefined;
733
+ return {
734
+ value: parseCapabilitySettings(nested),
735
+ revision: revision ?? 0,
736
+ writable: value["writable"] === true,
737
+ };
738
+ }
739
+
740
+ function parseUsageWindow(value: unknown): UsageWindowView | undefined {
741
+ if (!isRecord(value)) return undefined;
742
+ const usedPercent = optionalPercent(value["usedPercent"] ?? value["used_percent"]);
743
+ const remainingPercent = optionalPercent(value["remainingPercent"] ?? value["remaining_percent"]);
744
+ const windowSeconds = optionalFiniteNumber(value["windowSeconds"] ?? value["limit_window_seconds"]);
745
+ const resetsAt = optionalFiniteNumber(value["resetsAt"] ?? value["reset_at"]);
746
+ if (
747
+ usedPercent === undefined &&
748
+ remainingPercent === undefined &&
749
+ windowSeconds === undefined &&
750
+ resetsAt === undefined
751
+ ) {
752
+ return undefined;
753
+ }
754
+ return {
755
+ ...(usedPercent === undefined ? {} : { usedPercent }),
756
+ ...(remainingPercent === undefined ? {} : { remainingPercent }),
757
+ ...(windowSeconds !== undefined && windowSeconds > 0 ? { windowSeconds } : {}),
758
+ ...(resetsAt === undefined ? {} : { resetsAt }),
759
+ };
760
+ }
761
+
762
+ function parseUsageLimit(value: unknown, fallbackId: string): UsageLimitView | undefined {
763
+ if (!isRecord(value)) return undefined;
764
+ const id = optionalString(value["id"]) ?? optionalString(value["metered_feature"]) ?? fallbackId;
765
+ const name = optionalString(value["name"]) ?? optionalString(value["limit_name"]);
766
+ const nested = isRecord(value["rate_limit"]) ? value["rate_limit"] : value;
767
+ const windows = Array.isArray(value["windows"])
768
+ ? value["windows"].map(parseUsageWindow).filter((entry): entry is UsageWindowView => entry !== undefined)
769
+ : [
770
+ parseUsageWindow(nested["primary_window"]),
771
+ parseUsageWindow(nested["secondary_window"]),
772
+ parseUsageWindow(nested),
773
+ ].filter((entry): entry is UsageWindowView => entry !== undefined);
774
+ if (windows.length === 0 && name === undefined && optionalString(value["id"]) === undefined) return undefined;
775
+ return { id, windows, ...(name === undefined ? {} : { name }) };
776
+ }
777
+
778
+ function parseUsage(value: unknown): UsageView | undefined {
779
+ if (!isRecord(value)) return undefined;
780
+ const payload = isRecord(value["usage"]) ? value["usage"] : value;
781
+ const rateLimits: UsageLimitView[] = [];
782
+ const seen = new Set<string>();
783
+ const add = (limit: UsageLimitView | undefined): void => {
784
+ if (limit === undefined || seen.has(limit.id)) return;
785
+ seen.add(limit.id);
786
+ rateLimits.push(limit);
787
+ };
788
+ if (Array.isArray(payload["rateLimits"])) {
789
+ payload["rateLimits"].forEach((entry, index) => add(parseUsageLimit(entry, `limit-${String(index)}`)));
790
+ } else {
791
+ add(parseUsageLimit(payload["rate_limit"], "codex"));
792
+ if (Array.isArray(payload["additional_rate_limits"])) {
793
+ payload["additional_rate_limits"].forEach((entry, index) =>
794
+ add(parseUsageLimit(entry, `extra-${String(index)}`)),
795
+ );
796
+ }
797
+ add(parseUsageLimit(payload["code_review_rate_limit"], "code_review"));
798
+ }
799
+ const credits = isRecord(payload["credits"]) ? payload["credits"] : undefined;
800
+ const spend = isRecord(payload["individualLimit"])
801
+ ? payload["individualLimit"]
802
+ : isRecord(payload["spend_control"])
803
+ ? isRecord(payload["spend_control"]["individual_limit"])
804
+ ? payload["spend_control"]["individual_limit"]
805
+ : payload["spend_control"]
806
+ : undefined;
807
+ const resetRaw = isRecord(payload["resetCredits"])
808
+ ? payload["resetCredits"]["availableCount"]
809
+ : isRecord(payload["rate_limit_reset_credits"])
810
+ ? payload["rate_limit_reset_credits"]["available_count"]
811
+ : undefined;
812
+ const resetCredits = optionalFiniteNumber(resetRaw);
813
+ const fetchedAt = optionalFiniteNumber(payload["fetchedAt"]);
814
+ const spendControlReached =
815
+ optionalBoolean(payload["spendControlReached"]) ??
816
+ (isRecord(payload["spend_control"]) ? optionalBoolean(payload["spend_control"]["reached"]) : undefined);
817
+ const creditsBalance = credits === undefined ? undefined : optionalString(credits["balance"]);
818
+ const individualLimit = spend === undefined ? undefined : optionalString(spend["limit"]);
819
+ const individualUsed = spend === undefined ? undefined : optionalString(spend["used"]);
820
+ const individualRemaining = spend === undefined ? undefined : optionalString(spend["remaining"]);
821
+ const individualRemainingPercent =
822
+ spend === undefined ? undefined : optionalPercent(spend["remainingPercent"] ?? spend["remaining_percent"]);
823
+ const individualResetsAt =
824
+ spend === undefined ? undefined : optionalFiniteNumber(spend["resetsAt"] ?? spend["reset_at"]);
825
+ return {
826
+ rateLimits,
827
+ ...(credits !== undefined && typeof credits["unlimited"] === "boolean"
828
+ ? { creditsUnlimited: credits["unlimited"] }
829
+ : {}),
830
+ ...(creditsBalance === undefined ? {} : { creditsBalance }),
831
+ ...(individualLimit === undefined ? {} : { individualLimit }),
832
+ ...(individualUsed === undefined ? {} : { individualUsed }),
833
+ ...(individualRemaining === undefined ? {} : { individualRemaining }),
834
+ ...(individualRemainingPercent === undefined ? {} : { individualRemainingPercent }),
835
+ ...(individualResetsAt === undefined ? {} : { individualResetsAt }),
836
+ ...(spendControlReached === undefined ? {} : { spendControlReached }),
837
+ ...(resetCredits !== undefined && resetCredits >= 0 && Number.isSafeInteger(resetCredits) ? { resetCredits } : {}),
838
+ ...(fetchedAt === undefined ? {} : { fetchedAt }),
839
+ };
840
+ }
841
+
842
+ function usageHasVisibleFields(usage: UsageView): boolean {
843
+ return (
844
+ usage.rateLimits.some((limit) => limit.windows.length > 0 || limit.name !== undefined) ||
845
+ usage.creditsUnlimited !== undefined ||
846
+ usage.creditsBalance !== undefined ||
847
+ usage.individualLimit !== undefined ||
848
+ usage.individualUsed !== undefined ||
849
+ usage.individualRemaining !== undefined ||
850
+ usage.individualRemainingPercent !== undefined ||
851
+ usage.spendControlReached === true ||
852
+ usage.resetCredits !== undefined
853
+ );
854
+ }
855
+
856
+ interface GatewayView {
857
+ enabled: boolean;
858
+ running: boolean;
859
+ bind: string;
860
+ port: number;
861
+ keyHint: string;
862
+ warning: string;
863
+ }
864
+
865
+ function parseGateway(value: unknown): GatewayView | undefined {
866
+ if (!isRecord(value)) return undefined;
867
+ const bind = optionalString(value["bind"]);
868
+ const port = optionalFiniteNumber(value["port"]);
869
+ if (bind === undefined || port === undefined) return undefined;
870
+ return {
871
+ enabled: value["enabled"] === true,
872
+ running: value["running"] === true,
873
+ bind,
874
+ port,
875
+ keyHint: optionalString(value["keyHint"]) ?? "",
876
+ warning: optionalString(value["warning"]) ?? "",
877
+ };
878
+ }
879
+
880
+ function formatGatewayBaseUrl(bind: string, port: number): string {
881
+ const host = bind.includes(":") && !bind.startsWith("[") ? `[${bind}]` : bind;
882
+ return `http://${host}:${String(port)}`;
883
+ }
884
+
885
+ const GATEWAY_PORT_MIN = 1024;
886
+ const GATEWAY_PORT_MAX = 65_535;
887
+ const GATEWAY_RANDOM_PORT_MIN = 18_100;
888
+ const GATEWAY_RANDOM_PORT_MAX = 18_999;
889
+ const GATEWAY_RANDOM_RESERVED = new Set([22, 53, 3080, 7890, 9090, 18_080]);
890
+
891
+ function randomGatewayPort(exclude?: number): number {
892
+ for (let attempt = 0; attempt < 32; attempt += 1) {
893
+ const span = GATEWAY_RANDOM_PORT_MAX - GATEWAY_RANDOM_PORT_MIN + 1;
894
+ const candidate = GATEWAY_RANDOM_PORT_MIN + Math.floor(Math.random() * span);
895
+ if (candidate !== exclude && !GATEWAY_RANDOM_RESERVED.has(candidate)) return candidate;
896
+ }
897
+ return exclude === GATEWAY_RANDOM_PORT_MIN ? GATEWAY_RANDOM_PORT_MIN + 1 : GATEWAY_RANDOM_PORT_MIN;
898
+ }
899
+
900
+ function parseGatewayPort(value: string): number | undefined {
901
+ const port = Number(value);
902
+ if (!Number.isInteger(port) || port < GATEWAY_PORT_MIN || port > GATEWAY_PORT_MAX) return undefined;
903
+ return port;
904
+ }
905
+
906
+ async function copyText(text: string): Promise<boolean> {
907
+ try {
908
+ if (typeof navigator !== "undefined" && navigator.clipboard?.writeText !== undefined) {
909
+ await navigator.clipboard.writeText(text);
910
+ return true;
911
+ }
912
+ } catch {
913
+ // fall through to execCommand
914
+ }
915
+ try {
916
+ const area = document.createElement("textarea");
917
+ area.value = text;
918
+ area.setAttribute("readonly", "");
919
+ area.style.position = "fixed";
920
+ area.style.left = "-9999px";
921
+ document.body.appendChild(area);
922
+ area.select();
923
+ const ok = document.execCommand("copy");
924
+ document.body.removeChild(area);
925
+ return ok;
926
+ } catch {
927
+ return false;
928
+ }
929
+ }
930
+
931
+ function parseImagineCredential(value: unknown): ImagineCredentialView | undefined {
932
+ if (!isRecord(value)) return undefined;
933
+ const configured = optionalBoolean(value["configured"]);
934
+ if (configured === undefined && value["source"] === undefined && value["writable"] === undefined) return undefined;
935
+ const source = optionalString(value["source"]);
936
+ const writable = optionalBoolean(value["writable"]);
937
+ return {
938
+ configured: configured === true,
939
+ ...(source === undefined || looksSecret(source) ? {} : { source }),
940
+ ...(writable === undefined ? {} : { writable }),
941
+ };
942
+ }
943
+
944
+ function imagineSourceLabel(source: string | undefined, t: GrokBuildSettingsInjected["t"]): string {
945
+ if (source === undefined) return t("imagineSourceUnknown");
946
+ const mapped = IMAGINE_SOURCE_KEY[source] ?? IMAGINE_SOURCE_KEY[source.toLowerCase()];
947
+ if (mapped !== undefined) return t(mapped);
948
+ if (source.length <= 40 && /^[a-z0-9._-]+$/iu.test(source) && !looksSecret(source)) return source;
949
+ return t("imagineSourceUnknown");
950
+ }
951
+
952
+ function methodLabel(method: LoginMethod, t: GrokBuildSettingsInjected["t"]): string {
953
+ if (method === "device") return t("deviceLogin");
954
+ if (method === "browser") return t("browserLogin");
955
+ return t("pkceLogin");
956
+ }
957
+
958
+ function modelFields(status: ProviderStatus): { available: string[]; selected: string[] } {
959
+ if (status.status !== "signed-in") return { available: [], selected: [] };
960
+ return {
961
+ available: "available" in status ? status.available : [],
962
+ selected: "selected" in status ? status.selected : [],
963
+ };
964
+ }
965
+
966
+ /** Multi-provider coding subscription status and OAuth actions. */
967
+ export function GrokBuildSettings({ t }: GrokBuildSettingsProps) {
968
+ if (t === undefined) throw new Error("Coding OAuth settings requires its translation function");
969
+ const [status, setStatus] = useState<CodingOAuthStatus | undefined>(undefined);
970
+ const [requestError, setRequestError] = useState<string | undefined>(undefined);
971
+ const [busyProvider, setBusyProvider] = useState<ProviderSlug | undefined>(undefined);
972
+ const [codeInputs, setCodeInputs] = useState<Partial<Record<ProviderSlug, string>>>({});
973
+ const [popupBlocked, setPopupBlocked] = useState<Partial<Record<ProviderSlug, boolean>>>({});
974
+ const [sources, setSources] = useState<SourceStatus[] | undefined>(undefined);
975
+ const [sourcesError, setSourcesError] = useState<string | undefined>(undefined);
976
+ const [sourcesBusy, setSourcesBusy] = useState(false);
977
+ const [preview, setPreview] = useState<SourcePreview | undefined>(undefined);
978
+ const previewRef = useRef<SourcePreview | undefined>(undefined);
979
+ const previewEpochRef = useRef(0);
980
+ const mountedRef = useRef(true);
981
+ const [confirmOverwrite, setConfirmOverwrite] = useState(false);
982
+ const [sourcesNotice, setSourcesNotice] = useState<string | undefined>(undefined);
983
+ const [capabilities, setCapabilities] = useState<CapabilitySnapshot | undefined>(undefined);
984
+ const [capabilitiesError, setCapabilitiesError] = useState<string | undefined>(undefined);
985
+ const [capabilitiesBusy, setCapabilitiesBusy] = useState(false);
986
+ const [usage, setUsage] = useState<UsageView | undefined>(undefined);
987
+ const [usageError, setUsageError] = useState<string | undefined>(undefined);
988
+ const [usageLoading, setUsageLoading] = useState(false);
989
+ const [imagine, setImagine] = useState<ImagineCredentialView | undefined>(undefined);
990
+ const [imagineError, setImagineError] = useState<string | undefined>(undefined);
991
+ const [gateway, setGateway] = useState<GatewayView | undefined>(undefined);
992
+ const [gatewayError, setGatewayError] = useState<string | undefined>(undefined);
993
+ const [gatewayBusy, setGatewayBusy] = useState(false);
994
+ const [gatewayOnceKey, setGatewayOnceKey] = useState<string | undefined>(undefined);
995
+ const [gatewayKeyVisible, setGatewayKeyVisible] = useState(false);
996
+ const [gatewayRotateConfirm, setGatewayRotateConfirm] = useState(false);
997
+ const [gatewayRevealError, setGatewayRevealError] = useState<string | undefined>(undefined);
998
+ const [portDraft, setPortDraft] = useState("");
999
+ const [activeTab, setActiveTab] = useState<SettingsTabId>("accounts");
1000
+ const [copiedField, setCopiedField] = useState<CopyField | undefined>(undefined);
1001
+ const [copyFailedField, setCopyFailedField] = useState<CopyField | undefined>(undefined);
1002
+ const [expandedProviders, setExpandedProviders] = useState<Partial<Record<ProviderSlug, boolean>>>({});
1003
+ const copiedTimerRef = useRef<number | undefined>(undefined);
1004
+
1005
+ const refresh = useCallback(async () => {
1006
+ try {
1007
+ setStatus(await jsonRequest<CodingOAuthStatus>(STATUS_PATH));
1008
+ setRequestError(undefined);
1009
+ } catch (error: unknown) {
1010
+ setRequestError(error instanceof Error ? error.message : t("requestFailed"));
1011
+ }
1012
+ }, [t]);
1013
+
1014
+ const refreshSources = useCallback(async () => {
1015
+ try {
1016
+ setSources(parseSources(await jsonRequest<unknown>(SOURCES_PATH)));
1017
+ setSourcesError(undefined);
1018
+ } catch (error: unknown) {
1019
+ setSources(mergeSources([]));
1020
+ setSourcesError(error instanceof Error ? error.message : t("sourcesLoadFailed"));
1021
+ }
1022
+ }, [t]);
1023
+
1024
+ const refreshCapabilities = useCallback(async () => {
1025
+ try {
1026
+ const parsed = parseCapabilities(await jsonRequest<unknown>(CAPABILITIES_PATH));
1027
+ setCapabilities(parsed ?? { value: emptyCapabilitySettings(), revision: 0, writable: false });
1028
+ setCapabilitiesError(undefined);
1029
+ return parsed;
1030
+ } catch (error: unknown) {
1031
+ setCapabilities({ value: emptyCapabilitySettings(), revision: 0, writable: false });
1032
+ setCapabilitiesError(error instanceof Error ? error.message : t("capabilitiesLoadFailed"));
1033
+ return undefined;
1034
+ }
1035
+ }, [t]);
1036
+
1037
+ const refreshGateway = useCallback(async () => {
1038
+ try {
1039
+ setGateway(parseGateway(await jsonRequest<unknown>(GATEWAY_PATH)));
1040
+ setGatewayError(undefined);
1041
+ } catch (error: unknown) {
1042
+ setGatewayError(error instanceof Error ? error.message : t("gatewayLoadFailed"));
1043
+ }
1044
+ }, [t]);
1045
+
1046
+ const refreshImagine = useCallback(async () => {
1047
+ try {
1048
+ setImagine(parseImagineCredential(await jsonRequest<unknown>(IMAGINE_CREDENTIAL_PATH)));
1049
+ setImagineError(undefined);
1050
+ } catch (error: unknown) {
1051
+ setImagineError(error instanceof Error ? error.message : t("imagineLoadFailed"));
1052
+ }
1053
+ }, [t]);
1054
+
1055
+ const refreshUsage = useCallback(async () => {
1056
+ setUsageLoading(true);
1057
+ try {
1058
+ setUsage(parseUsage(await jsonRequest<unknown>(CODEX_USAGE_PATH)));
1059
+ setUsageError(undefined);
1060
+ } catch (error: unknown) {
1061
+ setUsage(undefined);
1062
+ setUsageError(error instanceof Error ? error.message : t("usageUnavailable"));
1063
+ } finally {
1064
+ setUsageLoading(false);
1065
+ }
1066
+ }, [t]);
1067
+
1068
+ useEffect(() => {
1069
+ void refresh();
1070
+ void refreshSources();
1071
+ void refreshCapabilities();
1072
+ void refreshImagine();
1073
+ void refreshGateway();
1074
+ }, [refresh, refreshSources, refreshCapabilities, refreshImagine, refreshGateway]);
1075
+ useEffect(() => {
1076
+ previewRef.current = preview;
1077
+ }, [preview]);
1078
+ useEffect(() => {
1079
+ mountedRef.current = true;
1080
+ return () => {
1081
+ mountedRef.current = false;
1082
+ if (copiedTimerRef.current !== undefined) window.clearTimeout(copiedTimerRef.current);
1083
+ previewEpochRef.current += 1;
1084
+ const active = previewRef.current;
1085
+ if (active !== undefined) cancelPreviewTicket(active.previewId, true);
1086
+ };
1087
+ }, []);
1088
+ useEffect(() => {
1089
+ const signingIn =
1090
+ status !== undefined && Object.values(status.providers).some((provider) => provider.status === "signing-in");
1091
+ if (!signingIn) return;
1092
+ const timer = window.setInterval(() => {
1093
+ void refresh();
1094
+ }, POLL_INTERVAL_MS);
1095
+ return () => {
1096
+ window.clearInterval(timer);
1097
+ };
1098
+ }, [refresh, status]);
1099
+ useEffect(() => {
1100
+ const signedIn = status?.providers.codex.status === "signed-in";
1101
+ if (capabilities?.value.codexUsage === true && signedIn) {
1102
+ void refreshUsage();
1103
+ return;
1104
+ }
1105
+ setUsage(undefined);
1106
+ setUsageError(undefined);
1107
+ setUsageLoading(false);
1108
+ }, [capabilities?.value.codexUsage, refreshUsage, status?.providers.codex.status]);
1109
+
1110
+ const signIn = async (provider: ProviderSlug, method: LoginMethod): Promise<void> => {
1111
+ const popup = window.open("about:blank", "_blank");
1112
+ if (popup !== null) popup.opener = null;
1113
+ setBusyProvider(provider);
1114
+ setRequestError(undefined);
1115
+ setPopupBlocked((current) => ({ ...current, [provider]: popup === null }));
1116
+ try {
1117
+ const challenge = await jsonRequest<LoginChallenge>(LOGIN_PATH, "POST", { provider, method });
1118
+ if (popup !== null) popup.location.replace(challenge.url);
1119
+ await refresh();
1120
+ } catch (error: unknown) {
1121
+ popup?.close();
1122
+ setRequestError(error instanceof Error ? error.message : t("requestFailed"));
1123
+ await refresh();
1124
+ } finally {
1125
+ setBusyProvider(undefined);
1126
+ }
1127
+ };
1128
+
1129
+ const submitCode = async (provider: ProviderSlug): Promise<void> => {
1130
+ const code = codeInputs[provider]?.trim() ?? "";
1131
+ if (code.length === 0) return;
1132
+ setBusyProvider(provider);
1133
+ try {
1134
+ await jsonRequest<{ ok: true }>(LOGIN_CODE_PATH, "POST", { provider, code });
1135
+ setCodeInputs((current) => ({ ...current, [provider]: "" }));
1136
+ await refresh();
1137
+ } catch (error: unknown) {
1138
+ setRequestError(error instanceof Error ? error.message : t("requestFailed"));
1139
+ } finally {
1140
+ setBusyProvider(undefined);
1141
+ }
1142
+ };
1143
+
1144
+ const cancelLogin = async (provider: ProviderSlug): Promise<void> => {
1145
+ setBusyProvider(provider);
1146
+ try {
1147
+ setStatus(await jsonRequest<CodingOAuthStatus>(LOGIN_CANCEL_PATH, "POST", { provider }));
1148
+ } catch (error: unknown) {
1149
+ setRequestError(error instanceof Error ? error.message : t("requestFailed"));
1150
+ } finally {
1151
+ setBusyProvider(undefined);
1152
+ }
1153
+ };
1154
+
1155
+ const signOut = async (provider: ProviderSlug): Promise<void> => {
1156
+ setBusyProvider(provider);
1157
+ try {
1158
+ setStatus(await jsonRequest<CodingOAuthStatus>(LOGOUT_PATH, "POST", { provider }));
1159
+ } catch (error: unknown) {
1160
+ setRequestError(error instanceof Error ? error.message : t("requestFailed"));
1161
+ } finally {
1162
+ setBusyProvider(undefined);
1163
+ }
1164
+ };
1165
+
1166
+ const saveModels = async (provider: ProviderSlug, selected: string[]): Promise<void> => {
1167
+ setBusyProvider(provider);
1168
+ try {
1169
+ setStatus(await jsonRequest<CodingOAuthStatus>(MODELS_PATH, "POST", { provider, selected }));
1170
+ } catch (error: unknown) {
1171
+ setRequestError(error instanceof Error ? error.message : t("requestFailed"));
1172
+ } finally {
1173
+ setBusyProvider(undefined);
1174
+ }
1175
+ };
1176
+
1177
+ const previewSource = async (kind: SourceKind): Promise<void> => {
1178
+ const epoch = ++previewEpochRef.current;
1179
+ const previous = previewRef.current;
1180
+ if (previous !== undefined) {
1181
+ previewRef.current = undefined;
1182
+ setPreview(undefined);
1183
+ cancelPreviewTicket(previous.previewId);
1184
+ }
1185
+ setSourcesBusy(true);
1186
+ setSourcesNotice(undefined);
1187
+ setConfirmOverwrite(false);
1188
+ try {
1189
+ const next = parsePreview(await jsonRequest<unknown>(SOURCES_PREVIEW_PATH, "POST", { kind }));
1190
+ if (next === undefined) throw new Error(t("sourcesPreviewFailed"));
1191
+ if (!mountedRef.current || epoch !== previewEpochRef.current) {
1192
+ cancelPreviewTicket(next.previewId, !mountedRef.current);
1193
+ return;
1194
+ }
1195
+ previewRef.current = next;
1196
+ setPreview(next);
1197
+ setSourcesError(undefined);
1198
+ } catch (error: unknown) {
1199
+ if (!mountedRef.current || epoch !== previewEpochRef.current) return;
1200
+ setPreview(undefined);
1201
+ setSourcesError(error instanceof Error ? error.message : t("sourcesPreviewFailed"));
1202
+ } finally {
1203
+ if (mountedRef.current && epoch === previewEpochRef.current) setSourcesBusy(false);
1204
+ }
1205
+ };
1206
+
1207
+ const commitSource = async (): Promise<void> => {
1208
+ if (preview === undefined) return;
1209
+ if (preview.action === "blocked") return;
1210
+ if (preview.confirmOverwriteRequired && !confirmOverwrite) return;
1211
+ setSourcesBusy(true);
1212
+ try {
1213
+ const result = await jsonRequest<unknown>(SOURCES_COMMIT_PATH, "POST", {
1214
+ kind: preview.kind,
1215
+ previewId: preview.previewId,
1216
+ confirmOverwrite,
1217
+ });
1218
+ const action = parseCommitAction(result);
1219
+ previewRef.current = undefined;
1220
+ setPreview(undefined);
1221
+ setConfirmOverwrite(false);
1222
+ setSourcesNotice(
1223
+ t("sourcesCommitSuccess", { action: action === undefined ? "" : t(SOURCE_COMMIT_ACTION_KEY[action]) }),
1224
+ );
1225
+ setSourcesError(undefined);
1226
+ await refreshSources();
1227
+ await refresh();
1228
+ } catch (error: unknown) {
1229
+ if (isConsumedPreviewError(error)) {
1230
+ previewRef.current = undefined;
1231
+ setPreview(undefined);
1232
+ setConfirmOverwrite(false);
1233
+ }
1234
+ setSourcesError(error instanceof Error ? error.message : t("sourcesCommitFailed"));
1235
+ await refreshSources();
1236
+ } finally {
1237
+ setSourcesBusy(false);
1238
+ }
1239
+ };
1240
+
1241
+ const cancelSourcePreview = async (): Promise<void> => {
1242
+ if (preview === undefined) return;
1243
+ const previewId = preview.previewId;
1244
+ previewEpochRef.current += 1;
1245
+ previewRef.current = undefined;
1246
+ setPreview(undefined);
1247
+ setConfirmOverwrite(false);
1248
+ setSourcesBusy(true);
1249
+ try {
1250
+ await jsonRequest<unknown>(SOURCES_CANCEL_PATH, "POST", { previewId });
1251
+ setSourcesError(undefined);
1252
+ } catch (error: unknown) {
1253
+ setSourcesError(error instanceof Error ? error.message : t("requestFailed"));
1254
+ } finally {
1255
+ setSourcesBusy(false);
1256
+ }
1257
+ };
1258
+
1259
+ const patchCapability = async (key: CapabilitySettingKey, value: boolean | number): Promise<boolean> => {
1260
+ if (capabilities === undefined || !capabilities.writable) return false;
1261
+ if (key === "codexImageEdits" && value === true && !capabilities.value.codexImages) return false;
1262
+ const patch: Partial<CapabilitySettingsView> =
1263
+ key === "codexImages" && value === false && capabilities.value.codexImageEdits
1264
+ ? { codexImages: false, codexImageEdits: false }
1265
+ : ({ [key]: value } as Partial<CapabilitySettingsView>);
1266
+ setCapabilitiesBusy(true);
1267
+ try {
1268
+ const updated = parseCapabilities(
1269
+ await jsonRequest<unknown>(CAPABILITIES_PATH, "PATCH", {
1270
+ expectedRevision: capabilities.revision,
1271
+ patch,
1272
+ }),
1273
+ );
1274
+ if (updated !== undefined) setCapabilities(updated);
1275
+ else await refreshCapabilities();
1276
+ setCapabilitiesError(undefined);
1277
+ return true;
1278
+ } catch (error: unknown) {
1279
+ await refreshCapabilities();
1280
+ setCapabilitiesError(
1281
+ isConflictError(error)
1282
+ ? t("capabilitiesConflictRefreshed")
1283
+ : error instanceof Error
1284
+ ? error.message
1285
+ : t("capabilitiesSaveFailed"),
1286
+ );
1287
+ return false;
1288
+ } finally {
1289
+ setCapabilitiesBusy(false);
1290
+ }
1291
+ };
1292
+
1293
+ const showUsage = capabilities?.value.codexUsage === true && status?.providers.codex.status === "signed-in";
1294
+
1295
+ const markCopied = (field: CopyField): void => {
1296
+ if (copiedTimerRef.current !== undefined) window.clearTimeout(copiedTimerRef.current);
1297
+ setCopiedField(field);
1298
+ setCopyFailedField(undefined);
1299
+ copiedTimerRef.current = window.setTimeout(() => {
1300
+ if (mountedRef.current) {
1301
+ setCopiedField(undefined);
1302
+ setCopyFailedField(undefined);
1303
+ }
1304
+ copiedTimerRef.current = undefined;
1305
+ }, 2000);
1306
+ };
1307
+
1308
+ const handleCopy = async (field: CopyField, text: string): Promise<void> => {
1309
+ const ok = await copyText(text);
1310
+ if (ok) {
1311
+ markCopied(field);
1312
+ return;
1313
+ }
1314
+ setCopiedField(undefined);
1315
+ setCopyFailedField(field);
1316
+ };
1317
+
1318
+ const copyLabel = (field: CopyField, idle?: string): string => {
1319
+ if (copiedField === field) return t("copied");
1320
+ if (copyFailedField === field) return t("copyFailed");
1321
+ return idle ?? t("copy");
1322
+ };
1323
+
1324
+ const ensureGatewayKey = async (): Promise<string> => {
1325
+ if (gatewayOnceKey !== undefined) return gatewayOnceKey;
1326
+ const value = await jsonRequest<{ apiKey?: string }>(GATEWAY_REVEAL_PATH, "POST");
1327
+ if (typeof value.apiKey !== "string" || value.apiKey.length === 0) {
1328
+ throw new Error(t("gatewayRevealFailed"));
1329
+ }
1330
+ setGatewayOnceKey(value.apiKey);
1331
+ return value.apiKey;
1332
+ };
1333
+
1334
+ const copyGatewayKey = async (): Promise<void> => {
1335
+ try {
1336
+ const key = await ensureGatewayKey();
1337
+ await handleCopy("key", key);
1338
+ setGatewayRevealError(undefined);
1339
+ } catch {
1340
+ setGatewayRevealError(t("gatewayRevealFailed"));
1341
+ }
1342
+ };
1343
+
1344
+ const toggleGatewayKeyVisible = async (): Promise<void> => {
1345
+ if (gatewayKeyVisible) {
1346
+ setGatewayKeyVisible(false);
1347
+ return;
1348
+ }
1349
+ try {
1350
+ await ensureGatewayKey();
1351
+ setGatewayKeyVisible(true);
1352
+ setGatewayRevealError(undefined);
1353
+ } catch {
1354
+ setGatewayRevealError(t("gatewayRevealFailed"));
1355
+ }
1356
+ };
1357
+
1358
+ useEffect(() => {
1359
+ if (gateway !== undefined) setPortDraft(String(gateway.port));
1360
+ }, [gateway]);
1361
+
1362
+ const applyGatewayPort = async (): Promise<void> => {
1363
+ const port = parseGatewayPort(portDraft);
1364
+ if (port === undefined) {
1365
+ setGatewayError(t("gatewayPortInvalid"));
1366
+ return;
1367
+ }
1368
+ if (gateway !== undefined && port === gateway.port) return;
1369
+ setGatewayBusy(true);
1370
+ try {
1371
+ setGateway(parseGateway(await jsonRequest<unknown>(GATEWAY_PATH, "PATCH", { port })) ?? gateway);
1372
+ setGatewayError(undefined);
1373
+ } catch (error: unknown) {
1374
+ setGatewayError(error instanceof Error ? error.message : t("gatewaySaveFailed"));
1375
+ if (gateway !== undefined) setPortDraft(String(gateway.port));
1376
+ } finally {
1377
+ setGatewayBusy(false);
1378
+ }
1379
+ };
1380
+
1381
+ const rotateGatewayKey = async (): Promise<void> => {
1382
+ setGatewayBusy(true);
1383
+ try {
1384
+ const value = await jsonRequest<{ apiKey?: string; keyHint?: string }>(GATEWAY_ROTATE_PATH, "POST");
1385
+ if (typeof value.apiKey === "string") setGatewayOnceKey(value.apiKey);
1386
+ setGatewayKeyVisible(true);
1387
+ setGatewayRotateConfirm(false);
1388
+ setGatewayRevealError(undefined);
1389
+ await refreshGateway();
1390
+ } catch (error: unknown) {
1391
+ setGatewayError(error instanceof Error ? error.message : t("gatewaySaveFailed"));
1392
+ } finally {
1393
+ setGatewayBusy(false);
1394
+ }
1395
+ };
1396
+
1397
+ return (
1398
+ <section style={pageStyle} aria-labelledby="coding-oauth-settings-title">
1399
+ <div>
1400
+ <h2 id="coding-oauth-settings-title" style={titleStyle}>
1401
+ {t("title")}
1402
+ </h2>
1403
+ <p style={{ ...bodyStyle, marginTop: 6 }}>{t("intro")}</p>
1404
+ </div>
1405
+ {requestError === undefined ? null : (
1406
+ <p style={errorStyle} role="alert">
1407
+ {requestError}
1408
+ </p>
1409
+ )}
1410
+ <nav style={tabNavStyle} aria-label={t("title")}>
1411
+ {SETTINGS_TABS.map((tab) => (
1412
+ <button
1413
+ key={tab.id}
1414
+ type="button"
1415
+ style={activeTab === tab.id ? tabButtonActiveStyle : tabButtonStyle}
1416
+ aria-current={activeTab === tab.id ? "page" : undefined}
1417
+ onClick={() => {
1418
+ setActiveTab(tab.id);
1419
+ }}
1420
+ >
1421
+ {t(tab.label)}
1422
+ </button>
1423
+ ))}
1424
+ </nav>
1425
+ <div style={panelStyle}>
1426
+ {activeTab === "accounts" ? (
1427
+ status === undefined ? (
1428
+ <div style={cardStyle}>
1429
+ <div style={statusStyle}>
1430
+ <span aria-hidden="true" style={dotStyle("loading")} />
1431
+ {t("loadingAccount")}
1432
+ </div>
1433
+ </div>
1434
+ ) : (
1435
+ <>
1436
+ {sourcesError === undefined ? null : (
1437
+ <p style={errorStyle} role="alert">
1438
+ {sourcesError}
1439
+ </p>
1440
+ )}
1441
+ {sourcesNotice === undefined ? null : (
1442
+ <p style={bodyStyle} role="status">
1443
+ {sourcesNotice}
1444
+ </p>
1445
+ )}
1446
+ <div style={accountGridStyle}>
1447
+ {PROVIDERS.map((definition) => {
1448
+ const providerStatus = status.providers[definition.slug];
1449
+ const grokProviderStatus = definition.slug === "grok" ? (providerStatus as GrokStatus) : undefined;
1450
+ const busy = busyProvider === definition.slug;
1451
+ const statusLabel =
1452
+ providerStatus.status === "signed-in"
1453
+ ? t("signedIn")
1454
+ : providerStatus.status === "signing-in"
1455
+ ? t("signingIn")
1456
+ : providerStatus.status === "error"
1457
+ ? t("requestFailed")
1458
+ : t("signedOut");
1459
+ const activeMethod =
1460
+ providerStatus.status === "signing-in" ? providerStatus.method : definition.recommended;
1461
+ const { available, selected } = modelFields(providerStatus);
1462
+ const localCode = codeInputs[definition.slug] ?? "";
1463
+ const source = sources?.find((entry) => entry.kind === definition.slug);
1464
+ const expanded =
1465
+ providerStatus.status === "signing-in" || expandedProviders[definition.slug] === true;
1466
+ const usagePercent =
1467
+ definition.slug === "codex" && showUsage
1468
+ ? usage?.individualRemainingPercent === undefined
1469
+ ? usage?.rateLimits[0]?.windows[0]?.usedPercent
1470
+ : 100 - usage.individualRemainingPercent
1471
+ : undefined;
1472
+ return (
1473
+ <div key={definition.slug} style={cardStyle}>
1474
+ <div style={rowStyle}>
1475
+ <div>
1476
+ <h3 style={{ ...titleStyle, fontSize: 16 }}>{t(definition.titleKey)}</h3>
1477
+ {providerStatus.status === "signed-in" && !expanded ? (
1478
+ <p style={{ ...hintStyle, marginTop: 4 }}>
1479
+ {t("modelsSummary", { selected: selected.length, total: available.length })}
1480
+ {usagePercent === undefined
1481
+ ? ""
1482
+ : ` · ${t("usageUsedShort", { value: `${String(usagePercent)}%` })}`}
1483
+ </p>
1484
+ ) : (
1485
+ <>
1486
+ <p style={{ ...bodyStyle, marginTop: 4 }}>{t(definition.descriptionKey)}</p>
1487
+ <p style={{ ...bodyStyle, marginTop: 4 }}>
1488
+ <span style={monoStyle}>{definition.route}</span>
1489
+ </p>
1490
+ </>
1491
+ )}
1492
+ </div>
1493
+ <div style={statusStyle} role="status">
1494
+ <span aria-hidden="true" style={dotStyle(providerStatus.status)} />
1495
+ <span>{statusLabel}</span>
1496
+ </div>
1497
+ </div>
1498
+ <div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
1499
+ {providerStatus.status === "signed-in" ? (
1500
+ <>
1501
+ <button
1502
+ type="button"
1503
+ style={buttonStyle}
1504
+ disabled={busy}
1505
+ onClick={() => {
1506
+ void signOut(definition.slug);
1507
+ }}
1508
+ >
1509
+ {busy ? t("working") : t("logout")}
1510
+ </button>
1511
+ <button
1512
+ type="button"
1513
+ style={buttonStyle}
1514
+ onClick={() => {
1515
+ setExpandedProviders((current) => ({
1516
+ ...current,
1517
+ [definition.slug]: !expanded,
1518
+ }));
1519
+ }}
1520
+ >
1521
+ {expanded ? t("collapseModels") : t("expandModels")}
1522
+ </button>
1523
+ {source?.available === true ? (
1524
+ <button
1525
+ type="button"
1526
+ style={buttonStyle}
1527
+ disabled={sourcesBusy}
1528
+ onClick={() => {
1529
+ void previewSource(definition.slug);
1530
+ }}
1531
+ >
1532
+ {t("sourcesPullCopy")}
1533
+ </button>
1534
+ ) : null}
1535
+ </>
1536
+ ) : providerStatus.status === "signing-in" ? (
1537
+ <>
1538
+ {definition.methods
1539
+ .filter((method) => method !== activeMethod)
1540
+ .map((method) => (
1541
+ <button
1542
+ key={method}
1543
+ type="button"
1544
+ style={buttonStyle}
1545
+ disabled={busy}
1546
+ onClick={() => {
1547
+ void signIn(definition.slug, method);
1548
+ }}
1549
+ >
1550
+ {methodLabel(method, t)}
1551
+ </button>
1552
+ ))}
1553
+ <button
1554
+ type="button"
1555
+ style={buttonStyle}
1556
+ disabled={busy}
1557
+ onClick={() => {
1558
+ void cancelLogin(definition.slug);
1559
+ }}
1560
+ >
1561
+ {t("cancelLogin")}
1562
+ </button>
1563
+ </>
1564
+ ) : (
1565
+ <>
1566
+ {definition.methods.map((method, index) => (
1567
+ <button
1568
+ key={method}
1569
+ type="button"
1570
+ style={index === 0 ? primaryButtonStyle : buttonStyle}
1571
+ disabled={busy}
1572
+ onClick={() => {
1573
+ void signIn(definition.slug, method);
1574
+ }}
1575
+ >
1576
+ {busy ? t("working") : methodLabel(method, t)}
1577
+ </button>
1578
+ ))}
1579
+ {source?.available === true ? (
1580
+ <button
1581
+ type="button"
1582
+ style={buttonStyle}
1583
+ disabled={sourcesBusy}
1584
+ onClick={() => {
1585
+ void previewSource(definition.slug);
1586
+ }}
1587
+ >
1588
+ {t("sourcesPullCopy")}
1589
+ </button>
1590
+ ) : source !== undefined && source.reason !== undefined ? (
1591
+ <span style={hintStyle}>{t(SOURCE_REASON_KEY[source.reason])}</span>
1592
+ ) : null}
1593
+ </>
1594
+ )}
1595
+ </div>
1596
+ {providerStatus.status === "error" ? <p style={errorStyle}>{providerStatus.message}</p> : null}
1597
+ {providerStatus.status === "signing-in" && providerStatus.userCode !== undefined ? (
1598
+ <p style={bodyStyle}>
1599
+ {t("userCode")} <span style={codeStyle}>{providerStatus.userCode}</span>
1600
+ </p>
1601
+ ) : null}
1602
+ {providerStatus.status === "signing-in" && providerStatus.url !== undefined ? (
1603
+ <p style={bodyStyle}>
1604
+ {popupBlocked[definition.slug] === true ? t("popupBlocked") : t("openUrl")}{" "}
1605
+ <a href={providerStatus.url} target="_blank" rel="noreferrer" style={linkStyle}>
1606
+ {providerStatus.url}
1607
+ </a>
1608
+ </p>
1609
+ ) : null}
1610
+ {providerStatus.status === "signing-in" && activeMethod !== "device" ? (
1611
+ <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
1612
+ <p style={bodyStyle}>
1613
+ {t(activeMethod === "browser" ? "pasteBrowserCodeHint" : "pasteCodeHint")}
1614
+ </p>
1615
+ <div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
1616
+ <input
1617
+ style={{ ...inputStyle, flex: "1 1 360px" }}
1618
+ value={localCode}
1619
+ placeholder={t("pasteCodePlaceholder")}
1620
+ disabled={busy}
1621
+ onChange={(event) =>
1622
+ setCodeInputs((current) => ({ ...current, [definition.slug]: event.target.value }))
1623
+ }
1624
+ onKeyDown={(event) => {
1625
+ if (event.key === "Enter") {
1626
+ event.preventDefault();
1627
+ void submitCode(definition.slug);
1628
+ }
1629
+ }}
1630
+ />
1631
+ <button
1632
+ type="button"
1633
+ style={primaryButtonStyle}
1634
+ disabled={busy || localCode.trim().length === 0}
1635
+ onClick={() => {
1636
+ void submitCode(definition.slug);
1637
+ }}
1638
+ >
1639
+ {t("submitCode")}
1640
+ </button>
1641
+ </div>
1642
+ </div>
1643
+ ) : null}
1644
+ {providerStatus.status === "signed-in" && expanded ? (
1645
+ <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
1646
+ <div style={rowStyle}>
1647
+ <h4 style={{ ...titleStyle, fontSize: 14 }}>{t("models")}</h4>
1648
+ <button
1649
+ type="button"
1650
+ style={buttonStyle}
1651
+ disabled={busy}
1652
+ onClick={() => {
1653
+ void saveModels(definition.slug, []);
1654
+ }}
1655
+ >
1656
+ {t("selectAll")}
1657
+ </button>
1658
+ </div>
1659
+ {grokProviderStatus?.status === "signed-in" ? (
1660
+ <p style={bodyStyle}>
1661
+ {grokProviderStatus.catalogSource === "live"
1662
+ ? t("catalogLive")
1663
+ : grokProviderStatus.catalogSource === "cache"
1664
+ ? t("catalogCache")
1665
+ : t("catalogFallback")}
1666
+ </p>
1667
+ ) : null}
1668
+ <p style={bodyStyle}>
1669
+ {t("modelHint")} <span style={monoStyle}>{definition.route}/&lt;id&gt;</span>
1670
+ </p>
1671
+ <ul style={listStyle}>
1672
+ {available.map((id) => {
1673
+ const checked = selected.includes(id);
1674
+ return (
1675
+ <li key={id}>
1676
+ <label style={checkRowStyle}>
1677
+ <input
1678
+ type="checkbox"
1679
+ checked={checked}
1680
+ disabled={busy}
1681
+ onChange={() => {
1682
+ const current = new Set(selected);
1683
+ if (checked) current.delete(id);
1684
+ else current.add(id);
1685
+ void saveModels(definition.slug, [...current]);
1686
+ }}
1687
+ />
1688
+ <span style={monoStyle}>{id}</span>
1689
+ </label>
1690
+ </li>
1691
+ );
1692
+ })}
1693
+ </ul>
1694
+ {grokProviderStatus?.status === "signed-in" &&
1695
+ grokProviderStatus.catalogError !== undefined ? (
1696
+ <p style={errorStyle}>{t("catalogError")}</p>
1697
+ ) : null}
1698
+ {definition.slug === "codex" && showUsage ? (
1699
+ <div style={nestedStyle}>
1700
+ <p style={{ ...bodyStyle, color: "var(--dsw-alias-label-primary)" }}>{t("usageTitle")}</p>
1701
+ {usageError === undefined ? null : (
1702
+ <p style={errorStyle} role="alert">
1703
+ {usageError}
1704
+ </p>
1705
+ )}
1706
+ {usageLoading && usage === undefined ? (
1707
+ <p style={hintStyle}>{t("usageLoading")}</p>
1708
+ ) : usage === undefined || !usageHasVisibleFields(usage) ? (
1709
+ <p style={hintStyle}>{t("usageEmpty")}</p>
1710
+ ) : (
1711
+ <>
1712
+ {formatEpoch(usage.fetchedAt) === undefined ? null : (
1713
+ <p style={hintStyle}>
1714
+ {t("usageFetchedAt", { time: formatEpoch(usage.fetchedAt) })}
1715
+ </p>
1716
+ )}
1717
+ {usage.rateLimits.map((limit) => (
1718
+ <p key={limit.id} style={hintStyle}>
1719
+ {limit.name ?? t("usageRateLimit")}
1720
+ {limit.windows[0]?.usedPercent === undefined
1721
+ ? ""
1722
+ : ` · ${t("usageUsed", { value: `${String(limit.windows[0].usedPercent)}%` })}`}
1723
+ {formatEpoch(limit.windows[0]?.resetsAt) === undefined
1724
+ ? ""
1725
+ : ` · ${t("usageResets", { time: formatEpoch(limit.windows[0]?.resetsAt) })}`}
1726
+ </p>
1727
+ ))}
1728
+ </>
1729
+ )}
1730
+ </div>
1731
+ ) : null}
1732
+ </div>
1733
+ ) : null}
1734
+ </div>
1735
+ );
1736
+ })}
1737
+ </div>
1738
+ {preview === undefined ? null : (
1739
+ <div style={cardStyle} aria-live="polite">
1740
+ <p style={bodyStyle}>
1741
+ {t("sourcesPreviewTitle")} · {t(SOURCE_KIND_KEY[preview.kind])}
1742
+ </p>
1743
+ <p style={hintStyle}>
1744
+ <span style={monoStyle}>{preview.displayPath}</span>
1745
+ </p>
1746
+ <p style={bodyStyle}>
1747
+ {t("sourcesConflict", {
1748
+ detail: t(
1749
+ preview.conflict === undefined
1750
+ ? "sourceConflictUnrecognized"
1751
+ : SOURCE_CONFLICT_KEY[preview.conflict],
1752
+ ),
1753
+ })}
1754
+ </p>
1755
+ <p style={bodyStyle}>
1756
+ {t("sourcesAction", {
1757
+ detail: t(
1758
+ preview.action === undefined
1759
+ ? "sourceActionUnrecognized"
1760
+ : SOURCE_PREVIEW_ACTION_KEY[preview.action],
1761
+ ),
1762
+ })}
1763
+ </p>
1764
+ {formatEpoch(preview.expiresAt) === undefined ? null : (
1765
+ <p style={hintStyle}>{t("sourcesPreviewExpires", { time: formatEpoch(preview.expiresAt) })}</p>
1766
+ )}
1767
+ {formatEpoch(preview.ticketExpiresAt) === undefined ? null : (
1768
+ <p style={hintStyle}>{t("sourcesTicketExpires", { time: formatEpoch(preview.ticketExpiresAt) })}</p>
1769
+ )}
1770
+ {preview.warnings.length === 0 ? null : (
1771
+ <ul style={{ ...listStyle, gap: 4 }} aria-label={t("sourcesWarnings")}>
1772
+ {preview.warnings.map((warning) => (
1773
+ <li key={warning} style={hintStyle}>
1774
+ {warning}
1775
+ </li>
1776
+ ))}
1777
+ </ul>
1778
+ )}
1779
+ {preview.confirmOverwriteRequired ? (
1780
+ <label style={checkRowStyle}>
1781
+ <input
1782
+ type="checkbox"
1783
+ checked={confirmOverwrite}
1784
+ disabled={sourcesBusy || preview.action === "blocked"}
1785
+ onChange={(event) => setConfirmOverwrite(event.target.checked)}
1786
+ />
1787
+ <span>
1788
+ {t("sourcesConfirmOverwrite")}
1789
+ <span style={{ display: "block", ...hintStyle }}>{t("sourcesConfirmOverwriteHint")}</span>
1790
+ </span>
1791
+ </label>
1792
+ ) : null}
1793
+ <div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
1794
+ <button
1795
+ type="button"
1796
+ style={primaryButtonStyle}
1797
+ disabled={
1798
+ sourcesBusy ||
1799
+ preview.action === "blocked" ||
1800
+ (preview.confirmOverwriteRequired && !confirmOverwrite)
1801
+ }
1802
+ onClick={() => {
1803
+ void commitSource();
1804
+ }}
1805
+ >
1806
+ {t("sourcesCommit")}
1807
+ </button>
1808
+ <button
1809
+ type="button"
1810
+ style={buttonStyle}
1811
+ disabled={sourcesBusy}
1812
+ onClick={() => {
1813
+ void cancelSourcePreview();
1814
+ }}
1815
+ >
1816
+ {t("sourcesCancelPreview")}
1817
+ </button>
1818
+ </div>
1819
+ </div>
1820
+ )}
1821
+ </>
1822
+ )
1823
+ ) : null}
1824
+ {activeTab === "capabilities" ? (
1825
+ <section style={cardStyle} aria-labelledby="coding-oauth-capabilities-title">
1826
+ <div>
1827
+ <h3 id="coding-oauth-capabilities-title" style={{ ...titleStyle, fontSize: 16 }}>
1828
+ {t("capabilitiesTitle")}
1829
+ </h3>
1830
+ <p style={{ ...bodyStyle, marginTop: 4 }}>{t("capabilitiesIntro")}</p>
1831
+ <p style={{ ...hintStyle, marginTop: 4 }}>{t("capabilitiesQuotaHint")}</p>
1832
+ </div>
1833
+ {imagineError === undefined ? null : (
1834
+ <p style={errorStyle} role="alert">
1835
+ {imagineError}
1836
+ </p>
1837
+ )}
1838
+ {imagine === undefined && imagineError === undefined ? (
1839
+ <div style={statusStyle} role="status">
1840
+ <span aria-hidden="true" style={dotStyle("loading")} />
1841
+ {t("imagineLoading")}
1842
+ </div>
1843
+ ) : imagine === undefined ? null : (
1844
+ <div style={nestedStyle}>
1845
+ <p style={statusStyle} role="status">
1846
+ <span aria-hidden="true" style={dotStyle(imagine.configured ? "available" : "unavailable")} />
1847
+ <span>{imagine.configured ? t("imagineConfigured") : t("imagineNotConfigured")}</span>
1848
+ </p>
1849
+ <p style={hintStyle}>{t("imagineSource", { source: imagineSourceLabel(imagine.source, t) })}</p>
1850
+ </div>
1851
+ )}
1852
+ {capabilitiesError === undefined ? null : (
1853
+ <p style={errorStyle} role="alert">
1854
+ {capabilitiesError}
1855
+ </p>
1856
+ )}
1857
+ {capabilities === undefined ? (
1858
+ <div style={statusStyle} role="status">
1859
+ <span aria-hidden="true" style={dotStyle("loading")} />
1860
+ {t("capabilitiesLoading")}
1861
+ </div>
1862
+ ) : (
1863
+ <fieldset style={{ border: 0, margin: 0, padding: 0, minWidth: 0 }}>
1864
+ <legend style={{ ...bodyStyle, position: "absolute", width: 1, height: 1, overflow: "hidden" }}>
1865
+ {t("capabilitiesTitle")}
1866
+ </legend>
1867
+ {capabilities.writable ? null : <p style={hintStyle}>{t("capabilitiesReadOnly")}</p>}
1868
+ <ul style={listStyle}>
1869
+ {CAPABILITY_TOGGLES.filter((item) => !item.key.startsWith("grokImagine")).map((item) => {
1870
+ const checked = capabilities.value[item.key];
1871
+ const imagesOff = item.requiresImages === true && !capabilities.value.codexImages;
1872
+ const disabled = capabilitiesBusy || !capabilities.writable || imagesOff;
1873
+ return (
1874
+ <li key={item.key}>
1875
+ <label style={checkRowStyle}>
1876
+ <input
1877
+ type="checkbox"
1878
+ checked={checked}
1879
+ disabled={disabled}
1880
+ aria-describedby={`cap-hint-${item.key}`}
1881
+ onChange={(event) => {
1882
+ void patchCapability(item.key, event.target.checked);
1883
+ }}
1884
+ />
1885
+ <span>
1886
+ <span style={{ display: "block" }}>{t(item.label)}</span>
1887
+ <span id={`cap-hint-${item.key}`} style={{ display: "block", ...hintStyle }}>
1888
+ {t(item.hint)}
1889
+ </span>
1890
+ </span>
1891
+ </label>
1892
+ </li>
1893
+ );
1894
+ })}
1895
+ </ul>
1896
+ <h4 style={{ ...titleStyle, fontSize: 14 }}>{t("imagineTitle")}</h4>
1897
+ <ul style={listStyle}>
1898
+ {CAPABILITY_TOGGLES.filter((item) => item.key.startsWith("grokImagine")).map((item) => {
1899
+ const checked = capabilities.value[item.key];
1900
+ const disabled = capabilitiesBusy || !capabilities.writable;
1901
+ return (
1902
+ <li key={item.key}>
1903
+ <label style={checkRowStyle}>
1904
+ <input
1905
+ type="checkbox"
1906
+ checked={checked}
1907
+ disabled={disabled}
1908
+ aria-describedby={`cap-hint-${item.key}`}
1909
+ onChange={(event) => {
1910
+ void patchCapability(item.key, event.target.checked);
1911
+ }}
1912
+ />
1913
+ <span>
1914
+ <span style={{ display: "block" }}>{t(item.label)}</span>
1915
+ <span id={`cap-hint-${item.key}`} style={{ display: "block", ...hintStyle }}>
1916
+ {t(item.hint)}
1917
+ </span>
1918
+ </span>
1919
+ </label>
1920
+ </li>
1921
+ );
1922
+ })}
1923
+ </ul>
1924
+ <div style={nestedStyle}>
1925
+ <h4 style={{ ...titleStyle, fontSize: 14 }}>{t("capabilityLimitsTitle")}</h4>
1926
+ <p style={hintStyle}>{t("capabilityLimitsHint")}</p>
1927
+ <ul style={listStyle}>
1928
+ {CAPABILITY_LIMITS.map((item) => {
1929
+ const displayValue = capabilities.value[item.key] / item.scale;
1930
+ const inputId = `cap-limit-${item.key}`;
1931
+ return (
1932
+ <li key={item.key} style={rowStyle}>
1933
+ <label htmlFor={inputId} style={{ ...bodyStyle, flex: "1 1 360px" }}>
1934
+ <span style={{ display: "block", color: "var(--dsw-alias-label-primary)" }}>
1935
+ {t(item.label)}
1936
+ </span>
1937
+ <span id={`${inputId}-hint`} style={{ display: "block", ...hintStyle }}>
1938
+ {t(item.hint)}
1939
+ </span>
1940
+ </label>
1941
+ <input
1942
+ key={`${item.key}-${String(capabilities.revision)}-${String(displayValue)}`}
1943
+ id={inputId}
1944
+ type="number"
1945
+ inputMode="numeric"
1946
+ min={item.min}
1947
+ max={item.max}
1948
+ step={1}
1949
+ defaultValue={displayValue}
1950
+ disabled={capabilitiesBusy || !capabilities.writable}
1951
+ aria-describedby={`${inputId}-hint`}
1952
+ style={{ ...inputStyle, width: 112, flex: "0 0 112px" }}
1953
+ onInput={(event) => event.currentTarget.setCustomValidity("")}
1954
+ onKeyDown={(event) => {
1955
+ if (event.key === "Enter") event.currentTarget.blur();
1956
+ if (event.key === "Escape") {
1957
+ event.currentTarget.value = String(displayValue);
1958
+ event.currentTarget.setCustomValidity("");
1959
+ }
1960
+ }}
1961
+ onBlur={(event) => {
1962
+ const target = event.currentTarget;
1963
+ const next = Number(target.value);
1964
+ if (!Number.isInteger(next) || next < item.min || next > item.max) {
1965
+ target.setCustomValidity(t("capabilityLimitInvalid", { min: item.min, max: item.max }));
1966
+ target.reportValidity();
1967
+ return;
1968
+ }
1969
+ target.setCustomValidity("");
1970
+ const apiValue = next * item.scale;
1971
+ if (apiValue === capabilities.value[item.key]) return;
1972
+ void patchCapability(item.key, apiValue).then((saved) => {
1973
+ if (!saved && target.isConnected) target.value = String(displayValue);
1974
+ });
1975
+ }}
1976
+ />
1977
+ </li>
1978
+ );
1979
+ })}
1980
+ </ul>
1981
+ </div>
1982
+ </fieldset>
1983
+ )}
1984
+ </section>
1985
+ ) : null}
1986
+ {activeTab === "about" ? <p style={warningStyle}>{t("termsWarning")}</p> : null}
1987
+ {activeTab === "gateway" ? (
1988
+ <section style={cardStyle} aria-labelledby="coding-oauth-gateway-title">
1989
+ <div>
1990
+ <h3 id="coding-oauth-gateway-title" style={{ ...titleStyle, fontSize: 16 }}>
1991
+ {t("gatewayTitle")}
1992
+ </h3>
1993
+ <p style={{ ...bodyStyle, marginTop: 4 }}>{t("gatewayIntro")}</p>
1994
+ <p style={{ ...hintStyle, marginTop: 8 }}>{t("gatewayWarning")}</p>
1995
+ </div>
1996
+ {gatewayError === undefined ? null : (
1997
+ <p style={errorStyle} role="alert">
1998
+ {gatewayError}
1999
+ </p>
2000
+ )}
2001
+ {gatewayRevealError === undefined ? null : (
2002
+ <p style={errorStyle} role="alert">
2003
+ {gatewayRevealError}
2004
+ </p>
2005
+ )}
2006
+ {gateway === undefined && gatewayError === undefined ? (
2007
+ <div style={statusStyle} role="status">
2008
+ <span aria-hidden="true" style={dotStyle("loading")} />
2009
+ {t("gatewayLoading")}
2010
+ </div>
2011
+ ) : gateway === undefined ? null : (
2012
+ <div style={nestedStyle}>
2013
+ <p style={statusStyle} role="status">
2014
+ <span aria-hidden="true" style={dotStyle(gateway.running ? "available" : "unavailable")} />
2015
+ <span>{gateway.running ? t("gatewayRunning") : t("gatewayStopped")}</span>
2016
+ </p>
2017
+ <label style={checkRowStyle}>
2018
+ <input
2019
+ type="checkbox"
2020
+ checked={gateway.enabled}
2021
+ disabled={gatewayBusy}
2022
+ onChange={(event) => {
2023
+ const enabled = event.target.checked;
2024
+ setGatewayBusy(true);
2025
+ void jsonRequest<unknown>(GATEWAY_PATH, "PATCH", { enabled })
2026
+ .then((value) => {
2027
+ setGateway(parseGateway(value) ?? gateway);
2028
+ setGatewayError(undefined);
2029
+ })
2030
+ .catch((error: unknown) => {
2031
+ setGatewayError(error instanceof Error ? error.message : t("gatewaySaveFailed"));
2032
+ })
2033
+ .finally(() => setGatewayBusy(false));
2034
+ }}
2035
+ />
2036
+ <span>{t("gatewayEnabled")}</span>
2037
+ </label>
2038
+ <div>
2039
+ <label
2040
+ htmlFor="coding-oauth-gateway-port"
2041
+ style={{ ...bodyStyle, color: "var(--dsw-alias-label-primary)" }}
2042
+ >
2043
+ {t("gatewayPort")}
2044
+ </label>
2045
+ <p id="coding-oauth-gateway-port-hint" style={hintStyle}>
2046
+ {t("gatewayPortHint")}
2047
+ </p>
2048
+ <div style={{ display: "flex", flexWrap: "wrap", gap: 8, marginTop: 8 }}>
2049
+ <input
2050
+ id="coding-oauth-gateway-port"
2051
+ type="number"
2052
+ inputMode="numeric"
2053
+ min={GATEWAY_PORT_MIN}
2054
+ max={GATEWAY_PORT_MAX}
2055
+ step={1}
2056
+ value={portDraft}
2057
+ disabled={gatewayBusy}
2058
+ aria-describedby="coding-oauth-gateway-port-hint"
2059
+ style={{ ...inputStyle, width: 112, flex: "0 0 112px" }}
2060
+ onChange={(event) => setPortDraft(event.target.value)}
2061
+ onKeyDown={(event) => {
2062
+ if (event.key === "Enter") {
2063
+ event.preventDefault();
2064
+ void applyGatewayPort();
2065
+ }
2066
+ if (event.key === "Escape" && gateway !== undefined) {
2067
+ setPortDraft(String(gateway.port));
2068
+ }
2069
+ }}
2070
+ />
2071
+ <button
2072
+ type="button"
2073
+ style={primaryButtonStyle}
2074
+ disabled={
2075
+ gatewayBusy || portDraft === String(gateway.port) || parseGatewayPort(portDraft) === undefined
2076
+ }
2077
+ onClick={() => {
2078
+ void applyGatewayPort();
2079
+ }}
2080
+ >
2081
+ {t("gatewayPortApply")}
2082
+ </button>
2083
+ <button
2084
+ type="button"
2085
+ style={buttonStyle}
2086
+ disabled={gatewayBusy}
2087
+ onClick={() => {
2088
+ setPortDraft(String(randomGatewayPort(gateway.port)));
2089
+ }}
2090
+ >
2091
+ {t("gatewayPortRandom")}
2092
+ </button>
2093
+ </div>
2094
+ </div>
2095
+ <p style={copyRowStyle}>
2096
+ <span style={hintStyle}>
2097
+ {t("gatewayOpenAiUrl")}
2098
+ <span style={{ display: "block", ...monoStyle }}>
2099
+ {`${formatGatewayBaseUrl(gateway.bind, gateway.port)}/v1`}
2100
+ </span>
2101
+ </span>
2102
+ <button
2103
+ type="button"
2104
+ style={primaryButtonStyle}
2105
+ onClick={() => {
2106
+ void handleCopy("openai", `${formatGatewayBaseUrl(gateway.bind, gateway.port)}/v1`);
2107
+ }}
2108
+ >
2109
+ {copyLabel("openai")}
2110
+ </button>
2111
+ </p>
2112
+ <p style={copyRowStyle}>
2113
+ <span style={hintStyle}>
2114
+ {t("gatewayAnthropicUrl")}
2115
+ <span style={{ display: "block", ...monoStyle }}>
2116
+ {formatGatewayBaseUrl(gateway.bind, gateway.port)}
2117
+ </span>
2118
+ </span>
2119
+ <button
2120
+ type="button"
2121
+ style={buttonStyle}
2122
+ onClick={() => {
2123
+ void handleCopy("anthropic", formatGatewayBaseUrl(gateway.bind, gateway.port));
2124
+ }}
2125
+ >
2126
+ {copyLabel("anthropic")}
2127
+ </button>
2128
+ </p>
2129
+ <p style={copyRowStyle}>
2130
+ <span style={hintStyle}>
2131
+ {t("gatewayKeyHint")}
2132
+ <span style={{ display: "block", ...monoStyle, overflowWrap: "anywhere" }}>
2133
+ {gatewayKeyVisible && gatewayOnceKey !== undefined ? gatewayOnceKey : gateway.keyHint || "—"}
2134
+ </span>
2135
+ </span>
2136
+ <span style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
2137
+ <button
2138
+ type="button"
2139
+ style={primaryButtonStyle}
2140
+ disabled={gatewayBusy}
2141
+ onClick={() => {
2142
+ void copyGatewayKey();
2143
+ }}
2144
+ >
2145
+ {copyLabel("key", t("gatewayCopyKey"))}
2146
+ </button>
2147
+ <button
2148
+ type="button"
2149
+ style={buttonStyle}
2150
+ disabled={gatewayBusy}
2151
+ onClick={() => {
2152
+ void toggleGatewayKeyVisible();
2153
+ }}
2154
+ >
2155
+ {gatewayKeyVisible ? t("gatewayHideKey") : t("gatewayShowKey")}
2156
+ </button>
2157
+ </span>
2158
+ </p>
2159
+ <p style={hintStyle}>{t("gatewayKeyCopyHint")}</p>
2160
+ {gatewayRotateConfirm ? (
2161
+ <div style={nestedStyle}>
2162
+ <p style={bodyStyle}>{t("gatewayRotateConfirm")}</p>
2163
+ <p style={hintStyle}>{t("gatewayRotateConfirmHint")}</p>
2164
+ <div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
2165
+ <button
2166
+ type="button"
2167
+ style={buttonStyle}
2168
+ disabled={gatewayBusy}
2169
+ onClick={() => {
2170
+ void rotateGatewayKey();
2171
+ }}
2172
+ >
2173
+ {t("gatewayRotateConfirmAction")}
2174
+ </button>
2175
+ <button
2176
+ type="button"
2177
+ style={buttonStyle}
2178
+ disabled={gatewayBusy}
2179
+ onClick={() => setGatewayRotateConfirm(false)}
2180
+ >
2181
+ {t("gatewayRotateCancel")}
2182
+ </button>
2183
+ </div>
2184
+ </div>
2185
+ ) : (
2186
+ <button
2187
+ type="button"
2188
+ style={buttonStyle}
2189
+ disabled={gatewayBusy}
2190
+ onClick={() => setGatewayRotateConfirm(true)}
2191
+ >
2192
+ {t("gatewayRotate")}
2193
+ </button>
2194
+ )}
2195
+ </div>
2196
+ )}
2197
+ </section>
2198
+ ) : null}
2199
+ {activeTab === "about" && status !== undefined ? (
2200
+ <div style={cardStyle}>
2201
+ <div style={rowStyle}>
2202
+ <div>
2203
+ <h3 style={{ ...titleStyle, fontSize: 16 }}>{t("antigravityTitle")}</h3>
2204
+ <p style={{ ...bodyStyle, marginTop: 4 }}>{t("antigravityDescription")}</p>
2205
+ <p style={{ ...bodyStyle, marginTop: 4 }}>
2206
+ <span style={monoStyle}>{status.antigravity.route}</span>
2207
+ </p>
2208
+ </div>
2209
+ <div style={statusStyle} role="status">
2210
+ <span aria-hidden="true" style={dotStyle("signed-out", status.antigravity.installed)} />
2211
+ <span>{status.antigravity.installed ? t("antigravityInstalled") : t("antigravityMissing")}</span>
2212
+ </div>
2213
+ </div>
2214
+ <p style={bodyStyle}>{t("antigravityCliHint")}</p>
2215
+ <code style={{ ...monoStyle, fontSize: 12, overflowWrap: "anywhere" }}>{t("antigravityCliCommand")}</code>
2216
+ </div>
2217
+ ) : null}
2218
+ </div>
2219
+ </section>
2220
+ );
2221
+ }