dsh-coding-subscription-oauth 0.5.3 → 0.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/INSTALL.md +4 -4
  3. package/README.de.md +23 -6
  4. package/README.es.md +24 -6
  5. package/README.fr.md +24 -6
  6. package/README.ja.md +24 -6
  7. package/README.ko.md +24 -6
  8. package/README.md +23 -6
  9. package/README.pt-BR.md +24 -6
  10. package/README.ru.md +24 -6
  11. package/README.zh-CN.md +23 -6
  12. package/docs/02-architecture.md +1 -1
  13. package/docs/02-architecture.zh-CN.md +1 -1
  14. package/lib/client.js +34 -1
  15. package/lib/client.js.map +4 -4
  16. package/media/settings_accounts.png +0 -0
  17. package/media/settings_capabilities.png +0 -0
  18. package/media/settings_gateway.png +0 -0
  19. package/media/settings_overview.png +0 -0
  20. package/package.json +2 -1
  21. package/src/client/GrokBuildSettings.tsx +270 -1773
  22. package/src/client/api.ts +88 -0
  23. package/src/client/components/AboutTab.tsx +20 -0
  24. package/src/client/components/AccountsTab.tsx +224 -0
  25. package/src/client/components/Badge.tsx +33 -0
  26. package/src/client/components/CapabilitiesTab.tsx +227 -0
  27. package/src/client/components/CliPullPreview.tsx +107 -0
  28. package/src/client/components/CopyButton.tsx +57 -0
  29. package/src/client/components/GatewayTab.tsx +364 -0
  30. package/src/client/components/NoticeBanner.tsx +46 -0
  31. package/src/client/components/ProgressBar.tsx +52 -0
  32. package/src/client/components/ProviderCard.tsx +502 -0
  33. package/src/client/components/SettingsTabs.tsx +75 -0
  34. package/src/client/components/ToggleSwitch.tsx +60 -0
  35. package/src/client/constants.ts +206 -0
  36. package/src/client/display.ts +61 -0
  37. package/src/client/gatewaySnippets.ts +36 -0
  38. package/src/client/locales.ts +81 -15
  39. package/src/client/microStyles.ts +33 -0
  40. package/src/client/parsers.ts +386 -0
  41. package/src/client/styles.ts +306 -0
  42. package/src/client/types.ts +185 -0
