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.
- package/CHANGELOG.md +21 -0
- package/INSTALL.md +4 -4
- package/README.de.md +23 -6
- package/README.es.md +24 -6
- package/README.fr.md +24 -6
- package/README.ja.md +24 -6
- package/README.ko.md +24 -6
- package/README.md +23 -6
- package/README.pt-BR.md +24 -6
- package/README.ru.md +24 -6
- package/README.zh-CN.md +23 -6
- package/docs/02-architecture.md +1 -1
- package/docs/02-architecture.zh-CN.md +1 -1
- package/lib/client.js +34 -1
- package/lib/client.js.map +4 -4
- package/media/settings_accounts.png +0 -0
- package/media/settings_capabilities.png +0 -0
- package/media/settings_gateway.png +0 -0
- package/media/settings_overview.png +0 -0
- package/package.json +2 -1
- package/src/client/GrokBuildSettings.tsx +270 -1773
- package/src/client/api.ts +88 -0
- package/src/client/components/AboutTab.tsx +20 -0
- package/src/client/components/AccountsTab.tsx +224 -0
- package/src/client/components/Badge.tsx +33 -0
- package/src/client/components/CapabilitiesTab.tsx +227 -0
- package/src/client/components/CliPullPreview.tsx +107 -0
- package/src/client/components/CopyButton.tsx +57 -0
- package/src/client/components/GatewayTab.tsx +364 -0
- package/src/client/components/NoticeBanner.tsx +46 -0
- package/src/client/components/ProgressBar.tsx +52 -0
- package/src/client/components/ProviderCard.tsx +502 -0
- package/src/client/components/SettingsTabs.tsx +75 -0
- package/src/client/components/ToggleSwitch.tsx +60 -0
- package/src/client/constants.ts +206 -0
- package/src/client/display.ts +61 -0
- package/src/client/gatewaySnippets.ts +36 -0
- package/src/client/locales.ts +81 -15
- package/src/client/microStyles.ts +33 -0
- package/src/client/parsers.ts +386 -0
- package/src/client/styles.ts +306 -0
- 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
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
type
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1071
|
-
|
|
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
|
-
}, [
|
|
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
|
-
<
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
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
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
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}/<id></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
|
-
<
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
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
|
-
<
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
{
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
{
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
)
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
)
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
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
|
);
|