dsh-coding-subscription-oauth 0.6.2 → 0.6.3

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 (121) hide show
  1. package/CHANGELOG.md +222 -206
  2. package/CONTRIBUTING.md +129 -120
  3. package/INSTALL.md +218 -218
  4. package/LICENSE +19 -19
  5. package/NOTICE +11 -11
  6. package/README.de.md +289 -289
  7. package/README.es.md +290 -290
  8. package/README.fr.md +290 -290
  9. package/README.ja.md +290 -290
  10. package/README.ko.md +290 -290
  11. package/README.md +304 -304
  12. package/README.pt-BR.md +290 -290
  13. package/README.ru.md +290 -290
  14. package/README.zh-CN.md +302 -302
  15. package/compatibility/dsh-bom.json +0 -0
  16. package/cordis.patch.yml +13 -13
  17. package/docs/00-project-rules.md +212 -195
  18. package/docs/02-architecture.md +130 -130
  19. package/docs/02-architecture.zh-CN.md +130 -130
  20. package/lib/bin.js.map +1 -1
  21. package/lib/capability-settings.d.ts +8 -2
  22. package/lib/capability-settings.d.ts.map +1 -1
  23. package/lib/capability-tools.d.ts +7 -2
  24. package/lib/capability-tools.d.ts.map +1 -1
  25. package/lib/client.js +3 -3
  26. package/lib/client.js.map +2 -2
  27. package/lib/codex-images.d.ts +8 -0
  28. package/lib/codex-images.d.ts.map +1 -1
  29. package/lib/index.js +32 -7
  30. package/lib/index.js.map +2 -2
  31. package/lib/invariant.js.map +1 -1
  32. package/media/en/settings_accounts.png +0 -0
  33. package/media/en/settings_capabilities.png +0 -0
  34. package/media/en/settings_gateway.png +0 -0
  35. package/media/settings_accounts.png +0 -0
  36. package/media/settings_capabilities.png +0 -0
  37. package/media/settings_gateway.png +0 -0
  38. package/media/settings_overview.png +0 -0
  39. package/media/zh-CN/settings_accounts.png +0 -0
  40. package/media/zh-CN/settings_capabilities.png +0 -0
  41. package/media/zh-CN/settings_gateway.png +0 -0
  42. package/package.json +148 -148
  43. package/patches/dsh-agy@0.1.2.patch +25 -25
  44. package/scripts/release.mjs +186 -186
  45. package/scripts/smoke-deployed-routes.mjs +146 -146
  46. package/scripts/verify-deployed-catalog.mjs +87 -87
  47. package/src/adapter.ts +273 -273
  48. package/src/alias-adapter.ts +130 -130
  49. package/src/auth-routes.ts +921 -921
  50. package/src/auth.ts +67 -67
  51. package/src/bin.ts +350 -350
  52. package/src/capability-routes.ts +279 -279
  53. package/src/capability-runtime.ts +314 -313
  54. package/src/capability-settings.ts +671 -658
  55. package/src/capability-tools.ts +685 -666
  56. package/src/catalog.ts +271 -271
  57. package/src/client/GrokBuildSettings.tsx +771 -770
  58. package/src/client/api.ts +88 -88
  59. package/src/client/components/AboutTab.tsx +30 -30
  60. package/src/client/components/AccountsTab.tsx +241 -241
  61. package/src/client/components/Badge.tsx +33 -33
  62. package/src/client/components/CapabilitiesTab.tsx +265 -265
  63. package/src/client/components/CliPullPreview.tsx +116 -116
  64. package/src/client/components/CopyButton.tsx +57 -57
  65. package/src/client/components/GatewayTab.tsx +469 -469
  66. package/src/client/components/NoticeBanner.tsx +46 -46
  67. package/src/client/components/ProgressBar.tsx +53 -53
  68. package/src/client/components/ProviderCard.tsx +606 -606
  69. package/src/client/components/SettingsTabs.tsx +75 -75
  70. package/src/client/components/ToggleSwitch.tsx +71 -71
  71. package/src/client/constants.ts +230 -224
  72. package/src/client/display.ts +61 -61
  73. package/src/client/dshClientAdapter.ts +127 -127
  74. package/src/client/gatewaySnippets.ts +37 -37
  75. package/src/client/index.tsx +156 -156
  76. package/src/client/locales.ts +540 -535
  77. package/src/client/microStyles.ts +52 -52
  78. package/src/client/parsers.ts +398 -396
  79. package/src/client/styles.ts +325 -325
  80. package/src/client/types.ts +199 -197
  81. package/src/codex-http.ts +447 -447
  82. package/src/codex-images.ts +503 -485
  83. package/src/codex-model-capabilities.ts +320 -320
  84. package/src/codex-search.ts +245 -245
  85. package/src/codex-usage.ts +263 -263
  86. package/src/compatibility.ts +41 -41
  87. package/src/dsh-host-adapter.ts +173 -173
  88. package/src/gateway-anthropic-messages.ts +84 -84
  89. package/src/gateway-auth.ts +102 -102
  90. package/src/gateway-backend.ts +274 -274
  91. package/src/gateway-body.ts +49 -49
  92. package/src/gateway-config.ts +76 -76
  93. package/src/gateway-http.ts +104 -104
  94. package/src/gateway-openai-chat.ts +124 -124
  95. package/src/gateway-openai-responses.ts +53 -53
  96. package/src/gateway-parse.ts +224 -224
  97. package/src/gateway-protocol.ts +52 -52
  98. package/src/gateway-routes.ts +158 -158
  99. package/src/gateway.ts +258 -258
  100. package/src/grok-errors.ts +24 -24
  101. package/src/grok-imagine.ts +1627 -1627
  102. package/src/grok-import.ts +151 -151
  103. package/src/http-json.ts +82 -82
  104. package/src/ids.ts +59 -59
  105. package/src/imagine-routes.ts +463 -463
  106. package/src/index.ts +709 -709
  107. package/src/invariant.ts +17 -17
  108. package/src/kimi-errors.ts +26 -26
  109. package/src/media-store.ts +927 -927
  110. package/src/oauth-import-routes.ts +324 -324
  111. package/src/oauth-providers.ts +152 -152
  112. package/src/oauth-session.ts +183 -183
  113. package/src/oauth-sources.ts +1104 -1104
  114. package/src/oauth.ts +620 -620
  115. package/src/provider.ts +128 -128
  116. package/src/proxy.ts +11 -11
  117. package/src/redact.ts +72 -72
  118. package/src/session.ts +218 -218
  119. package/src/store.ts +217 -217
  120. package/src/web-origin.ts +296 -296
  121. package/src/web-routes.ts +38 -38
