dsh-coding-subscription-oauth 0.5.2 → 0.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/INSTALL.md +11 -11
  3. package/README.de.md +34 -9
  4. package/README.es.md +35 -9
  5. package/README.fr.md +35 -9
  6. package/README.ja.md +35 -9
  7. package/README.ko.md +35 -9
  8. package/README.md +34 -13
  9. package/README.pt-BR.md +35 -9
  10. package/README.ru.md +35 -9
  11. package/README.zh-CN.md +34 -13
  12. package/docs/02-architecture.md +4 -3
  13. package/docs/02-architecture.zh-CN.md +8 -3
  14. package/lib/adapter.d.ts.map +1 -1
  15. package/lib/alias-adapter.d.ts.map +1 -1
  16. package/lib/client.js +1 -1
  17. package/lib/client.js.map +4 -4
  18. package/lib/grok-errors.d.ts +13 -0
  19. package/lib/grok-errors.d.ts.map +1 -0
  20. package/lib/index.js +13 -3
  21. package/lib/index.js.map +3 -3
  22. package/media/settings_accounts.png +0 -0
  23. package/media/settings_capabilities.png +0 -0
  24. package/media/settings_gateway.png +0 -0
  25. package/media/settings_overview.png +0 -0
  26. package/package.json +2 -1
  27. package/src/adapter.ts +7 -2
  28. package/src/alias-adapter.ts +2 -1
  29. package/src/client/GrokBuildSettings.tsx +247 -1772
  30. package/src/client/api.ts +88 -0
  31. package/src/client/components/AboutTab.tsx +20 -0
  32. package/src/client/components/AccountsTab.tsx +224 -0
  33. package/src/client/components/CapabilitiesTab.tsx +214 -0
  34. package/src/client/components/CliPullPreview.tsx +107 -0
  35. package/src/client/components/GatewayTab.tsx +287 -0
  36. package/src/client/components/ProviderCard.tsx +322 -0
  37. package/src/client/components/SettingsTabs.tsx +65 -0
  38. package/src/client/constants.ts +206 -0
  39. package/src/client/display.ts +61 -0
  40. package/src/client/locales.ts +47 -15
  41. package/src/client/parsers.ts +386 -0
  42. package/src/client/styles.ts +180 -0
  43. package/src/client/types.ts +185 -0
  44. package/src/grok-errors.ts +24 -0
@@ -1,971 +1,81 @@
1
1
  /** Plugin-owned coding subscription account section inside the dsh Settings shell. */
2
2
 
3
- import type { CSSProperties } from "react";
4
3
  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> {
4
+ import { cancelPreviewTicket, copyText, isConflictError, isConsumedPreviewError, jsonRequest } from "./api.ts";
5
+ import { AboutTab } from "./components/AboutTab.tsx";
6
+ import { AccountsTab } from "./components/AccountsTab.tsx";
7
+ import { CapabilitiesTab } from "./components/CapabilitiesTab.tsx";
8
+ import { GatewayTab } from "./components/GatewayTab.tsx";
9
+ import { SettingsTabs } from "./components/SettingsTabs.tsx";
10
+ import {
11
+ CAPABILITIES_PATH,
12
+ CODEX_USAGE_PATH,
13
+ GATEWAY_PATH,
14
+ GATEWAY_REVEAL_PATH,
15
+ GATEWAY_ROTATE_PATH,
16
+ IMAGINE_CREDENTIAL_PATH,
17
+ LOGIN_CANCEL_PATH,
18
+ LOGIN_CODE_PATH,
19
+ LOGIN_PATH,
20
+ LOGOUT_PATH,
21
+ MODELS_PATH,
22
+ POLL_INTERVAL_MS,
23
+ SOURCE_COMMIT_ACTION_KEY,
24
+ SOURCES_CANCEL_PATH,
25
+ SOURCES_COMMIT_PATH,
26
+ SOURCES_PATH,
27
+ SOURCES_PREVIEW_PATH,
28
+ STATUS_PATH,
29
+ } from "./constants.ts";
30
+ import { isLikelyRemoteHost } from "./display.ts";
31
+ import {
32
+ emptyCapabilitySettings,
33
+ mergeSources,
34
+ parseCapabilities,
35
+ parseCommitAction,
36
+ parseGateway,
37
+ parseGatewayPort,
38
+ parseImagineCredential,
39
+ parsePreview,
40
+ parseSources,
41
+ parseUsage,
42
+ } from "./parsers.ts";
43
+ import { bodyStyle, errorStyle, pageStyle, panelStyle, titleStyle } from "./styles.ts";
44
+ import type {
45
+ CapabilitySettingKey,
46
+ CapabilitySettingsView,
47
+ CapabilitySnapshot,
48
+ CodingOAuthStatus,
49
+ CopyField,
50
+ GatewayView,
51
+ GrokBuildSettingsProps,
52
+ ImagineCredentialView,
53
+ LoginChallenge,
54
+ LoginMethod,
55
+ ProviderSlug,
56
+ SettingsTabId,
57
+ SourceKind,
58
+ SourcePreview,
59
+ SourceStatus,
60
+ UsageView,
61
+ } from "./types.ts";
62
+
63
+ export type { GrokBuildSettingsInjected, GrokBuildSettingsProps } from "./types.ts";
64
+
65
+ const REMOTE_TIP_STORAGE_KEY = "dsh-coding-oauth-remote-tip-dismissed";
66
+
67
+ function readRemoteTipDismissed(): boolean {
907
68
  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;
69
+ return globalThis.sessionStorage?.getItem(REMOTE_TIP_STORAGE_KEY) === "1";
926
70
  } catch {
927
71
  return false;
928
72
  }
929
73
  }
