dsh-coding-subscription-oauth 0.5.2 → 0.5.4

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 (44) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/INSTALL.md +11 -11
  3. package/README.de.md +34 -9
  4. package/README.es.md +35 -9
  5. package/README.fr.md +35 -9
  6. package/README.ja.md +35 -9
  7. package/README.ko.md +35 -9
  8. package/README.md +34 -13
  9. package/README.pt-BR.md +35 -9
  10. package/README.ru.md +35 -9
  11. package/README.zh-CN.md +34 -13
  12. package/docs/02-architecture.md +4 -3
  13. package/docs/02-architecture.zh-CN.md +8 -3
  14. package/lib/adapter.d.ts.map +1 -1
  15. package/lib/alias-adapter.d.ts.map +1 -1
  16. package/lib/client.js +1 -1
  17. package/lib/client.js.map +4 -4
  18. package/lib/grok-errors.d.ts +13 -0
  19. package/lib/grok-errors.d.ts.map +1 -0
  20. package/lib/index.js +13 -3
  21. package/lib/index.js.map +3 -3
  22. package/media/settings_accounts.png +0 -0
  23. package/media/settings_capabilities.png +0 -0
  24. package/media/settings_gateway.png +0 -0
  25. package/media/settings_overview.png +0 -0
  26. package/package.json +2 -1
  27. package/src/adapter.ts +7 -2
  28. package/src/alias-adapter.ts +2 -1
  29. package/src/client/GrokBuildSettings.tsx +247 -1772
  30. package/src/client/api.ts +88 -0
  31. package/src/client/components/AboutTab.tsx +20 -0
  32. package/src/client/components/AccountsTab.tsx +224 -0
  33. package/src/client/components/CapabilitiesTab.tsx +214 -0
  34. package/src/client/components/CliPullPreview.tsx +107 -0
  35. package/src/client/components/GatewayTab.tsx +287 -0
  36. package/src/client/components/ProviderCard.tsx +322 -0
  37. package/src/client/components/SettingsTabs.tsx +65 -0
  38. package/src/client/constants.ts +206 -0
  39. package/src/client/display.ts +61 -0
  40. package/src/client/locales.ts +47 -15
  41. package/src/client/parsers.ts +386 -0
  42. package/src/client/styles.ts +180 -0
  43. package/src/client/types.ts +185 -0
  44. package/src/grok-errors.ts +24 -0