@@ -1,469 +1,469 @@
1
- /** Local API gateway settings tab. */
2
-
3
- import { type KeyboardEvent, useEffect, useMemo, useRef, useState } from "react";
4
- import { GATEWAY_PORT_MAX, GATEWAY_PORT_MIN } from "../constants.ts";
5
- import { buildGatewaySnippets, type GatewaySnippetId } from "../gatewaySnippets.ts";
6
- import { formatGatewayBaseUrl, parseGatewayPort, randomGatewayPort } from "../parsers.ts";
7
- import {
8
- bodyStyle,
9
- buttonStyle,
10
- cardStyle,
11
- copyRowStyle,
12
- dotStyle,
13
- errorStyle,
14
- hintStyle,
15
- inputStyle,
16
- monoStyle,
17
- nestedStyle,
18
- primaryButtonStyle,
19
- segmentedNavStyle,
20
- segmentedTabActiveStyle,
21
- segmentedTabStyle,
22
- skeletonStyle,
23
- snippetStyle,
24
- statusStyle,
25
- titleStyle,
26
- warningStyle,
27
- } from "../styles.ts";
28
- import type { CopyField, GatewayView, GrokBuildSettingsInjected } from "../types.ts";
29
- import { Badge } from "./Badge.tsx";
30
- import { CopyButton } from "./CopyButton.tsx";
31
- import { ToggleSwitch } from "./ToggleSwitch.tsx";
32
-
33
- export interface GatewayTabProps {
34
- t: GrokBuildSettingsInjected["t"];
35
- gateway: GatewayView | undefined;
36
- gatewayError: string | undefined;
37
- gatewayBusy: boolean;
38
- gatewayOnceKey: string | undefined;
39
- gatewayKeyVisible: boolean;
40
- gatewayRotateConfirm: boolean;
41
- gatewayRevealError: string | undefined;
42
- portDraft: string;
43
- copiedField: CopyField | undefined;
44
- copyFailedField: CopyField | undefined;
45
- onEnabledChange: (enabled: boolean) => void;
46
- onRetry: () => void;
47
- onPortDraftChange: (value: string) => void;
48
- onApplyPort: () => void;
49
- onRandomPort: (port: number) => void;
50
- onCopy: (field: CopyField, text: string) => void;
51
- onCopyKey: () => void;
52
- onToggleKeyVisible: () => void;
53
- onRotateConfirm: () => void;
54
- onRotateCancel: () => void;
55
- onRotate: () => void;
56
- }
57
-
58
- const SNIPPET_TABS: readonly {
59
- id: GatewaySnippetId;
60
- labelKey: "gatewaySnippetCurl" | "gatewaySnippetPython" | "gatewaySnippetIde";
61
- }[] = [
62
- { id: "curl", labelKey: "gatewaySnippetCurl" },
63
- { id: "python", labelKey: "gatewaySnippetPython" },
64
- { id: "ide", labelKey: "gatewaySnippetIde" },
65
- ];
66
-
67
- export function GatewayTab({
68
- t,
69
- gateway,
70
- gatewayError,
71
- gatewayBusy,
72
- gatewayOnceKey,
73
- gatewayKeyVisible,
74
- gatewayRotateConfirm,
75
- gatewayRevealError,
76
- portDraft,
77
- copiedField,
78
- copyFailedField,
79
- onEnabledChange,
80
- onRetry,
81
- onPortDraftChange,
82
- onApplyPort,
83
- onRandomPort,
84
- onCopyKey,
85
- onToggleKeyVisible,
86
- onRotateConfirm,
87
- onRotateCancel,
88
- onRotate,
89
- }: GatewayTabProps) {
90
- const [enableConfirm, setEnableConfirm] = useState(false);
91
- const [activeSnippet, setActiveSnippet] = useState<GatewaySnippetId>("curl");
92
- const enableConfirmAction = useRef<HTMLButtonElement>(null);
93
- const rotateTrigger = useRef<HTMLButtonElement>(null);
94
- const rotateConfirmAction = useRef<HTMLButtonElement>(null);
95
- const restoreEnableFocus = useRef(false);
96
- const restoreRotateFocus = useRef(false);
97
-
98
- useEffect(() => {
99
- if (enableConfirm) {
100
- enableConfirmAction.current?.focus();
101
- return;
102
- }
103
- if (!restoreEnableFocus.current) return;
104
- restoreEnableFocus.current = false;
105
- document.getElementById("coding-oauth-gateway-enabled")?.focus();
106
- }, [enableConfirm]);
107
- useEffect(() => {
108
- if (gatewayRotateConfirm) {
109
- rotateConfirmAction.current?.focus();
110
- return;
111
- }
112
- if (!restoreRotateFocus.current) return;
113
- restoreRotateFocus.current = false;
114
- rotateTrigger.current?.focus();
115
- }, [gatewayRotateConfirm]);
116
-
117
- const portValid = parseGatewayPort(portDraft) !== undefined;
118
- const portChanged = gateway !== undefined && portDraft !== String(gateway.port);
119
-
120
- const snippets = useMemo(() => {
121
- if (gateway === undefined || !gatewayKeyVisible || gatewayOnceKey === undefined || gateway.models.length === 0) {
122
- return undefined;
123
- }
124
- const openAi = `${formatGatewayBaseUrl(gateway.bind, gateway.port)}/v1`;
125
- const anthropic = formatGatewayBaseUrl(gateway.bind, gateway.port);
126
- // A masked keyHint is display-only and can never produce an executable command.
127
- return buildGatewaySnippets(openAi, anthropic, gatewayOnceKey, gateway.models[0]);
128
- }, [gateway, gatewayKeyVisible, gatewayOnceKey]);
129
-
130
- const copyLabel = (field: CopyField, idle?: string): string => {
131
- if (copyFailedField === field) return t("copyFailed");
132
- if (copiedField === field) return t("copied");
133
- return idle ?? t("copy");
134
- };
135
-
136
- const focusSnippetTab = (index: number): void => {
137
- const tab = SNIPPET_TABS[index];
138
- if (tab === undefined) return;
139
- setActiveSnippet(tab.id);
140
- document.getElementById(`coding-oauth-snippet-tab-${tab.id}`)?.focus();
141
- };
142
-
143
- const onSnippetKeyDown = (event: KeyboardEvent<HTMLDivElement>): void => {
144
- const current = SNIPPET_TABS.findIndex((tab) => tab.id === activeSnippet);
145
- if (current < 0) return;
146
- if (event.key === "ArrowRight" || event.key === "ArrowDown") {
147
- event.preventDefault();
148
- focusSnippetTab((current + 1) % SNIPPET_TABS.length);
149
- } else if (event.key === "ArrowLeft" || event.key === "ArrowUp") {
150
- event.preventDefault();
151
- focusSnippetTab((current - 1 + SNIPPET_TABS.length) % SNIPPET_TABS.length);
152
- } else if (event.key === "Home") {
153
- event.preventDefault();
154
- focusSnippetTab(0);
155
- } else if (event.key === "End") {
156
- event.preventDefault();
157
- focusSnippetTab(SNIPPET_TABS.length - 1);
158
- }
159
- };
160
-
161
- const openAiUrl = gateway === undefined ? "" : `${formatGatewayBaseUrl(gateway.bind, gateway.port)}/v1`;
162
- const anthropicUrl = gateway === undefined ? "" : formatGatewayBaseUrl(gateway.bind, gateway.port);
163
-
164
- return (
165
- <section style={cardStyle} aria-labelledby="coding-oauth-gateway-title">
166
- <div>
167
- <h3 id="coding-oauth-gateway-title" style={{ ...titleStyle, fontSize: 16 }}>
168
- {t("gatewayTitle")}
169
- </h3>
170
- <p style={{ ...bodyStyle, marginTop: 4 }}>{t("gatewayIntro")}</p>
171
- <p style={{ ...warningStyle, marginTop: 8 }}>{t("gatewayWarning")}</p>
172
- <p style={{ ...hintStyle, marginTop: 8 }}>{t("gatewayLoopbackHint")}</p>
173
- </div>
174
- {gatewayError === undefined ? null : (
175
- <p style={errorStyle} role="alert">
176
- {gatewayError}
177
- </p>
178
- )}
179
- {gatewayRevealError === undefined ? null : (
180
- <p style={errorStyle} role="alert">
181
- {gatewayRevealError}
182
- </p>
183
- )}
184
- {gateway === undefined && gatewayError === undefined ? (
185
- <div style={skeletonStyle} role="status" aria-busy="true">
186
- <div style={statusStyle}>
187
- <span aria-hidden="true" style={dotStyle("loading")} />
188
- {t("gatewayLoading")}
189
- </div>
190
- </div>
191
- ) : gateway === undefined ? (
192
- <div style={nestedStyle} role="status">
193
- <p style={hintStyle}>{t("gatewayLoadFailed")}</p>
194
- <button type="button" style={buttonStyle} onClick={onRetry}>
195
- {t("retry")}
196
- </button>
197
- </div>
198
- ) : (
199
- <div style={nestedStyle}>
200
- <Badge
201
- label={gateway.running ? t("gatewayRunning") : t("gatewayStopped")}
202
- tone={gateway.running ? "success" : "neutral"}
203
- />
204
- <p style={hintStyle}>
205
- {gateway.models.length > 0
206
- ? t("gatewayModelsReady", { count: gateway.models.length, model: gateway.models[0] })
207
- : t("gatewayModelsUnavailable")}
208
- </p>
209
- <p style={hintStyle}>{gateway.keyConfigured ? t("gatewayKeyConfigured") : t("gatewayKeyNotConfigured")}</p>
210
- {enableConfirm ? (
211
- <div style={nestedStyle}>
212
- <p style={bodyStyle}>{t("gatewayEnableConfirm")}</p>
213
- <div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
214
- <button
215
- ref={enableConfirmAction}
216
- type="button"
217
- style={primaryButtonStyle}
218
- disabled={gatewayBusy}
219
- onClick={() => {
220
- restoreEnableFocus.current = true;
221
- setEnableConfirm(false);
222
- onEnabledChange(true);
223
- }}
224
- >
225
- {t("gatewayEnableConfirmAction")}
226
- </button>
227
- <button
228
- type="button"
229
- style={buttonStyle}
230
- disabled={gatewayBusy}
231
- onClick={() => {
232
- restoreEnableFocus.current = true;
233
- setEnableConfirm(false);
234
- }}
235
- >
236
- {t("gatewayEnableCancel")}
237
- </button>
238
- </div>
239
- </div>
240
- ) : (
241
- <label
242
- htmlFor="coding-oauth-gateway-enabled"
243
- style={{
244
- display: "flex",
245
- alignItems: "center",
246
- justifyContent: "space-between",
247
- gap: 12,
248
- fontSize: 14,
249
- color: "var(--dsw-alias-label-primary)",
250
- }}
251
- >
252
- <span>{t("gatewayEnabled")}</span>
253
- <ToggleSwitch
254
- id="coding-oauth-gateway-enabled"
255
- checked={gateway.enabled}
256
- disabled={gatewayBusy}
257
- onChange={(enabled) => {
258
- if (enabled) {
259
- setEnableConfirm(true);
260
- return;
261
- }
262
- onEnabledChange(false);
263
- }}
264
- />
265
- </label>
266
- )}
267
- <div>
268
- <label
269
- htmlFor="coding-oauth-gateway-port"
270
- style={{ ...bodyStyle, color: "var(--dsw-alias-label-primary)" }}
271
- >
272
- {t("gatewayPort")}
273
- </label>
274
- <p id="coding-oauth-gateway-port-hint" style={hintStyle}>
275
- {t("gatewayPortHint")}
276
- </p>
277
- {!portValid && portDraft.length > 0 ? (
278
- <p style={{ ...hintStyle, color: "var(--dsw-alias-state-error-primary)" }} role="alert">
279
- {t("gatewayPortInvalid")}
280
- </p>
281
- ) : null}
282
- <div style={{ display: "flex", flexWrap: "wrap", gap: 8, marginTop: 8 }}>
283
- <input
284
- id="coding-oauth-gateway-port"
285
- type="number"
286
- inputMode="numeric"
287
- min={GATEWAY_PORT_MIN}
288
- max={GATEWAY_PORT_MAX}
289
- step={1}
290
- value={portDraft}
291
- disabled={gatewayBusy}
292
- aria-describedby="coding-oauth-gateway-port-hint"
293
- aria-invalid={!portValid && portDraft.length > 0}
294
- style={{
295
- ...inputStyle,
296
- width: 112,
297
- flex: "0 0 112px",
298
- borderColor:
299
- !portValid && portDraft.length > 0
300
- ? "var(--dsw-alias-state-error-primary)"
301
- : "var(--dsw-alias-border-l2)",
302
- }}
303
- onChange={(event) => {
304
- onPortDraftChange(event.target.value);
305
- }}
306
- onKeyDown={(event) => {
307
- if (event.key === "Enter") {
308
- event.preventDefault();
309
- onApplyPort();
310
- }
311
- if (event.key === "Escape") {
312
- onPortDraftChange(String(gateway.port));
313
- }
314
- }}
315
- />
316
- <button
317
- type="button"
318
- style={primaryButtonStyle}
319
- disabled={gatewayBusy || !portChanged || !portValid}
320
- onClick={onApplyPort}
321
- >
322
- {t("gatewayPortApply")}
323
- </button>
324
- <button
325
- type="button"
326
- style={buttonStyle}
327
- disabled={gatewayBusy}
328
- onClick={() => {
329
- onRandomPort(randomGatewayPort(gateway.port));
330
- }}
331
- >
332
- {t("gatewayPortRandom")}
333
- </button>
334
- </div>
335
- </div>
336
- <p style={copyRowStyle}>
337
- <span style={hintStyle}>
338
- {t("gatewayOpenAiUrl")}
339
- <span style={{ display: "block", ...monoStyle }}>{openAiUrl}</span>
340
- </span>
341
- <CopyButton
342
- text={openAiUrl}
343
- idleLabel={t("copy")}
344
- copiedLabel={t("copied")}
345
- failedLabel={t("copyFailed")}
346
- />
347
- </p>
348
- <p style={copyRowStyle}>
349
- <span style={hintStyle}>
350
- {t("gatewayAnthropicUrl")}
351
- <span style={{ display: "block", ...monoStyle }}>{anthropicUrl}</span>
352
- </span>
353
- <CopyButton
354
- text={anthropicUrl}
355
- idleLabel={t("copy")}
356
- copiedLabel={t("copied")}
357
- failedLabel={t("copyFailed")}
358
- />
359
- </p>
360
- <p style={copyRowStyle}>
361
- <span style={hintStyle}>
362
- {t("gatewayKeyHint")}
363
- <span style={{ display: "block", ...monoStyle, overflowWrap: "anywhere" }}>
364
- {gatewayKeyVisible && gatewayOnceKey !== undefined ? gatewayOnceKey : gateway.keyHint || "—"}
365
- </span>
366
- </span>
367
- <span style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
368
- <button type="button" style={primaryButtonStyle} disabled={gatewayBusy} onClick={onCopyKey}>
369
- {copyLabel("key", t("gatewayCopyKey"))}
370
- </button>
371
- <button type="button" style={buttonStyle} disabled={gatewayBusy} onClick={onToggleKeyVisible}>
372
- {gatewayKeyVisible ? t("gatewayHideKey") : t("gatewayShowKey")}
373
- </button>
374
- </span>
375
- </p>
376
- <p style={hintStyle}>{t("gatewayKeyCopyHint")}</p>
377
- {gateway.enabled && snippets !== undefined ? (
378
- <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
379
- <h4 style={{ ...titleStyle, fontSize: 14 }}>{t("gatewaySnippetsTitle")}</h4>
380
- <div
381
- role="tablist"
382
- aria-label={t("gatewaySnippetsTitle")}
383
- style={segmentedNavStyle}
384
- onKeyDown={onSnippetKeyDown}
385
- >
386
- {SNIPPET_TABS.map((tab) => {
387
- const selected = activeSnippet === tab.id;
388
- return (
389
- <button
390
- key={tab.id}
391
- id={`coding-oauth-snippet-tab-${tab.id}`}
392
- type="button"
393
- role="tab"
394
- aria-selected={selected}
395
- aria-controls="coding-oauth-snippet-panel"
396
- tabIndex={selected ? 0 : -1}
397
- style={selected ? segmentedTabActiveStyle : segmentedTabStyle}
398
- onClick={() => {
399
- setActiveSnippet(tab.id);
400
- }}
401
- >
402
- {t(tab.labelKey)}
403
- </button>
404
- );
405
- })}
406
- </div>
407
- <div
408
- id="coding-oauth-snippet-panel"
409
- role="tabpanel"
410
- aria-labelledby={`coding-oauth-snippet-tab-${activeSnippet}`}
411
- >
412
- <code style={snippetStyle}>{snippets[activeSnippet]}</code>
413
- </div>
414
- <CopyButton
415
- text={snippets[activeSnippet]}
416
- idleLabel={t("copy")}
417
- copiedLabel={t("copied")}
418
- failedLabel={t("copyFailed")}
419
- />
420
- </div>
421
- ) : gateway.enabled ? (
422
- <p style={hintStyle}>{t("gatewaySnippetsUnavailable")}</p>
423
- ) : null}
424
- {gatewayRotateConfirm ? (
425
- <div style={nestedStyle}>
426
- <p style={bodyStyle}>{t("gatewayRotateConfirm")}</p>
427
- <p style={hintStyle}>{t("gatewayRotateConfirmHint")}</p>
428
- <div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
429
- <button
430
- ref={rotateConfirmAction}
431
- type="button"
432
- style={primaryButtonStyle}
433
- disabled={gatewayBusy}
434
- onClick={() => {
435
- restoreRotateFocus.current = true;
436
- onRotate();
437
- }}
438
- >
439
- {t("gatewayRotateConfirmAction")}
440
- </button>
441
- <button
442
- type="button"
443
- style={buttonStyle}
444
- disabled={gatewayBusy}
445
- onClick={() => {
446
- restoreRotateFocus.current = true;
447
- onRotateCancel();
448
- }}
449
- >
450
- {t("gatewayRotateCancel")}
451
- </button>
452
- </div>
453
- </div>
454
- ) : (
455
- <button
456
- ref={rotateTrigger}
457
- type="button"
458
- style={buttonStyle}
459
- disabled={gatewayBusy}
460
- onClick={onRotateConfirm}
461
- >
462
- {t("gatewayRotate")}
463
- </button>
464
- )}
465
- </div>
466
- )}
467
- </section>
468
- );
469
- }
1
+ /** Local API gateway settings tab. */
2
+
3
+ import { type KeyboardEvent, useEffect, useMemo, useRef, useState } from "react";
4
+ import { GATEWAY_PORT_MAX, GATEWAY_PORT_MIN } from "../constants.ts";
5
+ import { buildGatewaySnippets, type GatewaySnippetId } from "../gatewaySnippets.ts";
6
+ import { formatGatewayBaseUrl, parseGatewayPort, randomGatewayPort } from "../parsers.ts";
7
+ import {
8
+ bodyStyle,
9
+ buttonStyle,
10
+ cardStyle,
11
+ copyRowStyle,
12
+ dotStyle,
13
+ errorStyle,
14
+ hintStyle,
15
+ inputStyle,
16
+ monoStyle,
17
+ nestedStyle,
18
+ primaryButtonStyle,
19
+ segmentedNavStyle,
20
+ segmentedTabActiveStyle,
21
+ segmentedTabStyle,
22
+ skeletonStyle,
23
+ snippetStyle,
24
+ statusStyle,
25
+ titleStyle,
26
+ warningStyle,
27
+ } from "../styles.ts";
28
+ import type { CopyField, GatewayView, GrokBuildSettingsInjected } from "../types.ts";
29
+ import { Badge } from "./Badge.tsx";
30
+ import { CopyButton } from "./CopyButton.tsx";
31
+ import { ToggleSwitch } from "./ToggleSwitch.tsx";
32
+
33
+ export interface GatewayTabProps {
34
+ t: GrokBuildSettingsInjected["t"];
35
+ gateway: GatewayView | undefined;
36
+ gatewayError: string | undefined;
37
+ gatewayBusy: boolean;
38
+ gatewayOnceKey: string | undefined;
39
+ gatewayKeyVisible: boolean;
40
+ gatewayRotateConfirm: boolean;
41
+ gatewayRevealError: string | undefined;
42
+ portDraft: string;
43
+ copiedField: CopyField | undefined;
44
+ copyFailedField: CopyField | undefined;
45
+ onEnabledChange: (enabled: boolean) => void;
46
+ onRetry: () => void;
47
+ onPortDraftChange: (value: string) => void;
48
+ onApplyPort: () => void;
49
+ onRandomPort: (port: number) => void;
50
+ onCopy: (field: CopyField, text: string) => void;
51
+ onCopyKey: () => void;
52
+ onToggleKeyVisible: () => void;
53
+ onRotateConfirm: () => void;
54
+ onRotateCancel: () => void;
55
+ onRotate: () => void;
56
+ }
57
+
58
+ const SNIPPET_TABS: readonly {
59
+ id: GatewaySnippetId;
60
+ labelKey: "gatewaySnippetCurl" | "gatewaySnippetPython" | "gatewaySnippetIde";
61
+ }[] = [
62
+ { id: "curl", labelKey: "gatewaySnippetCurl" },
63
+ { id: "python", labelKey: "gatewaySnippetPython" },
64
+ { id: "ide", labelKey: "gatewaySnippetIde" },
65
+ ];
66
+
67
+ export function GatewayTab({
68
+ t,
69
+ gateway,
70
+ gatewayError,
71
+ gatewayBusy,
72
+ gatewayOnceKey,
73
+ gatewayKeyVisible,
74
+ gatewayRotateConfirm,
75
+ gatewayRevealError,
76
+ portDraft,
77
+ copiedField,
78
+ copyFailedField,
79
+ onEnabledChange,
80
+ onRetry,
81
+ onPortDraftChange,
82
+ onApplyPort,
83
+ onRandomPort,
84
+ onCopyKey,
85
+ onToggleKeyVisible,
86
+ onRotateConfirm,
87
+ onRotateCancel,
88
+ onRotate,
89
+ }: GatewayTabProps) {
90
+ const [enableConfirm, setEnableConfirm] = useState(false);
91
+ const [activeSnippet, setActiveSnippet] = useState<GatewaySnippetId>("curl");
92
+ const enableConfirmAction = useRef<HTMLButtonElement>(null);
93
+ const rotateTrigger = useRef<HTMLButtonElement>(null);
94
+ const rotateConfirmAction = useRef<HTMLButtonElement>(null);
95
+ const restoreEnableFocus = useRef(false);
96
+ const restoreRotateFocus = useRef(false);
97
+
98
+ useEffect(() => {
99
+ if (enableConfirm) {
100
+ enableConfirmAction.current?.focus();
101
+ return;
102
+ }
103
+ if (!restoreEnableFocus.current) return;
104
+ restoreEnableFocus.current = false;
105
+ document.getElementById("coding-oauth-gateway-enabled")?.focus();
106
+ }, [enableConfirm]);
107
+ useEffect(() => {
108
+ if (gatewayRotateConfirm) {
109
+ rotateConfirmAction.current?.focus();
110
+ return;
111
+ }
112
+ if (!restoreRotateFocus.current) return;
113
+ restoreRotateFocus.current = false;
114
+ rotateTrigger.current?.focus();
115
+ }, [gatewayRotateConfirm]);
116
+
117
+ const portValid = parseGatewayPort(portDraft) !== undefined;
118
+ const portChanged = gateway !== undefined && portDraft !== String(gateway.port);
119
+
120
+ const snippets = useMemo(() => {
121
+ if (gateway === undefined || !gatewayKeyVisible || gatewayOnceKey === undefined || gateway.models.length === 0) {
122
+ return undefined;
123
+ }
124
+ const openAi = `${formatGatewayBaseUrl(gateway.bind, gateway.port)}/v1`;
125
+ const anthropic = formatGatewayBaseUrl(gateway.bind, gateway.port);
126
+ // A masked keyHint is display-only and can never produce an executable command.
127
+ return buildGatewaySnippets(openAi, anthropic, gatewayOnceKey, gateway.models[0]);
128
+ }, [gateway, gatewayKeyVisible, gatewayOnceKey]);
129
+
130
+ const copyLabel = (field: CopyField, idle?: string): string => {
131
+ if (copyFailedField === field) return t("copyFailed");
132
+ if (copiedField === field) return t("copied");
133
+ return idle ?? t("copy");
134
+ };
135
+
136
+ const focusSnippetTab = (index: number): void => {
137
+ const tab = SNIPPET_TABS[index];
138
+ if (tab === undefined) return;
139
+ setActiveSnippet(tab.id);
140
+ document.getElementById(`coding-oauth-snippet-tab-${tab.id}`)?.focus();
141
+ };
142
+
143
+ const onSnippetKeyDown = (event: KeyboardEvent<HTMLDivElement>): void => {
144
+ const current = SNIPPET_TABS.findIndex((tab) => tab.id === activeSnippet);
145
+ if (current < 0) return;
146
+ if (event.key === "ArrowRight" || event.key === "ArrowDown") {
147
+ event.preventDefault();
148
+ focusSnippetTab((current + 1) % SNIPPET_TABS.length);
149
+ } else if (event.key === "ArrowLeft" || event.key === "ArrowUp") {
150
+ event.preventDefault();
151
+ focusSnippetTab((current - 1 + SNIPPET_TABS.length) % SNIPPET_TABS.length);
152
+ } else if (event.key === "Home") {
153
+ event.preventDefault();
154
+ focusSnippetTab(0);
155
+ } else if (event.key === "End") {
156
+ event.preventDefault();
157
+ focusSnippetTab(SNIPPET_TABS.length - 1);
158
+ }
159
+ };
160
+
161
+ const openAiUrl = gateway === undefined ? "" : `${formatGatewayBaseUrl(gateway.bind, gateway.port)}/v1`;
162
+ const anthropicUrl = gateway === undefined ? "" : formatGatewayBaseUrl(gateway.bind, gateway.port);
163
+
164
+ return (
165
+ <section style={cardStyle} aria-labelledby="coding-oauth-gateway-title">
166
+ <div>
167
+ <h3 id="coding-oauth-gateway-title" style={{ ...titleStyle, fontSize: 16 }}>
168
+ {t("gatewayTitle")}
169
+ </h3>
170
+ <p style={{ ...bodyStyle, marginTop: 4 }}>{t("gatewayIntro")}</p>
171
+ <p style={{ ...warningStyle, marginTop: 8 }}>{t("gatewayWarning")}</p>
172
+ <p style={{ ...hintStyle, marginTop: 8 }}>{t("gatewayLoopbackHint")}</p>
173
+ </div>
174
+ {gatewayError === undefined ? null : (
175
+ <p style={errorStyle} role="alert">
176
+ {gatewayError}
177
+ </p>
178
+ )}
179
+ {gatewayRevealError === undefined ? null : (
180
+ <p style={errorStyle} role="alert">
181
+ {gatewayRevealError}
182
+ </p>
183
+ )}
184
+ {gateway === undefined && gatewayError === undefined ? (
185
+ <div style={skeletonStyle} role="status" aria-busy="true">
186
+ <div style={statusStyle}>
187
+ <span aria-hidden="true" style={dotStyle("loading")} />
188
+ {t("gatewayLoading")}
189
+ </div>
190
+ </div>
191
+ ) : gateway === undefined ? (
192
+ <div style={nestedStyle} role="status">
193
+ <p style={hintStyle}>{t("gatewayLoadFailed")}</p>
194
+ <button type="button" style={buttonStyle} onClick={onRetry}>
195
+ {t("retry")}
196
+ </button>
197
+ </div>
198
+ ) : (
199
+ <div style={nestedStyle}>
200
+ <Badge
201
+ label={gateway.running ? t("gatewayRunning") : t("gatewayStopped")}
202
+ tone={gateway.running ? "success" : "neutral"}
203
+ />
204
+ <p style={hintStyle}>
205
+ {gateway.models.length > 0
206
+ ? t("gatewayModelsReady", { count: gateway.models.length, model: gateway.models[0] })
207
+ : t("gatewayModelsUnavailable")}
208
+ </p>
209
+ <p style={hintStyle}>{gateway.keyConfigured ? t("gatewayKeyConfigured") : t("gatewayKeyNotConfigured")}</p>
210
+ {enableConfirm ? (
211
+ <div style={nestedStyle}>
212
+ <p style={bodyStyle}>{t("gatewayEnableConfirm")}</p>
213
+ <div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
214
+ <button
215
+ ref={enableConfirmAction}
216
+ type="button"
217
+ style={primaryButtonStyle}
218
+ disabled={gatewayBusy}
219
+ onClick={() => {
220
+ restoreEnableFocus.current = true;
221
+ setEnableConfirm(false);
222
+ onEnabledChange(true);
223
+ }}
224
+ >
225
+ {t("gatewayEnableConfirmAction")}
226
+ </button>
227
+ <button
228
+ type="button"
229
+ style={buttonStyle}
230
+ disabled={gatewayBusy}
231
+ onClick={() => {
232
+ restoreEnableFocus.current = true;
233
+ setEnableConfirm(false);
234
+ }}
235
+ >
236
+ {t("gatewayEnableCancel")}
237
+ </button>
238
+ </div>
239
+ </div>
240
+ ) : (
241
+ <label
242
+ htmlFor="coding-oauth-gateway-enabled"
243
+ style={{
244
+ display: "flex",
245
+ alignItems: "center",
246
+ justifyContent: "space-between",
247
+ gap: 12,
248
+ fontSize: 14,
249
+ color: "var(--dsw-alias-label-primary)",
250
+ }}
251
+ >
252
+ <span>{t("gatewayEnabled")}</span>
253
+ <ToggleSwitch
254
+ id="coding-oauth-gateway-enabled"
255
+ checked={gateway.enabled}
256
+ disabled={gatewayBusy}
257
+ onChange={(enabled) => {
258
+ if (enabled) {
259
+ setEnableConfirm(true);
260
+ return;
261
+ }
262
+ onEnabledChange(false);
263
+ }}
264
+ />
265
+ </label>
266
+ )}
267
+ <div>
268
+ <label
269
+ htmlFor="coding-oauth-gateway-port"
270
+ style={{ ...bodyStyle, color: "var(--dsw-alias-label-primary)" }}
271
+ >
272
+ {t("gatewayPort")}
273
+ </label>
274
+ <p id="coding-oauth-gateway-port-hint" style={hintStyle}>
275
+ {t("gatewayPortHint")}
276
+ </p>
277
+ {!portValid && portDraft.length > 0 ? (
278
+ <p style={{ ...hintStyle, color: "var(--dsw-alias-state-error-primary)" }} role="alert">
279
+ {t("gatewayPortInvalid")}
280
+ </p>
281
+ ) : null}
282
+ <div style={{ display: "flex", flexWrap: "wrap", gap: 8, marginTop: 8 }}>
283
+ <input
284
+ id="coding-oauth-gateway-port"
285
+ type="number"
286
+ inputMode="numeric"
287
+ min={GATEWAY_PORT_MIN}
288
+ max={GATEWAY_PORT_MAX}
289
+ step={1}
290
+ value={portDraft}
291
+ disabled={gatewayBusy}
292
+ aria-describedby="coding-oauth-gateway-port-hint"
293
+ aria-invalid={!portValid && portDraft.length > 0}
294
+ style={{
295
+ ...inputStyle,
296
+ width: 112,
297
+ flex: "0 0 112px",
298
+ borderColor:
299
+ !portValid && portDraft.length > 0
300
+ ? "var(--dsw-alias-state-error-primary)"
301
+ : "var(--dsw-alias-border-l2)",
302
+ }}
303
+ onChange={(event) => {
304
+ onPortDraftChange(event.target.value);
305
+ }}
306
+ onKeyDown={(event) => {
307
+ if (event.key === "Enter") {
308
+ event.preventDefault();
309
+ onApplyPort();
310
+ }
311
+ if (event.key === "Escape") {
312
+ onPortDraftChange(String(gateway.port));
313
+ }
314
+ }}
315
+ />
316
+ <button
317
+ type="button"
318
+ style={primaryButtonStyle}
319
+ disabled={gatewayBusy || !portChanged || !portValid}
320
+ onClick={onApplyPort}
321
+ >
322
+ {t("gatewayPortApply")}
323
+ </button>
324
+ <button
325
+ type="button"
326
+ style={buttonStyle}
327
+ disabled={gatewayBusy}
328
+ onClick={() => {
329
+ onRandomPort(randomGatewayPort(gateway.port));
330
+ }}
331
+ >
332
+ {t("gatewayPortRandom")}
333
+ </button>
334
+ </div>
335
+ </div>
336
+ <p style={copyRowStyle}>
337
+ <span style={hintStyle}>
338
+ {t("gatewayOpenAiUrl")}
339
+ <span style={{ display: "block", ...monoStyle }}>{openAiUrl}</span>
340
+ </span>
341
+ <CopyButton
342
+ text={openAiUrl}
343
+ idleLabel={t("copy")}
344
+ copiedLabel={t("copied")}
345
+ failedLabel={t("copyFailed")}
346
+ />
347
+ </p>
348
+ <p style={copyRowStyle}>
349
+ <span style={hintStyle}>
350
+ {t("gatewayAnthropicUrl")}
351
+ <span style={{ display: "block", ...monoStyle }}>{anthropicUrl}</span>
352
+ </span>
353
+ <CopyButton
354
+ text={anthropicUrl}
355
+ idleLabel={t("copy")}
356
+ copiedLabel={t("copied")}
357
+ failedLabel={t("copyFailed")}
358
+ />
359
+ </p>
360
+ <p style={copyRowStyle}>
361
+ <span style={hintStyle}>
362
+ {t("gatewayKeyHint")}
363
+ <span style={{ display: "block", ...monoStyle, overflowWrap: "anywhere" }}>
364
+ {gatewayKeyVisible && gatewayOnceKey !== undefined ? gatewayOnceKey : gateway.keyHint || "—"}
365
+ </span>
366
+ </span>
367
+ <span style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
368
+ <button type="button" style={primaryButtonStyle} disabled={gatewayBusy} onClick={onCopyKey}>
369
+ {copyLabel("key", t("gatewayCopyKey"))}
370
+ </button>
371
+ <button type="button" style={buttonStyle} disabled={gatewayBusy} onClick={onToggleKeyVisible}>
372
+ {gatewayKeyVisible ? t("gatewayHideKey") : t("gatewayShowKey")}
373
+ </button>
374
+ </span>
375
+ </p>
376
+ <p style={hintStyle}>{t("gatewayKeyCopyHint")}</p>
377
+ {gateway.enabled && snippets !== undefined ? (
378
+ <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
379
+ <h4 style={{ ...titleStyle, fontSize: 14 }}>{t("gatewaySnippetsTitle")}</h4>
380
+ <div
381
+ role="tablist"
382
+ aria-label={t("gatewaySnippetsTitle")}
383
+ style={segmentedNavStyle}
384
+ onKeyDown={onSnippetKeyDown}
385
+ >
386
+ {SNIPPET_TABS.map((tab) => {
387
+ const selected = activeSnippet === tab.id;
388
+ return (
389
+ <button
390
+ key={tab.id}
391
+ id={`coding-oauth-snippet-tab-${tab.id}`}
392
+ type="button"
393
+ role="tab"
394
+ aria-selected={selected}
395
+ aria-controls="coding-oauth-snippet-panel"
396
+ tabIndex={selected ? 0 : -1}
397
+ style={selected ? segmentedTabActiveStyle : segmentedTabStyle}
398
+ onClick={() => {
399
+ setActiveSnippet(tab.id);
400
+ }}
401
+ >
402
+ {t(tab.labelKey)}
403
+ </button>
404
+ );
405
+ })}
406
+ </div>
407
+ <div
408
+ id="coding-oauth-snippet-panel"
409
+ role="tabpanel"
410
+ aria-labelledby={`coding-oauth-snippet-tab-${activeSnippet}`}
411
+ >
412
+ <code style={snippetStyle}>{snippets[activeSnippet]}</code>
413
+ </div>
414
+ <CopyButton
415
+ text={snippets[activeSnippet]}
416
+ idleLabel={t("copy")}
417
+ copiedLabel={t("copied")}
418
+ failedLabel={t("copyFailed")}
419
+ />
420
+ </div>
421
+ ) : gateway.enabled ? (
422
+ <p style={hintStyle}>{t("gatewaySnippetsUnavailable")}</p>
423
+ ) : null}
424
+ {gatewayRotateConfirm ? (
425
+ <div style={nestedStyle}>
426
+ <p style={bodyStyle}>{t("gatewayRotateConfirm")}</p>
427
+ <p style={hintStyle}>{t("gatewayRotateConfirmHint")}</p>
428
+ <div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
429
+ <button
430
+ ref={rotateConfirmAction}
431
+ type="button"
432
+ style={primaryButtonStyle}
433
+ disabled={gatewayBusy}
434
+ onClick={() => {
435
+ restoreRotateFocus.current = true;
436
+ onRotate();
437
+ }}
438
+ >
439
+ {t("gatewayRotateConfirmAction")}
440
+ </button>
441
+ <button
442
+ type="button"
443
+ style={buttonStyle}
444
+ disabled={gatewayBusy}
445
+ onClick={() => {
446
+ restoreRotateFocus.current = true;
447
+ onRotateCancel();
448
+ }}
449
+ >
450
+ {t("gatewayRotateCancel")}
451
+ </button>
452
+ </div>
453
+ </div>
454
+ ) : (
455
+ <button
456
+ ref={rotateTrigger}
457
+ type="button"
458
+ style={buttonStyle}
459
+ disabled={gatewayBusy}
460
+ onClick={onRotateConfirm}
461
+ >
462
+ {t("gatewayRotate")}
463
+ </button>
464
+ )}
465
+ </div>
466
+ )}
467
+ </section>
468
+ );
469
+ }