930
74
 
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
75
  /** Multi-provider coding subscription status and OAuth actions. */
967
76
  export function GrokBuildSettings({ t }: GrokBuildSettingsProps) {
968
77
  if (t === undefined) throw new Error("Coding OAuth settings requires its translation function");
78
+
969
79
  const [status, setStatus] = useState<CodingOAuthStatus | undefined>(undefined);
970
80
  const [requestError, setRequestError] = useState<string | undefined>(undefined);
971
81
  const [busyProvider, setBusyProvider] = useState<ProviderSlug | undefined>(undefined);
@@ -983,6 +93,7 @@ export function GrokBuildSettings({ t }: GrokBuildSettingsProps) {
983
93
  const [capabilities, setCapabilities] = useState<CapabilitySnapshot | undefined>(undefined);
984
94
  const [capabilitiesError, setCapabilitiesError] = useState<string | undefined>(undefined);
985
95
  const [capabilitiesBusy, setCapabilitiesBusy] = useState(false);
96
+ const [capabilitiesLoaded, setCapabilitiesLoaded] = useState(false);
986
97
  const [usage, setUsage] = useState<UsageView | undefined>(undefined);
987
98
  const [usageError, setUsageError] = useState<string | undefined>(undefined);
988
99
  const [usageLoading, setUsageLoading] = useState(false);
@@ -991,6 +102,7 @@ export function GrokBuildSettings({ t }: GrokBuildSettingsProps) {
991
102
  const [gateway, setGateway] = useState<GatewayView | undefined>(undefined);
992
103
  const [gatewayError, setGatewayError] = useState<string | undefined>(undefined);
993
104
  const [gatewayBusy, setGatewayBusy] = useState(false);
105
+ const [gatewayLoaded, setGatewayLoaded] = useState(false);
994
106
  const [gatewayOnceKey, setGatewayOnceKey] = useState<string | undefined>(undefined);
995
107
  const [gatewayKeyVisible, setGatewayKeyVisible] = useState(false);
996
108
  const [gatewayRotateConfirm, setGatewayRotateConfirm] = useState(false);
@@ -1000,7 +112,9 @@ export function GrokBuildSettings({ t }: GrokBuildSettingsProps) {
1000
112
  const [copiedField, setCopiedField] = useState<CopyField | undefined>(undefined);
1001
113
  const [copyFailedField, setCopyFailedField] = useState<CopyField | undefined>(undefined);
1002
114
  const [expandedProviders, setExpandedProviders] = useState<Partial<Record<ProviderSlug, boolean>>>({});
115
+ const [remoteTipDismissed, setRemoteTipDismissed] = useState(readRemoteTipDismissed);
1003
116
  const copiedTimerRef = useRef<number | undefined>(undefined);
117
+ const remote = typeof window !== "undefined" ? isLikelyRemoteHost(window.location.hostname) : false;
1004
118
 
1005
119
  const refresh = useCallback(async () => {
1006
120
  try {
@@ -1031,6 +145,8 @@ export function GrokBuildSettings({ t }: GrokBuildSettingsProps) {
1031
145
  setCapabilities({ value: emptyCapabilitySettings(), revision: 0, writable: false });
1032
146
  setCapabilitiesError(error instanceof Error ? error.message : t("capabilitiesLoadFailed"));
1033
147
  return undefined;
148
+ } finally {
149
+ setCapabilitiesLoaded(true);
1034
150
  }
1035
151
  }, [t]);
1036
152
 
@@ -1040,6 +156,8 @@ export function GrokBuildSettings({ t }: GrokBuildSettingsProps) {
1040
156
  setGatewayError(undefined);
1041
157
  } catch (error: unknown) {
1042
158
  setGatewayError(error instanceof Error ? error.message : t("gatewayLoadFailed"));
159
+ } finally {
160
+ setGatewayLoaded(true);
1043
161
  }
1044
162
  }, [t]);
1045
163
 
@@ -1065,16 +183,41 @@ export function GrokBuildSettings({ t }: GrokBuildSettingsProps) {
1065
183
  }
1066
184
  }, [t]);
1067
185
 
186
+ // Accounts: status immediately; sources shortly after (non-blocking for first paint).
1068
187
  useEffect(() => {
1069
188
  void refresh();
1070
- void refreshSources();
1071
- void refreshCapabilities();
189
+ const timer = window.setTimeout(() => {
190
+ void refreshSources();
191
+ }, 0);
192
+ return () => {
193
+ window.clearTimeout(timer);
194
+ };
195
+ }, [refresh, refreshSources]);
196
+
197
+ // Capabilities tab: load settings + Imagine status on first visit.
198
+ useEffect(() => {
199
+ if (activeTab !== "capabilities") return;
200
+ if (!capabilitiesLoaded) void refreshCapabilities();
1072
201
  void refreshImagine();
202
+ }, [activeTab, capabilitiesLoaded, refreshCapabilities, refreshImagine]);
203
+
204
+ // Soft-fetch capabilities when Codex is signed in so Accounts can show quota without opening the tab.
205
+ useEffect(() => {
206
+ if (capabilitiesLoaded) return;
207
+ if (status?.providers.codex.status !== "signed-in") return;
208
+ void refreshCapabilities();
209
+ }, [capabilitiesLoaded, refreshCapabilities, status?.providers.codex.status]);
210
+
211
+ // Gateway: load only when that tab is opened.
212
+ useEffect(() => {
213
+ if (activeTab !== "gateway" || gatewayLoaded) return;
1073
214
  void refreshGateway();
1074
- }, [refresh, refreshSources, refreshCapabilities, refreshImagine, refreshGateway]);
215
+ }, [activeTab, gatewayLoaded, refreshGateway]);
216
+
1075
217
  useEffect(() => {
1076
218
  previewRef.current = preview;
1077
219
  }, [preview]);
