dsh-coding-subscription-oauth 0.5.3 → 0.5.5

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