dsh-coding-subscription-oauth 0.5.4 → 0.5.6

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.
@@ -1,5 +1,6 @@
1
1
  /** Single provider account card for the Accounts tab. */
2
2
 
3
+ import { useMemo, useState } from "react";
3
4
  import { SOURCE_REASON_KEY } from "../constants.ts";
4
5
  import { methodLabel, orderedLoginMethods, shouldShowPerCardSourceReason } from "../display.ts";
5
6
  import { formatEpoch, modelFields, usageHasVisibleFields } from "../parsers.ts";
@@ -9,17 +10,18 @@ import {
9
10
  cardStyle,
10
11
  checkRowStyle,
11
12
  codeStyle,
12
- dotStyle,
13
- errorStyle,
13
+ compactButtonStyle,
14
14
  hintStyle,
15
15
  inputStyle,
16
- linkStyle,
17
16
  listStyle,
18
17
  monoStyle,
19
18
  nestedStyle,
20
19
  primaryButtonStyle,
21
20
  rowStyle,
22
- statusStyle,
21
+ stepActiveStyle,
22
+ stepNumberActiveStyle,
23
+ stepNumberStyle,
24
+ stepRowStyle,
23
25
  titleStyle,
24
26
  } from "../styles.ts";
25
27
  import type {
@@ -31,6 +33,9 @@ import type {
31
33
  SourceStatus,
32
34
  UsageView,
33
35
  } from "../types.ts";
36
+ import { Badge } from "./Badge.tsx";
37
+ import { CopyButton } from "./CopyButton.tsx";
38
+ import { ProgressBar } from "./ProgressBar.tsx";
34
39
 
35
40
  export interface ProviderCardProps {
36
41
  t: GrokBuildSettingsInjected["t"];
@@ -57,6 +62,88 @@ export interface ProviderCardProps {
57
62
  onSaveModels: (selected: string[]) => void;
58
63
  }
59
64
 
65
+ function SignInSteps({
66
+ t,
67
+ activeMethod,
68
+ userCode,
69
+ url,
70
+ popupBlocked,
71
+ }: {
72
+ t: GrokBuildSettingsInjected["t"];
73
+ activeMethod: LoginMethod;
74
+ userCode: string | undefined;
75
+ url: string | undefined;
76
+ popupBlocked: boolean;
77
+ }) {
78
+ const hasCode = userCode !== undefined && userCode.length > 0;
79
+ const hasUrl = url !== undefined && url.length > 0;
80
+ const needsPaste = activeMethod !== "device";
81
+
82
+ return (
83
+ <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
84
+ <div style={hasUrl ? stepActiveStyle : stepRowStyle}>
85
+ <span style={hasUrl ? stepNumberActiveStyle : stepNumberStyle} aria-hidden="true">
86
+ 1
87
+ </span>
88
+ <span>{t("signInStepOpen")}</span>
89
+ </div>
90
+ {hasUrl ? (
91
+ <div style={{ display: "flex", flexWrap: "wrap", gap: 8, paddingLeft: 32 }}>
92
+ <a href={url} target="_blank" rel="noreferrer" style={primaryButtonStyle}>
93
+ {t("openAuthUrl")}
94
+ </a>
95
+ <CopyButton text={url} idleLabel={t("copy")} copiedLabel={t("copied")} failedLabel={t("copyFailed")} />
96
+ </div>
97
+ ) : null}
98
+ {popupBlocked && hasUrl ? <p style={hintStyle}>{t("popupBlocked")}</p> : null}
99
+ {hasCode ? (
100
+ <>
101
+ <div style={stepActiveStyle}>
102
+ <span style={stepNumberActiveStyle} aria-hidden="true">
103
+ 2
104
+ </span>
105
+ <span>{t("signInStepCode")}</span>
106
+ </div>
107
+ <div style={{ display: "flex", flexWrap: "wrap", alignItems: "center", gap: 8, paddingLeft: 32 }}>
108
+ <span style={codeStyle}>{userCode}</span>
109
+ <CopyButton
110
+ text={userCode}
111
+ idleLabel={t("copyUserCode")}
112
+ copiedLabel={t("copied")}
113
+ failedLabel={t("copyFailed")}
114
+ primary
115
+ />
116
+ </div>
117
+ </>
118
+ ) : null}
119
+ <div style={stepActiveStyle}>
120
+ <span style={stepNumberActiveStyle} aria-hidden="true">
121
+ {hasCode ? 3 : 2}
122
+ </span>
123
+ <span style={{ display: "inline-flex", alignItems: "center", gap: 8 }}>
124
+ <span
125
+ aria-hidden="true"
126
+ style={{
127
+ width: 14,
128
+ height: 14,
129
+ border: "2px solid var(--dsw-alias-brand-primary, #1677ff)",
130
+ borderTopColor: "transparent",
131
+ borderRadius: "50%",
132
+ animation: "dsh-coding-oauth-spin 0.8s linear infinite",
133
+ }}
134
+ />
135
+ {t("signInStepWait")}
136
+ </span>
137
+ </div>
138
+ {needsPaste ? (
139
+ <div style={{ display: "flex", flexDirection: "column", gap: 8, paddingLeft: 32 }}>
140
+ <p style={bodyStyle}>{t(activeMethod === "browser" ? "pasteBrowserCodeHint" : "pasteCodeHint")}</p>
141
+ </div>
142
+ ) : null}
143
+ </div>
144
+ );
145
+ }
146
+
60
147
  export function ProviderCard({
61
148
  t,
62
149
  definition,
@@ -81,7 +168,13 @@ export function ProviderCard({
81
168
  onPreviewSource,
82
169
  onSaveModels,
83
170
  }: ProviderCardProps) {
171
+ const [showAltMethods, setShowAltMethods] = useState(false);
172
+ const [modelFilter, setModelFilter] = useState("");
173
+
84
174
  const ordered = orderedLoginMethods(definition, remote);
175
+ const primaryMethod: LoginMethod = ordered[0] ?? definition.recommended;
176
+ const altMethods = ordered.filter((method) => method !== primaryMethod);
177
+
85
178
  const statusLabel =
86
179
  providerStatus.status === "signed-in"
87
180
  ? t("signedIn")
@@ -90,10 +183,17 @@ export function ProviderCard({
90
183
  : providerStatus.status === "error"
91
184
  ? t("requestFailed")
92
185
  : t("signedOut");
93
- const activeMethod = providerStatus.status === "signing-in" ? providerStatus.method : ordered[0];
186
+ const activeMethod = providerStatus.status === "signing-in" ? providerStatus.method : primaryMethod;
94
187
  const { available, selected } = modelFields(providerStatus);
95
188
  const grokProviderStatus = definition.slug === "grok" ? (providerStatus as GrokStatus) : undefined;
96
189
  const showSourceReason = shouldShowPerCardSourceReason(source);
190
+
191
+ const filteredModels = useMemo(() => {
192
+ const query = modelFilter.trim().toLowerCase();
193
+ if (query.length === 0) return available;
194
+ return available.filter((id) => id.toLowerCase().includes(query));
195
+ }, [available, modelFilter]);
196
+
97
197
  const usagePercent =
98
198
  definition.slug === "codex" && showUsage
99
199
  ? usage?.individualRemainingPercent === undefined
@@ -121,10 +221,7 @@ export function ProviderCard({
121
221
  </>
122
222
  )}
123
223
  </div>
124
- <div style={statusStyle} role="status">
125
- <span aria-hidden="true" style={dotStyle(providerStatus.status)} />
126
- <span>{statusLabel}</span>
127
- </div>
224
+ <Badge label={statusLabel} providerStatus={providerStatus.status} />
128
225
  </div>
129
226
  <div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
130
227
  {providerStatus.status === "signed-in" ? (
@@ -143,40 +240,76 @@ export function ProviderCard({
143
240
  </>
144
241
  ) : providerStatus.status === "signing-in" ? (
145
242
  <>
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
243
  <button type="button" style={buttonStyle} disabled={busy} onClick={onCancelLogin}>
162
244
  {t("cancelLogin")}
163
245
  </button>
246
+ {altMethods.length > 0 ? (
247
+ <button
248
+ type="button"
249
+ style={compactButtonStyle}
250
+ disabled={busy}
251
+ onClick={() => {
252
+ setShowAltMethods((current) => !current);
253
+ }}
254
+ >
255
+ {showAltMethods ? t("hideOtherLoginMethods") : t("otherLoginMethods")}
256
+ </button>
257
+ ) : null}
258
+ {showAltMethods
259
+ ? altMethods.map((method) => (
260
+ <button
261
+ key={method}
262
+ type="button"
263
+ style={compactButtonStyle}
264
+ disabled={busy}
265
+ onClick={() => {
266
+ onSignIn(method);
267
+ }}
268
+ >
269
+ {methodLabel(method, t, { remote, primary: false })}
270
+ </button>
271
+ ))
272
+ : null}
164
273
  </>
165
274
  ) : (
166
275
  <>
167
- {ordered.map((method, index) => (
276
+ <button
277
+ type="button"
278
+ style={primaryButtonStyle}
279
+ disabled={busy}
280
+ onClick={() => {
281
+ onSignIn(primaryMethod);
282
+ }}
283
+ >
284
+ {busy ? t("working") : methodLabel(primaryMethod, t, { remote, primary: true })}
285
+ </button>
286
+ {altMethods.length > 0 ? (
168
287
  <button
169
- key={method}
170
288
  type="button"
171
- style={index === 0 ? primaryButtonStyle : buttonStyle}
289
+ style={compactButtonStyle}
172
290
  disabled={busy}
173
291
  onClick={() => {
174
- onSignIn(method);
292
+ setShowAltMethods((current) => !current);
175
293
  }}
176
294
  >
177
- {busy ? t("working") : methodLabel(method, t, { remote, primary: index === 0 })}
295
+ {showAltMethods ? t("hideOtherLoginMethods") : t("otherLoginMethods")}
178
296
  </button>
179
- ))}
297
+ ) : null}
298
+ {showAltMethods
299
+ ? altMethods.map((method) => (
300
+ <button
301
+ key={method}
302
+ type="button"
303
+ style={compactButtonStyle}
304
+ disabled={busy}
305
+ onClick={() => {
306
+ onSignIn(method);
307
+ }}
308
+ >
309
+ {methodLabel(method, t, { remote, primary: false })}
310
+ </button>
311
+ ))
312
+ : null}
180
313
  {source?.available === true ? (
181
314
  <button type="button" style={buttonStyle} disabled={sourcesBusy} onClick={onPreviewSource}>
182
315
  {t("sourcesPullCopy")}
@@ -187,23 +320,20 @@ export function ProviderCard({
187
320
  </>
188
321
  )}
189
322
  </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>
323
+ {providerStatus.status === "error" ? (
324
+ <p style={{ ...bodyStyle, color: "var(--dsw-alias-state-error-primary)" }}>{providerStatus.message}</p>
195
325
  ) : 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>
326
+ {providerStatus.status === "signing-in" ? (
327
+ <SignInSteps
328
+ t={t}
329
+ activeMethod={activeMethod}
330
+ userCode={providerStatus.userCode}
331
+ url={providerStatus.url}
332
+ popupBlocked={popupBlocked}
333
+ />
203
334
  ) : null}
204
335
  {providerStatus.status === "signing-in" && activeMethod !== "device" ? (
205
336
  <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
206
- <p style={bodyStyle}>{t(activeMethod === "browser" ? "pasteBrowserCodeHint" : "pasteCodeHint")}</p>
207
337
  <div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
208
338
  <input
209
339
  style={{ ...inputStyle, flex: "1 1 360px" }}
@@ -235,17 +365,49 @@ export function ProviderCard({
235
365
  <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
236
366
  <div style={rowStyle}>
237
367
  <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>
368
+ <div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
369
+ <button
370
+ type="button"
371
+ style={compactButtonStyle}
372
+ disabled={busy}
373
+ onClick={() => {
374
+ onSaveModels([]);
375
+ }}
376
+ >
377
+ {t("deselectAll")}
378
+ </button>
379
+ <button
380
+ type="button"
381
+ style={compactButtonStyle}
382
+ disabled={busy}
383
+ onClick={() => {
384
+ onSaveModels([...available]);
385
+ }}
386
+ >
387
+ {t("selectAll")}
388
+ </button>
389
+ <button
390
+ type="button"
391
+ style={compactButtonStyle}
392
+ disabled={busy}
393
+ onClick={() => {
394
+ onSaveModels(available.length > 0 ? [available[0]!] : []);
395
+ }}
396
+ >
397
+ {t("resetModelsDefault")}
398
+ </button>
399
+ </div>
248
400
  </div>
401
+ <input
402
+ type="search"
403
+ style={inputStyle}
404
+ value={modelFilter}
405
+ placeholder={t("modelFilterPlaceholder")}
406
+ disabled={busy}
407
+ onChange={(event) => {
408
+ setModelFilter(event.target.value);
409
+ }}
410
+ />
249
411
  {grokProviderStatus?.status === "signed-in" ? (
250
412
  <p style={bodyStyle}>
251
413
  {grokProviderStatus.catalogSource === "live"
@@ -259,7 +421,7 @@ export function ProviderCard({
259
421
  {t("modelHint")} <span style={monoStyle}>{definition.route}/&lt;id&gt;</span>
260
422
  </p>
261
423
  <ul style={listStyle}>
262
- {available.map((id) => {
424
+ {filteredModels.map((id) => {
263
425
  const checked = selected.includes(id);
264
426
  return (
265
427
  <li key={id}>
@@ -281,14 +443,15 @@ export function ProviderCard({
281
443
  );
282
444
  })}
283
445
  </ul>
446
+ {filteredModels.length === 0 ? <p style={hintStyle}>{t("modelFilterPlaceholder")}</p> : null}
284
447
  {grokProviderStatus?.status === "signed-in" && grokProviderStatus.catalogError !== undefined ? (
285
- <p style={errorStyle}>{t("catalogError")}</p>
448
+ <p style={{ ...bodyStyle, color: "var(--dsw-alias-state-error-primary)" }}>{t("catalogError")}</p>
286
449
  ) : null}
287
450
  {definition.slug === "codex" && showUsage ? (
288
451
  <div style={nestedStyle}>
289
452
  <p style={{ ...bodyStyle, color: "var(--dsw-alias-label-primary)" }}>{t("usageTitle")}</p>
290
453
  {usageError === undefined ? null : (
291
- <p style={errorStyle} role="alert">
454
+ <p style={{ ...bodyStyle, color: "var(--dsw-alias-state-error-primary)" }} role="alert">
292
455
  {usageError}
293
456
  </p>
294
457
  )}
@@ -299,16 +462,33 @@ export function ProviderCard({
299
462
  ) : (
300
463
  <>
301
464
  {fetchedAt === undefined ? null : <p style={hintStyle}>{t("usageFetchedAt", { time: fetchedAt })}</p>}
465
+ {usagePercent !== undefined ? (
466
+ <ProgressBar
467
+ value={usagePercent}
468
+ label={t("usageRateLimit")}
469
+ meta={t("usageUsed", { value: `${String(usagePercent)}%` })}
470
+ />
471
+ ) : null}
302
472
  {usage.rateLimits.map((limit) => {
303
- const resetsAt = formatEpoch(limit.windows[0]?.resetsAt);
473
+ const window = limit.windows[0];
474
+ const used = window?.usedPercent;
475
+ const resetsAt = formatEpoch(window?.resetsAt);
304
476
  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>
477
+ <div key={limit.id} style={{ display: "flex", flexDirection: "column", gap: 4 }}>
478
+ {used === undefined ? (
479
+ <p style={hintStyle}>{limit.name ?? t("usageRateLimit")}</p>
480
+ ) : (
481
+ <ProgressBar
482
+ value={used}
483
+ label={limit.name ?? t("usageRateLimit")}
484
+ meta={
485
+ resetsAt === undefined
486
+ ? t("usageUsed", { value: `${String(used)}%` })
487
+ : `${t("usageUsed", { value: `${String(used)}%` })} · ${t("usageResets", { time: resetsAt })}`
488
+ }
489
+ />
490
+ )}
491
+ </div>
312
492
  );
313
493
  })}
314
494
  </>
@@ -1,17 +1,25 @@
1
- /** Accessible tablist for Coding OAuth settings. */
1
+ /** Accessible segmented tablist for Coding OAuth settings. */
2
2
 
3
3
  import type { KeyboardEvent } from "react";
4
4
  import { SETTINGS_TABS } from "../constants.ts";
5
- import { tabButtonActiveStyle, tabButtonStyle, tabNavStyle } from "../styles.ts";
5
+ import { segmentedNavStyle, segmentedTabActiveStyle, segmentedTabStyle } from "../styles.ts";
6
6
  import type { GrokBuildSettingsInjected, SettingsTabId } from "../types.ts";
7
7
 
8
+ export interface SettingsTabHint {
9
+ id: SettingsTabId;
10
+ suffix?: string;
11
+ }
12
+
8
13
  export interface SettingsTabsProps {
9
14
  t: GrokBuildSettingsInjected["t"];
10
15
  activeTab: SettingsTabId;
11
16
  onChange: (tab: SettingsTabId) => void;
17
+ hints?: readonly SettingsTabHint[];
12
18
  }
13
19
 
14
- export function SettingsTabs({ t, activeTab, onChange }: SettingsTabsProps) {
20
+ export function SettingsTabs({ t, activeTab, onChange, hints }: SettingsTabsProps) {
21
+ const hintFor = (id: SettingsTabId): string | undefined => hints?.find((entry) => entry.id === id)?.suffix;
22
+
15
23
  const focusTab = (index: number): void => {
16
24
  const tab = SETTINGS_TABS[index];
17
25
  if (tab === undefined) return;
@@ -39,9 +47,11 @@ export function SettingsTabs({ t, activeTab, onChange }: SettingsTabsProps) {
39
47
  };
40
48
 
41
49
  return (
42
- <div role="tablist" aria-label={t("title")} style={tabNavStyle} onKeyDown={onKeyDown}>
50
+ <div role="tablist" aria-label={t("title")} style={segmentedNavStyle} onKeyDown={onKeyDown}>
43
51
  {SETTINGS_TABS.map((tab) => {
44
52
  const selected = activeTab === tab.id;
53
+ const suffix = hintFor(tab.id);
54
+ const label = suffix === undefined ? t(tab.label) : `${t(tab.label)} (${suffix})`;
45
55
  return (
46
56
  <button
47
57
  key={tab.id}
@@ -51,12 +61,12 @@ export function SettingsTabs({ t, activeTab, onChange }: SettingsTabsProps) {
51
61
  aria-selected={selected}
52
62
  aria-controls={`coding-oauth-panel-${tab.id}`}
53
63
  tabIndex={selected ? 0 : -1}
54
- style={selected ? tabButtonActiveStyle : tabButtonStyle}
64
+ style={selected ? segmentedTabActiveStyle : segmentedTabStyle}
55
65
  onClick={() => {
56
66
  onChange(tab.id);
57
67
  }}
58
68
  >
59
- {t(tab.label)}
69
+ {label}
60
70
  </button>
61
71
  );
62
72
  })}
@@ -0,0 +1,60 @@
1
+ /** Accessible toggle switch replacing native checkboxes for capability settings. */
2
+
3
+ import type { CSSProperties } from "react";
4
+ import { TRANSITION } from "../styles.ts";
5
+
6
+ export interface ToggleSwitchProps {
7
+ checked: boolean;
8
+ disabled?: boolean;
9
+ onChange: (checked: boolean) => void;
10
+ /** Accessible label (visually hidden when label prop is rendered externally). */
11
+ ariaLabel?: string;
12
+ id?: string;
13
+ }
14
+
15
+ const trackStyle = (checked: boolean, disabled: boolean): CSSProperties => ({
16
+ position: "relative",
17
+ width: 40,
18
+ height: 22,
19
+ borderRadius: 11,
20
+ flex: "0 0 auto",
21
+ background: checked
22
+ ? "var(--dsw-alias-brand-primary, #315fc7)"
23
+ : "var(--dsw-alias-border-l4, rgba(127, 127, 127, 0.45))",
24
+ opacity: disabled ? 0.5 : 1,
25
+ cursor: disabled ? "not-allowed" : "pointer",
26
+ transition: TRANSITION,
27
+ border: "none",
28
+ padding: 0,
29
+ });
30
+
31
+ const thumbStyle = (checked: boolean): CSSProperties => ({
32
+ position: "absolute",
33
+ top: 2,
34
+ left: checked ? 20 : 2,
35
+ width: 18,
36
+ height: 18,
37
+ borderRadius: "50%",
38
+ background: "#ffffff",
39
+ boxShadow: "0 1px 3px rgba(0, 0, 0, 0.25)",
40
+ transition: TRANSITION,
41
+ });
42
+
43
+ export function ToggleSwitch({ checked, disabled = false, onChange, ariaLabel, id }: ToggleSwitchProps) {
44
+ return (
45
+ <button
46
+ id={id}
47
+ type="button"
48
+ role="switch"
49
+ aria-checked={checked}
50
+ aria-label={ariaLabel}
51
+ disabled={disabled}
52
+ style={trackStyle(checked, disabled)}
53
+ onClick={() => {
54
+ if (!disabled) onChange(!checked);
55
+ }}
56
+ >
57
+ <span aria-hidden="true" style={thumbStyle(checked)} />
58
+ </button>
59
+ );
60
+ }
@@ -203,4 +203,4 @@ export const CONSUMED_PREVIEW_CODES = new Set([
203
203
  "unsafe_destination",
204
204
  ]);
205
205
 
206
- export const PLUGIN_VERSION = "0.5.4";
206
+ export const PLUGIN_VERSION = "0.5.6";
@@ -0,0 +1,36 @@
1
+ /** Quick-setup code snippets for the local API gateway. */
2
+
3
+ export type GatewaySnippetId = "curl" | "python" | "ide";
4
+
5
+ export function buildGatewaySnippets(
6
+ openAiBaseUrl: string,
7
+ anthropicBaseUrl: string,
8
+ apiKeyPlaceholder: string,
9
+ ): Record<GatewaySnippetId, string> {
10
+ const key = apiKeyPlaceholder.length > 0 ? apiKeyPlaceholder : "<your-gateway-key>";
11
+ return {
12
+ curl: `curl ${openAiBaseUrl}/chat/completions \\
13
+ -H "Authorization: Bearer ${key}" \\
14
+ -H "Content-Type: application/json" \\
15
+ -d '{"model":"codex-oauth/gpt-4.1","messages":[{"role":"user","content":"Hello"}]}'`,
16
+ python: `from openai import OpenAI
17
+
18
+ client = OpenAI(
19
+ base_url="${openAiBaseUrl}",
20
+ api_key="${key}",
21
+ )
22
+
23
+ response = client.chat.completions.create(
24
+ model="codex-oauth/gpt-4.1",
25
+ messages=[{"role": "user", "content": "Hello"}],
26
+ )
27
+ print(response.choices[0].message.content)`,
28
+ ide: `# OpenAI-compatible client
29
+ base_url: ${openAiBaseUrl}
30
+ api_key: ${key}
31
+
32
+ # Anthropic-compatible client
33
+ base_url: ${anthropicBaseUrl}
34
+ api_key: ${key}`,
35
+ };
36
+ }
@@ -228,6 +228,23 @@ export const en = {
228
228
  pluginVersion: "Plugin version {version}",
229
229
  aboutDocsHint:
230
230
  "See the plugin README and INSTALL docs in the repository for setup, supported providers, and gateway safety notes.",
231
+ otherLoginMethods: "Other sign-in methods",
232
+ hideOtherLoginMethods: "Hide other methods",
233
+ signInStepOpen: "Open the provider authorization page",
234
+ signInStepCode: "Enter the device code when prompted",
235
+ signInStepWait: "Waiting for authorization to complete",
236
+ copyUserCode: "Copy code",
237
+ openAuthUrl: "Open authorization page",
238
+ modelFilterPlaceholder: "Filter models…",
239
+ deselectAll: "Hide all",
240
+ resetModelsDefault: "Reset selection",
241
+ requiresCodexImages: "Turn on Codex images first.",
242
+ gatewaySnippetsTitle: "Quick setup snippets",
243
+ gatewaySnippetCurl: "cURL",
244
+ gatewaySnippetPython: "Python (OpenAI SDK)",
245
+ gatewaySnippetIde: "IDE / client",
246
+ tabAccountsSignedIn: "{count} signed in",
247
+ tabGatewayActive: "Running",
231
248
  };
232
249
 
233
250
  export type GrokBuildSettingsKey = keyof typeof en;
@@ -450,4 +467,21 @@ export const zh: { [Key in GrokBuildSettingsKey]: string } = {
450
467
  "在远程 DSH 主机上,打开此处 Settings,对每个供应商使用设备码登录。在能访问供应商的任意浏览器完成验证码,再回到聊天选择对应路由模型。",
451
468
  pluginVersion: "插件版本 {version}",
452
469
  aboutDocsHint: "仓库中的 README 与 INSTALL 文档介绍了安装步骤、支持的供应商以及网关安全注意事项。",
470
+ otherLoginMethods: "其他登录方式",
471
+ hideOtherLoginMethods: "收起其他方式",
472
+ signInStepOpen: "打开供应商授权页面",
473
+ signInStepCode: "在提示处输入设备码",
474
+ signInStepWait: "等待授权完成",
475
+ copyUserCode: "复制设备码",
476
+ openAuthUrl: "打开授权页面",
477
+ modelFilterPlaceholder: "筛选模型…",
478
+ deselectAll: "全部隐藏",
479
+ resetModelsDefault: "重置选择",
480
+ requiresCodexImages: "需先开启 Codex 出图。",
481
+ gatewaySnippetsTitle: "快速配置片段",
482
+ gatewaySnippetCurl: "cURL",
483
+ gatewaySnippetPython: "Python (OpenAI SDK)",
484
+ gatewaySnippetIde: "IDE / 客户端",
485
+ tabAccountsSignedIn: "已登录 {count} 个",
486
+ tabGatewayActive: "运行中",
453
487
  };
@@ -0,0 +1,33 @@
1
+ /** Injects global keyframes once for skeleton pulse and spinner animations. */
2
+
3
+ const STYLE_ID = "dsh-coding-oauth-micro-styles";
4
+
5
+ const CSS = `
6
+ @keyframes dsh-coding-oauth-skeleton-pulse {
7
+ 0% { background-position: 200% 0; }
8
+ 100% { background-position: -200% 0; }
9
+ }
10
+ @keyframes dsh-coding-oauth-spin {
11
+ from { transform: rotate(0deg); }
12
+ to { transform: rotate(360deg); }
13
+ }
14
+ @keyframes dsh-coding-oauth-fade-in {
15
+ from { opacity: 0; transform: translateY(-4px); }
16
+ to { opacity: 1; transform: translateY(0); }
17
+ }
18
+ `;
19
+
20
+ let injected = false;
21
+
22
+ export function ensureMicroStyles(): void {
23
+ if (injected || typeof document === "undefined") return;
24
+ if (document.getElementById(STYLE_ID) !== null) {
25
+ injected = true;
26
+ return;
27
+ }
28
+ const style = document.createElement("style");
29
+ style.id = STYLE_ID;
30
+ style.textContent = CSS;
31
+ document.head.appendChild(style);
32
+ injected = true;
33
+ }