220
+
1078
221
  useEffect(() => {
1079
222
  mountedRef.current = true;
1080
223
  return () => {
@@ -1085,6 +228,7 @@ export function GrokBuildSettings({ t }: GrokBuildSettingsProps) {
1085
228
  if (active !== undefined) cancelPreviewTicket(active.previewId, true);
1086
229
  };
1087
230
  }, []);
231
+
1088
232
  useEffect(() => {
1089
233
  const signingIn =
1090
234
  status !== undefined && Object.values(status.providers).some((provider) => provider.status === "signing-in");
@@ -1096,6 +240,7 @@ export function GrokBuildSettings({ t }: GrokBuildSettingsProps) {
1096
240
  window.clearInterval(timer);
1097
241
  };
1098
242
  }, [refresh, status]);
243
+
1099
244
  useEffect(() => {
1100
245
  const signedIn = status?.providers.codex.status === "signed-in";
1101
246
  if (capabilities?.value.codexUsage === true && signedIn) {
@@ -1107,6 +252,10 @@ export function GrokBuildSettings({ t }: GrokBuildSettingsProps) {
1107
252
  setUsageLoading(false);
1108
253
  }, [capabilities?.value.codexUsage, refreshUsage, status?.providers.codex.status]);
1109
254
 
255
+ useEffect(() => {
256
+ if (gateway !== undefined) setPortDraft(String(gateway.port));
257
+ }, [gateway]);
258
+
1110
259
  const signIn = async (provider: ProviderSlug, method: LoginMethod): Promise<void> => {
1111
260
  const popup = window.open("about:blank", "_blank");
1112
261
  if (popup !== null) popup.opener = null;
@@ -1315,12 +464,6 @@ export function GrokBuildSettings({ t }: GrokBuildSettingsProps) {
1315
464
  setCopyFailedField(field);
1316
465
  };
1317
466
 
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
467
  const ensureGatewayKey = async (): Promise<string> => {
1325
468
  if (gatewayOnceKey !== undefined) return gatewayOnceKey;
1326
469
  const value = await jsonRequest<{ apiKey?: string }>(GATEWAY_REVEAL_PATH, "POST");
@@ -1355,10 +498,6 @@ export function GrokBuildSettings({ t }: GrokBuildSettingsProps) {
1355
498
  }
1356
499
  };
1357
500
 
1358
- useEffect(() => {
1359
- if (gateway !== undefined) setPortDraft(String(gateway.port));
1360
- }, [gateway]);
1361
-
1362
501
  const applyGatewayPort = async (): Promise<void> => {
1363
502
  const port = parseGatewayPort(portDraft);
1364
503
  if (port === undefined) {
@@ -1394,827 +533,163 @@ export function GrokBuildSettings({ t }: GrokBuildSettingsProps) {
1394
533
  }
1395
534
  };
1396
535
 
536
+ const setGatewayEnabled = (enabled: boolean): void => {
537
+ setGatewayBusy(true);
538
+ void jsonRequest<unknown>(GATEWAY_PATH, "PATCH", { enabled })
539
+ .then((value) => {
540
+ setGateway(parseGateway(value) ?? gateway);
541
+ setGatewayError(undefined);
542
+ })
543
+ .catch((error: unknown) => {
544
+ setGatewayError(error instanceof Error ? error.message : t("gatewaySaveFailed"));
545
+ })
546
+ .finally(() => {
547
+ setGatewayBusy(false);
548
+ });
549
+ };
550
+
551
+ const dismissRemoteTip = (): void => {
552
+ setRemoteTipDismissed(true);
553
+ try {
554
+ globalThis.sessionStorage?.setItem(REMOTE_TIP_STORAGE_KEY, "1");
555
+ } catch {
556
+ // ignore storage failures
557
+ }
558
+ };
559
+
1397
560
  return (
1398
561
  <section style={pageStyle} aria-labelledby="coding-oauth-settings-title">
1399
562
  <div>
1400
563
  <h2 id="coding-oauth-settings-title" style={titleStyle}>
1401
564
  {t("title")}
1402
565
  </h2>
1403
- <p style={{ ...bodyStyle, marginTop: 6 }}>{t("intro")}</p>
566
+ {activeTab === "accounts" ? <p style={{ ...bodyStyle, marginTop: 6 }}>{t("intro")}</p> : null}
1404
567
  </div>
1405
568
  {requestError === undefined ? null : (
1406
569
  <p style={errorStyle} role="alert">
1407
570
  {requestError}
1408
571
  </p>
1409
572
  )}
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}>
573
+ <SettingsTabs t={t} activeTab={activeTab} onChange={setActiveTab} />
574
+ <div
575
+ id={`coding-oauth-panel-${activeTab}`}
576
+ role="tabpanel"
577
+ aria-labelledby={`coding-oauth-tab-${activeTab}`}
578
+ style={panelStyle}
579
+ >
1426
580
  {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
- )
581
+ <AccountsTab
582
+ t={t}
583
+ status={status}
584
+ remote={remote}
585
+ remoteTipDismissed={remoteTipDismissed}
586
+ onDismissRemoteTip={dismissRemoteTip}
587
+ sources={sources}
588
+ sourcesError={sourcesError}
589
+ sourcesNotice={sourcesNotice}
590
+ sourcesBusy={sourcesBusy}
591
+ preview={preview}
592
+ confirmOverwrite={confirmOverwrite}
593
+ busyProvider={busyProvider}
594
+ codeInputs={codeInputs}
595
+ popupBlocked={popupBlocked}
596
+ expandedProviders={expandedProviders}
597
+ showUsage={showUsage === true}
598
+ usage={usage}
599
+ usageError={usageError}
600
+ usageLoading={usageLoading}
601
+ onSignIn={(slug, method) => {
602
+ void signIn(slug, method);
603
+ }}
604
+ onSignOut={(slug) => {
605
+ void signOut(slug);
606
+ }}
607
+ onCancelLogin={(slug) => {
608
+ void cancelLogin(slug);
609
+ }}
610
+ onSubmitCode={(slug) => {
611
+ void submitCode(slug);
612
+ }}
613
+ onCodeChange={(slug, value) => {
614
+ setCodeInputs((current) => ({ ...current, [slug]: value }));
615
+ }}
616
+ onToggleExpanded={(slug) => {
617
+ setExpandedProviders((current) => ({
618
+ ...current,
619
+ [slug]: !(current[slug] === true),
620
+ }));
621
+ }}
622
+ onPreviewSource={(slug) => {
623
+ void previewSource(slug);
624
+ }}
625
+ onSaveModels={(slug, selected) => {
626
+ void saveModels(slug, selected);
627
+ }}
628
+ onConfirmOverwriteChange={setConfirmOverwrite}
629
+ onCommitSource={() => {
630
+ void commitSource();
631
+ }}
632
+ onCancelSourcePreview={() => {
633
+ void cancelSourcePreview();
634
+ }}
635
+ onRefreshSources={() => {
636
+ void refreshSources();
637
+ }}
638
+ />
1823
639
  ) : null}
1824
640
  {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>
641
+ <CapabilitiesTab
642
+ t={t}
643
+ capabilities={capabilities}
644
+ capabilitiesError={capabilitiesError}
645
+ capabilitiesBusy={capabilitiesBusy}
646
+ imagine={imagine}
647
+ imagineError={imagineError}
648
+ onPatchCapability={(key, value) => patchCapability(key, value)}
649
+ />
1985
650
  ) : null}
1986
- {activeTab === "about" ? <p style={warningStyle}>{t("termsWarning")}</p> : null}
1987
651
  {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>
652
+ <GatewayTab
653
+ t={t}
654
+ gateway={gateway}
655
+ gatewayError={gatewayError}
656
+ gatewayBusy={gatewayBusy}
657
+ gatewayOnceKey={gatewayOnceKey}
658
+ gatewayKeyVisible={gatewayKeyVisible}
659
+ gatewayRotateConfirm={gatewayRotateConfirm}
660
+ gatewayRevealError={gatewayRevealError}
661
+ portDraft={portDraft}
662
+ copiedField={copiedField}
663
+ copyFailedField={copyFailedField}
664
+ onEnabledChange={setGatewayEnabled}
665
+ onPortDraftChange={setPortDraft}
666
+ onApplyPort={() => {
667
+ void applyGatewayPort();
668
+ }}
669
+ onRandomPort={(port) => {
670
+ setPortDraft(String(port));
671
+ }}
672
+ onCopy={(field, text) => {
673
+ void handleCopy(field, text);
674
+ }}
675
+ onCopyKey={() => {
676
+ void copyGatewayKey();
677
+ }}
678
+ onToggleKeyVisible={() => {
679
+ void toggleGatewayKeyVisible();
680
+ }}
681
+ onRotateConfirm={() => {
682
+ setGatewayRotateConfirm(true);
683
+ }}
684
+ onRotateCancel={() => {
685
+ setGatewayRotateConfirm(false);
686
+ }}
687
+ onRotate={() => {
688
+ void rotateGatewayKey();
689
+ }}
690
+ />
2217
691
  ) : null}
692
+ {activeTab === "about" ? <AboutTab t={t} /> : null}
2218
693
  </div>
2219
694
  </section>
2220
695
  );