@@ -0,0 +1,287 @@
1
+ /** Local API gateway settings tab. */
2
+
3
+ import { useState } from "react";
4
+ import { GATEWAY_PORT_MAX, GATEWAY_PORT_MIN } from "../constants.ts";
5
+ import { formatGatewayBaseUrl, parseGatewayPort, randomGatewayPort } from "../parsers.ts";
6
+ import {
7
+ bodyStyle,
8
+ buttonStyle,
9
+ cardStyle,
10
+ checkRowStyle,
11
+ copyRowStyle,
12
+ dotStyle,
13
+ errorStyle,
14
+ hintStyle,
15
+ inputStyle,
16
+ monoStyle,
17
+ nestedStyle,
18
+ primaryButtonStyle,
19
+ skeletonStyle,
20
+ statusStyle,
21
+ titleStyle,
22
+ warningStyle,
23
+ } from "../styles.ts";
24
+ import type { CopyField, GatewayView, GrokBuildSettingsInjected } from "../types.ts";
25
+
26
+ export interface GatewayTabProps {
27
+ t: GrokBuildSettingsInjected["t"];
28
+ gateway: GatewayView | undefined;
29
+ gatewayError: string | undefined;
30
+ gatewayBusy: boolean;
31
+ gatewayOnceKey: string | undefined;
32
+ gatewayKeyVisible: boolean;
33
+ gatewayRotateConfirm: boolean;
34
+ gatewayRevealError: string | undefined;
35
+ portDraft: string;
36
+ copiedField: CopyField | undefined;
37
+ copyFailedField: CopyField | undefined;
38
+ onEnabledChange: (enabled: boolean) => void;
39
+ onPortDraftChange: (value: string) => void;
40
+ onApplyPort: () => void;
41
+ onRandomPort: (port: number) => void;
42
+ onCopy: (field: CopyField, text: string) => void;
43
+ onCopyKey: () => void;
44
+ onToggleKeyVisible: () => void;
45
+ onRotateConfirm: () => void;
46
+ onRotateCancel: () => void;
47
+ onRotate: () => void;
48
+ }
49
+
50
+ export function GatewayTab({
51
+ t,
52
+ gateway,
53
+ gatewayError,
54
+ gatewayBusy,
55
+ gatewayOnceKey,
56
+ gatewayKeyVisible,
57
+ gatewayRotateConfirm,
58
+ gatewayRevealError,
59
+ portDraft,
60
+ copiedField,
61
+ copyFailedField,
62
+ onEnabledChange,
63
+ onPortDraftChange,
64
+ onApplyPort,
65
+ onRandomPort,
66
+ onCopy,
67
+ onCopyKey,
68
+ onToggleKeyVisible,
69
+ onRotateConfirm,
70
+ onRotateCancel,
71
+ onRotate,
72
+ }: GatewayTabProps) {
73
+ const [enableConfirm, setEnableConfirm] = useState(false);
74
+
75
+ const copyLabel = (field: CopyField, idle?: string): string => {
76
+ if (copyFailedField === field) return t("copyFailed");
77
+ if (copiedField === field) return t("copied");
78
+ return idle ?? t("copy");
79
+ };
80
+
81
+ return (
82
+ <section style={cardStyle} aria-labelledby="coding-oauth-gateway-title">
83
+ <div>
84
+ <h3 id="coding-oauth-gateway-title" style={{ ...titleStyle, fontSize: 16 }}>
85
+ {t("gatewayTitle")}
86
+ </h3>
87
+ <p style={{ ...bodyStyle, marginTop: 4 }}>{t("gatewayIntro")}</p>
88
+ <p style={{ ...warningStyle, marginTop: 8 }}>{t("gatewayWarning")}</p>
89
+ <p style={{ ...hintStyle, marginTop: 8 }}>{t("gatewayLoopbackHint")}</p>
90
+ </div>
91
+ {gatewayError === undefined ? null : (
92
+ <p style={errorStyle} role="alert">
93
+ {gatewayError}
94
+ </p>
95
+ )}
96
+ {gatewayRevealError === undefined ? null : (
97
+ <p style={errorStyle} role="alert">
98
+ {gatewayRevealError}
99
+ </p>
100
+ )}
101
+ {gateway === undefined && gatewayError === undefined ? (
102
+ <div style={skeletonStyle} role="status" aria-busy="true">
103
+ <div style={statusStyle}>
104
+ <span aria-hidden="true" style={dotStyle("loading")} />
105
+ {t("gatewayLoading")}
106
+ </div>
107
+ </div>
108
+ ) : gateway === undefined ? null : (
109
+ <div style={nestedStyle}>
110
+ <p style={statusStyle} role="status">
111
+ <span aria-hidden="true" style={dotStyle(gateway.running ? "available" : "unavailable")} />
112
+ <span>{gateway.running ? t("gatewayRunning") : t("gatewayStopped")}</span>
113
+ </p>
114
+ {enableConfirm ? (
115
+ <div style={nestedStyle}>
116
+ <p style={bodyStyle}>{t("gatewayEnableConfirm")}</p>
117
+ <div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
118
+ <button
119
+ type="button"
120
+ style={primaryButtonStyle}
121
+ disabled={gatewayBusy}
122
+ onClick={() => {
123
+ setEnableConfirm(false);
124
+ onEnabledChange(true);
125
+ }}
126
+ >
127
+ {t("gatewayEnableConfirmAction")}
128
+ </button>
129
+ <button
130
+ type="button"
131
+ style={buttonStyle}
132
+ disabled={gatewayBusy}
133
+ onClick={() => {
134
+ setEnableConfirm(false);
135
+ }}
136
+ >
137
+ {t("gatewayEnableCancel")}
138
+ </button>
139
+ </div>
140
+ </div>
141
+ ) : (
142
+ <label style={checkRowStyle}>
143
+ <input
144
+ type="checkbox"
145
+ checked={gateway.enabled}
146
+ disabled={gatewayBusy}
147
+ onChange={(event) => {
148
+ const enabled = event.target.checked;
149
+ if (enabled) {
150
+ setEnableConfirm(true);
151
+ return;
152
+ }
153
+ onEnabledChange(false);
154
+ }}
155
+ />
156
+ <span>{t("gatewayEnabled")}</span>
157
+ </label>
158
+ )}
159
+ <div>
160
+ <label
161
+ htmlFor="coding-oauth-gateway-port"
162
+ style={{ ...bodyStyle, color: "var(--dsw-alias-label-primary)" }}
163
+ >
164
+ {t("gatewayPort")}
165
+ </label>
166
+ <p id="coding-oauth-gateway-port-hint" style={hintStyle}>
167
+ {t("gatewayPortHint")}
168
+ </p>
169
+ <div style={{ display: "flex", flexWrap: "wrap", gap: 8, marginTop: 8 }}>
170
+ <input
171
+ id="coding-oauth-gateway-port"
172
+ type="number"
173
+ inputMode="numeric"
174
+ min={GATEWAY_PORT_MIN}
175
+ max={GATEWAY_PORT_MAX}
176
+ step={1}
177
+ value={portDraft}
178
+ disabled={gatewayBusy}
179
+ aria-describedby="coding-oauth-gateway-port-hint"
180
+ style={{ ...inputStyle, width: 112, flex: "0 0 112px" }}
181
+ onChange={(event) => {
182
+ onPortDraftChange(event.target.value);
183
+ }}
184
+ onKeyDown={(event) => {
185
+ if (event.key === "Enter") {
186
+ event.preventDefault();
187
+ onApplyPort();
188
+ }
189
+ if (event.key === "Escape") {
190
+ onPortDraftChange(String(gateway.port));
191
+ }
192
+ }}
193
+ />
194
+ <button
195
+ type="button"
196
+ style={primaryButtonStyle}
197
+ disabled={
198
+ gatewayBusy || portDraft === String(gateway.port) || parseGatewayPort(portDraft) === undefined
199
+ }
200
+ onClick={onApplyPort}
201
+ >
202
+ {t("gatewayPortApply")}
203
+ </button>
204
+ <button
205
+ type="button"
206
+ style={buttonStyle}
207
+ disabled={gatewayBusy}
208
+ onClick={() => {
209
+ onRandomPort(randomGatewayPort(gateway.port));
210
+ }}
211
+ >
212
+ {t("gatewayPortRandom")}
213
+ </button>
214
+ </div>
215
+ </div>
216
+ <p style={copyRowStyle}>
217
+ <span style={hintStyle}>
218
+ {t("gatewayOpenAiUrl")}
219
+ <span style={{ display: "block", ...monoStyle }}>
220
+ {`${formatGatewayBaseUrl(gateway.bind, gateway.port)}/v1`}
221
+ </span>
222
+ </span>
223
+ <button
224
+ type="button"
225
+ style={primaryButtonStyle}
226
+ onClick={() => {
227
+ onCopy("openai", `${formatGatewayBaseUrl(gateway.bind, gateway.port)}/v1`);
228
+ }}
229
+ >
230
+ {copyLabel("openai")}
231
+ </button>
232
+ </p>
233
+ <p style={copyRowStyle}>
234
+ <span style={hintStyle}>
235
+ {t("gatewayAnthropicUrl")}
236
+ <span style={{ display: "block", ...monoStyle }}>{formatGatewayBaseUrl(gateway.bind, gateway.port)}</span>
237
+ </span>
238
+ <button
239
+ type="button"
240
+ style={buttonStyle}
241
+ onClick={() => {
242
+ onCopy("anthropic", formatGatewayBaseUrl(gateway.bind, gateway.port));
243
+ }}
244
+ >
245
+ {copyLabel("anthropic")}
246
+ </button>
247
+ </p>
248
+ <p style={copyRowStyle}>
249
+ <span style={hintStyle}>
250
+ {t("gatewayKeyHint")}
251
+ <span style={{ display: "block", ...monoStyle, overflowWrap: "anywhere" }}>
252
+ {gatewayKeyVisible && gatewayOnceKey !== undefined ? gatewayOnceKey : gateway.keyHint || "—"}
253
+ </span>
254
+ </span>
255
+ <span style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
256
+ <button type="button" style={primaryButtonStyle} disabled={gatewayBusy} onClick={onCopyKey}>
257
+ {copyLabel("key", t("gatewayCopyKey"))}
258
+ </button>
259
+ <button type="button" style={buttonStyle} disabled={gatewayBusy} onClick={onToggleKeyVisible}>
260
+ {gatewayKeyVisible ? t("gatewayHideKey") : t("gatewayShowKey")}
261
+ </button>
262
+ </span>
263
+ </p>
264
+ <p style={hintStyle}>{t("gatewayKeyCopyHint")}</p>
265
+ {gatewayRotateConfirm ? (
266
+ <div style={nestedStyle}>
267
+ <p style={bodyStyle}>{t("gatewayRotateConfirm")}</p>
268
+ <p style={hintStyle}>{t("gatewayRotateConfirmHint")}</p>
269
+ <div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
270
+ <button type="button" style={buttonStyle} disabled={gatewayBusy} onClick={onRotate}>
271
+ {t("gatewayRotateConfirmAction")}
272
+ </button>
273
+ <button type="button" style={buttonStyle} disabled={gatewayBusy} onClick={onRotateCancel}>
274
+ {t("gatewayRotateCancel")}
275
+ </button>
276
+ </div>
277
+ </div>
278
+ ) : (
279
+ <button type="button" style={buttonStyle} disabled={gatewayBusy} onClick={onRotateConfirm}>
280
+ {t("gatewayRotate")}
281
+ </button>
282
+ )}
283
+ </div>
284
+ )}
285
+ </section>
286
+ );
287
+ }
@@ -0,0 +1,322 @@
1
+ /** Single provider account card for the Accounts tab. */
2
+
3
+ import { SOURCE_REASON_KEY } from "../constants.ts";
4
+ import { methodLabel, orderedLoginMethods, shouldShowPerCardSourceReason } from "../display.ts";
5
+ import { formatEpoch, modelFields, usageHasVisibleFields } from "../parsers.ts";
6
+ import {
7
+ bodyStyle,
8
+ buttonStyle,
9
+ cardStyle,
10
+ checkRowStyle,
11
+ codeStyle,
12
+ dotStyle,
13
+ errorStyle,
14
+ hintStyle,
15
+ inputStyle,
16
+ linkStyle,
17
+ listStyle,
18
+ monoStyle,
19
+ nestedStyle,
20
+ primaryButtonStyle,
21
+ rowStyle,
22
+ statusStyle,
23
+ titleStyle,
24
+ } from "../styles.ts";
25
+ import type {
26
+ GrokBuildSettingsInjected,
27
+ GrokStatus,
28
+ LoginMethod,
29
+ ProviderCardDefinition,
30
+ ProviderStatus,
31
+ SourceStatus,
32
+ UsageView,
33
+ } from "../types.ts";
34
+
35
+ export interface ProviderCardProps {
36
+ t: GrokBuildSettingsInjected["t"];
37
+ definition: ProviderCardDefinition;
38
+ providerStatus: ProviderStatus;
39
+ busy: boolean;
40
+ sourcesBusy: boolean;
41
+ remote: boolean;
42
+ codeInput: string;
43
+ popupBlocked: boolean;
44
+ expanded: boolean;
45
+ source: SourceStatus | undefined;
46
+ showUsage: boolean;
47
+ usage: UsageView | undefined;
48
+ usageError: string | undefined;
49
+ usageLoading: boolean;
50
+ onSignIn: (method: LoginMethod) => void;
51
+ onSignOut: () => void;
52
+ onCancelLogin: () => void;
53
+ onSubmitCode: () => void;
54
+ onCodeChange: (value: string) => void;
55
+ onToggleExpanded: () => void;
56
+ onPreviewSource: () => void;
57
+ onSaveModels: (selected: string[]) => void;
58
+ }
59
+
60
+ export function ProviderCard({
61
+ t,
62
+ definition,
63
+ providerStatus,
64
+ busy,
65
+ sourcesBusy,
66
+ remote,
67
+ codeInput,
68
+ popupBlocked,
69
+ expanded,
70
+ source,
71
+ showUsage,
72
+ usage,
73
+ usageError,
74
+ usageLoading,
75
+ onSignIn,
76
+ onSignOut,
77
+ onCancelLogin,
78
+ onSubmitCode,
79
+ onCodeChange,
80
+ onToggleExpanded,
81
+ onPreviewSource,
82
+ onSaveModels,
83
+ }: ProviderCardProps) {
84
+ const ordered = orderedLoginMethods(definition, remote);
85
+ const statusLabel =
86
+ providerStatus.status === "signed-in"
87
+ ? t("signedIn")
88
+ : providerStatus.status === "signing-in"
89
+ ? t("signingIn")
90
+ : providerStatus.status === "error"
91
+ ? t("requestFailed")
92
+ : t("signedOut");
93
+ const activeMethod = providerStatus.status === "signing-in" ? providerStatus.method : ordered[0];
94
+ const { available, selected } = modelFields(providerStatus);
95
+ const grokProviderStatus = definition.slug === "grok" ? (providerStatus as GrokStatus) : undefined;
96
+ const showSourceReason = shouldShowPerCardSourceReason(source);
97
+ const usagePercent =
98
+ definition.slug === "codex" && showUsage
99
+ ? usage?.individualRemainingPercent === undefined
100
+ ? usage?.rateLimits[0]?.windows[0]?.usedPercent
101
+ : 100 - usage.individualRemainingPercent
102
+ : undefined;
103
+ const fetchedAt = formatEpoch(usage?.fetchedAt);
104
+
105
+ return (
106
+ <div style={cardStyle}>
107
+ <div style={rowStyle}>
108
+ <div>
109
+ <h3 style={{ ...titleStyle, fontSize: 16 }}>{t(definition.titleKey)}</h3>
110
+ {providerStatus.status === "signed-in" && !expanded ? (
111
+ <p style={{ ...hintStyle, marginTop: 4 }}>
112
+ {t("modelsSummary", { selected: selected.length, total: available.length })}
113
+ {usagePercent === undefined ? "" : ` · ${t("usageUsedShort", { value: `${String(usagePercent)}%` })}`}
114
+ </p>
115
+ ) : (
116
+ <>
117
+ <p style={{ ...bodyStyle, marginTop: 4 }}>{t(definition.descriptionKey)}</p>
118
+ <p style={{ ...bodyStyle, marginTop: 4 }}>
119
+ <span style={monoStyle}>{definition.route}</span>
120
+ </p>
121
+ </>
122
+ )}
123
+ </div>
124
+ <div style={statusStyle} role="status">
125
+ <span aria-hidden="true" style={dotStyle(providerStatus.status)} />
126
+ <span>{statusLabel}</span>
127
+ </div>
128
+ </div>
129
+ <div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
130
+ {providerStatus.status === "signed-in" ? (
131
+ <>
132
+ <button type="button" style={buttonStyle} disabled={busy} onClick={onSignOut}>
133
+ {busy ? t("working") : t("logout")}
134
+ </button>
135
+ <button type="button" style={buttonStyle} onClick={onToggleExpanded}>
136
+ {expanded ? t("collapseModels") : t("expandModels")}
137
+ </button>
138
+ {source?.available === true ? (
139
+ <button type="button" style={buttonStyle} disabled={sourcesBusy} onClick={onPreviewSource}>
140
+ {t("sourcesPullCopy")}
141
+ </button>
142
+ ) : null}
143
+ </>
144
+ ) : providerStatus.status === "signing-in" ? (
145
+ <>
146
+ {ordered
147
+ .filter((method) => method !== activeMethod)
148
+ .map((method) => (
149
+ <button
150
+ key={method}
151
+ type="button"
152
+ style={buttonStyle}
153
+ disabled={busy}
154
+ onClick={() => {
155
+ onSignIn(method);
156
+ }}
157
+ >
158
+ {methodLabel(method, t, { remote, primary: method === ordered[0] })}
159
+ </button>
160
+ ))}
161
+ <button type="button" style={buttonStyle} disabled={busy} onClick={onCancelLogin}>
162
+ {t("cancelLogin")}
163
+ </button>
164
+ </>
165
+ ) : (
166
+ <>
167
+ {ordered.map((method, index) => (
168
+ <button
169
+ key={method}
170
+ type="button"
171
+ style={index === 0 ? primaryButtonStyle : buttonStyle}
172
+ disabled={busy}
173
+ onClick={() => {
174
+ onSignIn(method);
175
+ }}
176
+ >
177
+ {busy ? t("working") : methodLabel(method, t, { remote, primary: index === 0 })}
178
+ </button>
179
+ ))}
180
+ {source?.available === true ? (
181
+ <button type="button" style={buttonStyle} disabled={sourcesBusy} onClick={onPreviewSource}>
182
+ {t("sourcesPullCopy")}
183
+ </button>
184
+ ) : showSourceReason && source?.reason !== undefined ? (
185
+ <span style={hintStyle}>{t(SOURCE_REASON_KEY[source.reason])}</span>
186
+ ) : null}
187
+ </>
188
+ )}
189
+ </div>
190
+ {providerStatus.status === "error" ? <p style={errorStyle}>{providerStatus.message}</p> : null}
191
+ {providerStatus.status === "signing-in" && providerStatus.userCode !== undefined ? (
192
+ <p style={bodyStyle}>
193
+ {t("userCode")} <span style={codeStyle}>{providerStatus.userCode}</span>
194
+ </p>
195
+ ) : null}
196
+ {providerStatus.status === "signing-in" && providerStatus.url !== undefined ? (
197
+ <p style={bodyStyle}>
198
+ {popupBlocked ? t("popupBlocked") : t("openUrl")}{" "}
199
+ <a href={providerStatus.url} target="_blank" rel="noreferrer" style={linkStyle}>
200
+ {providerStatus.url}
201
+ </a>
202
+ </p>
203
+ ) : null}
204
+ {providerStatus.status === "signing-in" && activeMethod !== "device" ? (
205
+ <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
206
+ <p style={bodyStyle}>{t(activeMethod === "browser" ? "pasteBrowserCodeHint" : "pasteCodeHint")}</p>
207
+ <div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
208
+ <input
209
+ style={{ ...inputStyle, flex: "1 1 360px" }}
210
+ value={codeInput}
211
+ placeholder={t("pasteCodePlaceholder")}
212
+ disabled={busy}
213
+ onChange={(event) => {
214
+ onCodeChange(event.target.value);
215
+ }}
216
+ onKeyDown={(event) => {
217
+ if (event.key === "Enter") {
218
+ event.preventDefault();
219
+ onSubmitCode();
220
+ }
221
+ }}
222
+ />
223
+ <button
224
+ type="button"
225
+ style={primaryButtonStyle}
226
+ disabled={busy || codeInput.trim().length === 0}
227
+ onClick={onSubmitCode}
228
+ >
229
+ {t("submitCode")}
230
+ </button>
231
+ </div>
232
+ </div>
233
+ ) : null}
234
+ {providerStatus.status === "signed-in" && expanded ? (
235
+ <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
236
+ <div style={rowStyle}>
237
+ <h4 style={{ ...titleStyle, fontSize: 14 }}>{t("models")}</h4>
238
+ <button
239
+ type="button"
240
+ style={buttonStyle}
241
+ disabled={busy}
242
+ onClick={() => {
243
+ onSaveModels([]);
244
+ }}
245
+ >
246
+ {t("selectAll")}
247
+ </button>
248
+ </div>
249
+ {grokProviderStatus?.status === "signed-in" ? (
250
+ <p style={bodyStyle}>
251
+ {grokProviderStatus.catalogSource === "live"
252
+ ? t("catalogLive")
253
+ : grokProviderStatus.catalogSource === "cache"
254
+ ? t("catalogCache")
255
+ : t("catalogFallback")}
256
+ </p>
257
+ ) : null}
258
+ <p style={bodyStyle}>
259
+ {t("modelHint")} <span style={monoStyle}>{definition.route}/&lt;id&gt;</span>
260
+ </p>
261
+ <ul style={listStyle}>
262
+ {available.map((id) => {
263
+ const checked = selected.includes(id);
264
+ return (
265
+ <li key={id}>
266
+ <label style={checkRowStyle}>
267
+ <input
268
+ type="checkbox"
269
+ checked={checked}
270
+ disabled={busy}
271
+ onChange={() => {
272
+ const current = new Set(selected);
273
+ if (checked) current.delete(id);
274
+ else current.add(id);
275
+ onSaveModels([...current]);
276
+ }}
277
+ />
278
+ <span style={monoStyle}>{id}</span>
279
+ </label>
280
+ </li>
281
+ );
282
+ })}
283
+ </ul>
284
+ {grokProviderStatus?.status === "signed-in" && grokProviderStatus.catalogError !== undefined ? (
285
+ <p style={errorStyle}>{t("catalogError")}</p>
286
+ ) : null}
287
+ {definition.slug === "codex" && showUsage ? (
288
+ <div style={nestedStyle}>
289
+ <p style={{ ...bodyStyle, color: "var(--dsw-alias-label-primary)" }}>{t("usageTitle")}</p>
290
+ {usageError === undefined ? null : (
291
+ <p style={errorStyle} role="alert">
292
+ {usageError}
293
+ </p>
294
+ )}
295
+ {usageLoading && usage === undefined ? (
296
+ <p style={hintStyle}>{t("usageLoading")}</p>
297
+ ) : usage === undefined || !usageHasVisibleFields(usage) ? (
298
+ <p style={hintStyle}>{t("usageEmpty")}</p>
299
+ ) : (
300
+ <>
301
+ {fetchedAt === undefined ? null : <p style={hintStyle}>{t("usageFetchedAt", { time: fetchedAt })}</p>}
302
+ {usage.rateLimits.map((limit) => {
303
+ const resetsAt = formatEpoch(limit.windows[0]?.resetsAt);
304
+ return (
305
+ <p key={limit.id} style={hintStyle}>
306
+ {limit.name ?? t("usageRateLimit")}
307
+ {limit.windows[0]?.usedPercent === undefined
308
+ ? ""
309
+ : ` · ${t("usageUsed", { value: `${String(limit.windows[0].usedPercent)}%` })}`}
310
+ {resetsAt === undefined ? "" : ` · ${t("usageResets", { time: resetsAt })}`}
311
+ </p>
312
+ );
313
+ })}
314
+ </>
315
+ )}
316
+ </div>
317
+ ) : null}
318
+ </div>
319
+ ) : null}
320
+ </div>
321
+ );
322
+ }
@@ -0,0 +1,65 @@
1
+ /** Accessible tablist for Coding OAuth settings. */
2
+
3
+ import type { KeyboardEvent } from "react";
4
+ import { SETTINGS_TABS } from "../constants.ts";
5
+ import { tabButtonActiveStyle, tabButtonStyle, tabNavStyle } from "../styles.ts";
6
+ import type { GrokBuildSettingsInjected, SettingsTabId } from "../types.ts";
7
+
8
+ export interface SettingsTabsProps {
9
+ t: GrokBuildSettingsInjected["t"];
10
+ activeTab: SettingsTabId;
11
+ onChange: (tab: SettingsTabId) => void;
12
+ }
13
+
14
+ export function SettingsTabs({ t, activeTab, onChange }: SettingsTabsProps) {
15
+ const focusTab = (index: number): void => {
16
+ const tab = SETTINGS_TABS[index];
17
+ if (tab === undefined) return;
18
+ onChange(tab.id);
19
+ const button = document.getElementById(`coding-oauth-tab-${tab.id}`);
20
+ button?.focus();
21
+ };
22
+
23
+ const onKeyDown = (event: KeyboardEvent<HTMLDivElement>): void => {
24
+ const current = SETTINGS_TABS.findIndex((tab) => tab.id === activeTab);
25
+ if (current < 0) return;
26
+ if (event.key === "ArrowRight" || event.key === "ArrowDown") {
27
+ event.preventDefault();
28
+ focusTab((current + 1) % SETTINGS_TABS.length);
29
+ } else if (event.key === "ArrowLeft" || event.key === "ArrowUp") {
30
+ event.preventDefault();
31
+ focusTab((current - 1 + SETTINGS_TABS.length) % SETTINGS_TABS.length);
32
+ } else if (event.key === "Home") {
33
+ event.preventDefault();
34
+ focusTab(0);
35
+ } else if (event.key === "End") {
36
+ event.preventDefault();
37
+ focusTab(SETTINGS_TABS.length - 1);
38
+ }
39
+ };
40
+
41
+ return (
42
+ <div role="tablist" aria-label={t("title")} style={tabNavStyle} onKeyDown={onKeyDown}>
43
+ {SETTINGS_TABS.map((tab) => {
44
+ const selected = activeTab === tab.id;
45
+ return (
46
+ <button
47
+ key={tab.id}
48
+ id={`coding-oauth-tab-${tab.id}`}
49
+ type="button"
50
+ role="tab"
51
+ aria-selected={selected}
52
+ aria-controls={`coding-oauth-panel-${tab.id}`}
53
+ tabIndex={selected ? 0 : -1}
54
+ style={selected ? tabButtonActiveStyle : tabButtonStyle}
55
+ onClick={() => {
56
+ onChange(tab.id);
57
+ }}
58
+ >
59
+ {t(tab.label)}
60
+ </button>
61
+ );
62
+ })}
63
+ </div>
64
+ );
65
+ }