dsh-coding-subscription-oauth 0.5.8 → 0.6.2

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