@@ -0,0 +1,57 @@
1
+ /** Compact copy button with copied / failed feedback. */
2
+
3
+ import { useCallback, useEffect, useRef, useState } from "react";
4
+ import { copyText } from "../api.ts";
5
+ import { compactButtonStyle, primaryButtonStyle } from "../styles.ts";
6
+
7
+ export interface CopyButtonProps {
8
+ text: string;
9
+ idleLabel: string;
10
+ copiedLabel: string;
11
+ failedLabel: string;
12
+ primary?: boolean;
13
+ disabled?: boolean;
14
+ }
15
+
16
+ export function CopyButton({
17
+ text,
18
+ idleLabel,
19
+ copiedLabel,
20
+ failedLabel,
21
+ primary = false,
22
+ disabled = false,
23
+ }: CopyButtonProps) {
24
+ const [state, setState] = useState<"idle" | "copied" | "failed">("idle");
25
+ const timerRef = useRef<number | undefined>(undefined);
26
+
27
+ useEffect(() => {
28
+ return () => {
29
+ if (timerRef.current !== undefined) window.clearTimeout(timerRef.current);
30
+ };
31
+ }, []);
32
+
33
+ const handleClick = useCallback(async () => {
34
+ const ok = await copyText(text);
35
+ setState(ok ? "copied" : "failed");
36
+ if (timerRef.current !== undefined) window.clearTimeout(timerRef.current);
37
+ timerRef.current = window.setTimeout(() => {
38
+ setState("idle");
39
+ timerRef.current = undefined;
40
+ }, 2000);
41
+ }, [text]);
42
+
43
+ const label = state === "copied" ? copiedLabel : state === "failed" ? failedLabel : idleLabel;
44
+
45
+ return (
46
+ <button
47
+ type="button"
48
+ style={primary ? primaryButtonStyle : compactButtonStyle}
49
+ disabled={disabled || text.length === 0}
50
+ onClick={() => {
51
+ void handleClick();
52
+ }}
53
+ >
54
+ {label}
55
+ </button>
56
+ );
57
+ }
@@ -0,0 +1,364 @@
1
+ /** Local API gateway settings tab. */
2
+
3
+ import { 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
+ checkRowStyle,
12
+ copyRowStyle,
13
+ dotStyle,
14
+ errorStyle,
15
+ hintStyle,
16
+ inputStyle,
17
+ monoStyle,
18
+ nestedStyle,
19
+ primaryButtonStyle,
20
+ segmentedNavStyle,
21
+ segmentedTabActiveStyle,
22
+ segmentedTabStyle,
23
+ skeletonStyle,
24
+ snippetStyle,
25
+ statusStyle,
26
+ titleStyle,
27
+ warningStyle,
28
+ } from "../styles.ts";
29
+ import type { CopyField, GatewayView, GrokBuildSettingsInjected } from "../types.ts";
30
+ import { Badge } from "./Badge.tsx";
31
+ import { CopyButton } from "./CopyButton.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
+ onCopy,
83
+ onCopyKey,
84
+ onToggleKeyVisible,
85
+ onRotateConfirm,
86
+ onRotateCancel,
87
+ onRotate,
88
+ }: GatewayTabProps) {
89
+ const [enableConfirm, setEnableConfirm] = useState(false);
90
+ const [activeSnippet, setActiveSnippet] = useState<GatewaySnippetId>("curl");
91
+
92
+ const portValid = parseGatewayPort(portDraft) !== undefined;
93
+ const portChanged = gateway !== undefined && portDraft !== String(gateway.port);
94
+
95
+ const snippets = useMemo(() => {
96
+ if (gateway === undefined) return undefined;
97
+ const openAi = `${formatGatewayBaseUrl(gateway.bind, gateway.port)}/v1`;
98
+ const anthropic = formatGatewayBaseUrl(gateway.bind, gateway.port);
99
+ const key =
100
+ gatewayKeyVisible && gatewayOnceKey !== undefined
101
+ ? gatewayOnceKey
102
+ : gateway.keyHint.length > 0
103
+ ? gateway.keyHint
104
+ : "";
105
+ return buildGatewaySnippets(openAi, anthropic, key);
106
+ }, [gateway, gatewayKeyVisible, gatewayOnceKey]);
107
+
108
+ const copyLabel = (field: CopyField, idle?: string): string => {
109
+ if (copyFailedField === field) return t("copyFailed");
110
+ if (copiedField === field) return t("copied");
111
+ return idle ?? t("copy");
112
+ };
113
+
114
+ return (
115
+ <section style={cardStyle} aria-labelledby="coding-oauth-gateway-title">
116
+ <div>
117
+ <h3 id="coding-oauth-gateway-title" style={{ ...titleStyle, fontSize: 16 }}>
118
+ {t("gatewayTitle")}
119
+ </h3>
120
+ <p style={{ ...bodyStyle, marginTop: 4 }}>{t("gatewayIntro")}</p>
121
+ <p style={{ ...warningStyle, marginTop: 8 }}>{t("gatewayWarning")}</p>
122
+ <p style={{ ...hintStyle, marginTop: 8 }}>{t("gatewayLoopbackHint")}</p>
123
+ </div>
124
+ {gatewayError === undefined ? null : (
125
+ <p style={errorStyle} role="alert">
126
+ {gatewayError}
127
+ </p>
128
+ )}
129
+ {gatewayRevealError === undefined ? null : (
130
+ <p style={errorStyle} role="alert">
131
+ {gatewayRevealError}
132
+ </p>
133
+ )}
134
+ {gateway === undefined && gatewayError === undefined ? (
135
+ <div style={skeletonStyle} role="status" aria-busy="true">
136
+ <div style={statusStyle}>
137
+ <span aria-hidden="true" style={dotStyle("loading")} />
138
+ {t("gatewayLoading")}
139
+ </div>
140
+ </div>
141
+ ) : gateway === undefined ? null : (
142
+ <div style={nestedStyle}>
143
+ <Badge
144
+ label={gateway.running ? t("gatewayRunning") : t("gatewayStopped")}
145
+ tone={gateway.running ? "success" : "neutral"}
146
+ />
147
+ {enableConfirm ? (
148
+ <div style={nestedStyle}>
149
+ <p style={bodyStyle}>{t("gatewayEnableConfirm")}</p>
150
+ <div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
151
+ <button
152
+ type="button"
153
+ style={primaryButtonStyle}
154
+ disabled={gatewayBusy}
155
+ onClick={() => {
156
+ setEnableConfirm(false);
157
+ onEnabledChange(true);
158
+ }}
159
+ >
160
+ {t("gatewayEnableConfirmAction")}
161
+ </button>
162
+ <button
163
+ type="button"
164
+ style={buttonStyle}
165
+ disabled={gatewayBusy}
166
+ onClick={() => {
167
+ setEnableConfirm(false);
168
+ }}
169
+ >
170
+ {t("gatewayEnableCancel")}
171
+ </button>
172
+ </div>
173
+ </div>
174
+ ) : (
175
+ <label style={checkRowStyle}>
176
+ <input
177
+ type="checkbox"
178
+ checked={gateway.enabled}
179
+ disabled={gatewayBusy}
180
+ onChange={(event) => {
181
+ const enabled = event.target.checked;
182
+ if (enabled) {
183
+ setEnableConfirm(true);
184
+ return;
185
+ }
186
+ onEnabledChange(false);
187
+ }}
188
+ />
189
+ <span>{t("gatewayEnabled")}</span>
190
+ </label>
191
+ )}
192
+ <div>
193
+ <label
194
+ htmlFor="coding-oauth-gateway-port"
195
+ style={{ ...bodyStyle, color: "var(--dsw-alias-label-primary)" }}
196
+ >
197
+ {t("gatewayPort")}
198
+ </label>
199
+ <p id="coding-oauth-gateway-port-hint" style={hintStyle}>
200
+ {t("gatewayPortHint")}
201
+ </p>
202
+ {!portValid && portDraft.length > 0 ? (
203
+ <p style={{ ...hintStyle, color: "var(--dsw-alias-state-error-primary)" }} role="alert">
204
+ {t("gatewayPortInvalid")}
205
+ </p>
206
+ ) : null}
207
+ <div style={{ display: "flex", flexWrap: "wrap", gap: 8, marginTop: 8 }}>
208
+ <input
209
+ id="coding-oauth-gateway-port"
210
+ type="number"
211
+ inputMode="numeric"
212
+ min={GATEWAY_PORT_MIN}
213
+ max={GATEWAY_PORT_MAX}
214
+ step={1}
215
+ value={portDraft}
216
+ disabled={gatewayBusy}
217
+ aria-describedby="coding-oauth-gateway-port-hint"
218
+ aria-invalid={!portValid && portDraft.length > 0}
219
+ style={{
220
+ ...inputStyle,
221
+ width: 112,
222
+ flex: "0 0 112px",
223
+ borderColor:
224
+ !portValid && portDraft.length > 0
225
+ ? "var(--dsw-alias-state-error-primary)"
226
+ : "var(--dsw-alias-border-l2)",
227
+ }}
228
+ onChange={(event) => {
229
+ onPortDraftChange(event.target.value);
230
+ }}
231
+ onKeyDown={(event) => {
232
+ if (event.key === "Enter") {
233
+ event.preventDefault();
234
+ onApplyPort();
235
+ }
236
+ if (event.key === "Escape") {
237
+ onPortDraftChange(String(gateway.port));
238
+ }
239
+ }}
240
+ />
241
+ <button
242
+ type="button"
243
+ style={primaryButtonStyle}
244
+ disabled={gatewayBusy || !portChanged || !portValid}
245
+ onClick={onApplyPort}
246
+ >
247
+ {t("gatewayPortApply")}
248
+ </button>
249
+ <button
250
+ type="button"
251
+ style={buttonStyle}
252
+ disabled={gatewayBusy}
253
+ onClick={() => {
254
+ onRandomPort(randomGatewayPort(gateway.port));
255
+ }}
256
+ >
257
+ {t("gatewayPortRandom")}
258
+ </button>
259
+ </div>
260
+ </div>
261
+ <p style={copyRowStyle}>
262
+ <span style={hintStyle}>
263
+ {t("gatewayOpenAiUrl")}
264
+ <span style={{ display: "block", ...monoStyle }}>
265
+ {`${formatGatewayBaseUrl(gateway.bind, gateway.port)}/v1`}
266
+ </span>
267
+ </span>
268
+ <button
269
+ type="button"
270
+ style={primaryButtonStyle}
271
+ onClick={() => {
272
+ onCopy("openai", `${formatGatewayBaseUrl(gateway.bind, gateway.port)}/v1`);
273
+ }}
274
+ >
275
+ {copyLabel("openai")}
276
+ </button>
277
+ </p>
278
+ <p style={copyRowStyle}>
279
+ <span style={hintStyle}>
280
+ {t("gatewayAnthropicUrl")}
281
+ <span style={{ display: "block", ...monoStyle }}>{formatGatewayBaseUrl(gateway.bind, gateway.port)}</span>
282
+ </span>
283
+ <button
284
+ type="button"
285
+ style={buttonStyle}
286
+ onClick={() => {
287
+ onCopy("anthropic", formatGatewayBaseUrl(gateway.bind, gateway.port));
288
+ }}
289
+ >
290
+ {copyLabel("anthropic")}
291
+ </button>
292
+ </p>
293
+ <p style={copyRowStyle}>
294
+ <span style={hintStyle}>
295
+ {t("gatewayKeyHint")}
296
+ <span style={{ display: "block", ...monoStyle, overflowWrap: "anywhere" }}>
297
+ {gatewayKeyVisible && gatewayOnceKey !== undefined ? gatewayOnceKey : gateway.keyHint || "—"}
298
+ </span>
299
+ </span>
300
+ <span style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
301
+ <button type="button" style={primaryButtonStyle} disabled={gatewayBusy} onClick={onCopyKey}>
302
+ {copyLabel("key", t("gatewayCopyKey"))}
303
+ </button>
304
+ <button type="button" style={buttonStyle} disabled={gatewayBusy} onClick={onToggleKeyVisible}>
305
+ {gatewayKeyVisible ? t("gatewayHideKey") : t("gatewayShowKey")}
306
+ </button>
307
+ </span>
308
+ </p>
309
+ <p style={hintStyle}>{t("gatewayKeyCopyHint")}</p>
310
+ {gateway.enabled && snippets !== undefined ? (
311
+ <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
312
+ <h4 style={{ ...titleStyle, fontSize: 14 }}>{t("gatewaySnippetsTitle")}</h4>
313
+ <div role="tablist" aria-label={t("gatewaySnippetsTitle")} style={segmentedNavStyle}>
314
+ {SNIPPET_TABS.map((tab) => {
315
+ const selected = activeSnippet === tab.id;
316
+ return (
317
+ <button
318
+ key={tab.id}
319
+ type="button"
320
+ role="tab"
321
+ aria-selected={selected}
322
+ style={selected ? segmentedTabActiveStyle : segmentedTabStyle}
323
+ onClick={() => {
324
+ setActiveSnippet(tab.id);
325
+ }}
326
+ >
327
+ {t(tab.labelKey)}
328
+ </button>
329
+ );
330
+ })}
331
+ </div>
332
+ <code style={snippetStyle}>{snippets[activeSnippet]}</code>
333
+ <CopyButton
334
+ text={snippets[activeSnippet]}
335
+ idleLabel={t("copy")}
336
+ copiedLabel={t("copied")}
337
+ failedLabel={t("copyFailed")}
338
+ primary
339
+ />
340
+ </div>
341
+ ) : null}
342
+ {gatewayRotateConfirm ? (
343
+ <div style={nestedStyle}>
344
+ <p style={bodyStyle}>{t("gatewayRotateConfirm")}</p>
345
+ <p style={hintStyle}>{t("gatewayRotateConfirmHint")}</p>
346
+ <div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
347
+ <button type="button" style={buttonStyle} disabled={gatewayBusy} onClick={onRotate}>
348
+ {t("gatewayRotateConfirmAction")}
349
+ </button>
350
+ <button type="button" style={buttonStyle} disabled={gatewayBusy} onClick={onRotateCancel}>
351
+ {t("gatewayRotateCancel")}
352
+ </button>
353
+ </div>
354
+ </div>
355
+ ) : (
356
+ <button type="button" style={buttonStyle} disabled={gatewayBusy} onClick={onRotateConfirm}>
357
+ {t("gatewayRotate")}
358
+ </button>
359
+ )}
360
+ </div>
361
+ )}
362
+ </section>
363
+ );
364
+ }
@@ -0,0 +1,46 @@
1
+ /** Dismissible notice banner with optional auto-hide. */
2
+
3
+ import { useEffect } from "react";
4
+ import { bodyStyle, buttonStyle, noticeStyle } from "../styles.ts";
5
+
6
+ export interface NoticeBannerProps {
7
+ message: string;
8
+ dismissLabel?: string;
9
+ onDismiss?: () => void;
10
+ autoHideMs?: number;
11
+ tone?: "info" | "success";
12
+ }
13
+
14
+ export function NoticeBanner({ message, dismissLabel, onDismiss, autoHideMs, tone = "info" }: NoticeBannerProps) {
15
+ useEffect(() => {
16
+ if (autoHideMs === undefined || onDismiss === undefined) return;
17
+ const timer = window.setTimeout(onDismiss, autoHideMs);
18
+ return () => {
19
+ window.clearTimeout(timer);
20
+ };
21
+ }, [autoHideMs, onDismiss]);
22
+
23
+ const borderColor =
24
+ tone === "success" ? "var(--dsw-alias-state-success-primary, #22a06b)" : "var(--dsw-alias-brand-primary, #1677ff)";
25
+
26
+ return (
27
+ <div
28
+ style={{
29
+ ...noticeStyle,
30
+ display: "flex",
31
+ alignItems: "flex-start",
32
+ justifyContent: "space-between",
33
+ gap: 12,
34
+ borderLeft: `3px solid ${borderColor}`,
35
+ }}
36
+ role="status"
37
+ >
38
+ <p style={{ ...bodyStyle, margin: 0, color: "var(--dsw-alias-label-primary)" }}>{message}</p>
39
+ {onDismiss === undefined || dismissLabel === undefined ? null : (
40
+ <button type="button" style={buttonStyle} onClick={onDismiss}>
41
+ {dismissLabel}
42
+ </button>
43
+ )}
44
+ </div>
45
+ );
46
+ }
@@ -0,0 +1,52 @@
1
+ /** Usage quota progress bar with threshold-based color. */
2
+
3
+ import { hintStyle } from "../styles.ts";
4
+
5
+ export interface ProgressBarProps {
6
+ /** 0–100 used percentage. */
7
+ value: number;
8
+ label?: string;
9
+ meta?: string;
10
+ }
11
+
12
+ function barColor(percent: number): string {
13
+ if (percent >= 90) return "var(--dsw-alias-state-error-primary, #d92d20)";
14
+ if (percent >= 75) return "var(--dsw-alias-state-warning-primary, #e06c00)";
15
+ return "var(--dsw-alias-brand-primary, #1677ff)";
16
+ }
17
+
18
+ export function ProgressBar({ value, label, meta }: ProgressBarProps) {
19
+ const clamped = Math.max(0, Math.min(100, value));
20
+ return (
21
+ <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
22
+ {label === undefined ? null : (
23
+ <div style={{ display: "flex", justifyContent: "space-between", gap: 8, flexWrap: "wrap" }}>
24
+ <span style={{ ...hintStyle, color: "var(--dsw-alias-label-primary)" }}>{label}</span>
25
+ {meta === undefined ? null : <span style={hintStyle}>{meta}</span>}
26
+ </div>
27
+ )}
28
+ <div
29
+ role="progressbar"
30
+ aria-valuenow={clamped}
31
+ aria-valuemin={0}
32
+ aria-valuemax={100}
33
+ style={{
34
+ height: 8,
35
+ borderRadius: 4,
36
+ background: "var(--dsw-alias-border-l2)",
37
+ overflow: "hidden",
38
+ }}
39
+ >
40
+ <div
41
+ style={{
42
+ width: `${String(clamped)}%`,
43
+ height: "100%",
44
+ borderRadius: 4,
45
+ background: barColor(clamped),
46
+ transition: "width 0.3s ease, background 0.3s ease",
47
+ }}
48
+ />
49
+ </div>
50
+ </div>
51
+ );
52
+ }