dsh-plugin-subscriptions 0.5.0 → 0.5.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 (44) hide show
  1. package/README.md +21 -6
  2. package/README.zh.md +20 -6
  3. package/lib/auth/device-flow.d.ts +55 -0
  4. package/lib/auth/device-flow.js +177 -0
  5. package/lib/auth/oauth-flow.js +1 -1
  6. package/lib/auth/rpc.d.ts +18 -2
  7. package/lib/auth/rpc.js +98 -3
  8. package/lib/auth/store.d.ts +20 -2
  9. package/lib/auth/store.js +45 -9
  10. package/lib/client/SubscriptionsSection.d.ts +18 -1
  11. package/lib/client/SubscriptionsSection.js +216 -6
  12. package/lib/client/index.js +11 -0
  13. package/lib/client/locales.d.ts +72 -0
  14. package/lib/client/locales.js +72 -0
  15. package/lib/client.js +725 -144
  16. package/lib/client.js.map +1 -1
  17. package/lib/http.d.ts +114 -0
  18. package/lib/http.js +402 -0
  19. package/lib/index.d.ts +3 -2
  20. package/lib/index.js +2256 -226
  21. package/lib/providers/antigravity.d.ts +90 -0
  22. package/lib/providers/antigravity.js +392 -0
  23. package/lib/providers/catalog-store.js +15 -0
  24. package/lib/providers/claude.d.ts +20 -1
  25. package/lib/providers/claude.js +51 -33
  26. package/lib/providers/codex.js +58 -13
  27. package/lib/providers/common.d.ts +32 -1
  28. package/lib/providers/common.js +48 -1
  29. package/lib/providers/copilot.d.ts +315 -0
  30. package/lib/providers/copilot.js +787 -0
  31. package/lib/providers/grok.d.ts +7 -2
  32. package/lib/providers/grok.js +53 -24
  33. package/lib/tools/image-generate.js +2 -1
  34. package/lib/tools/video-generate.js +2 -1
  35. package/lib/tools/x-search.js +2 -1
  36. package/lib/translate/anthropic.d.ts +47 -6
  37. package/lib/translate/anthropic.js +135 -20
  38. package/lib/translate/antigravity.d.ts +110 -0
  39. package/lib/translate/antigravity.js +303 -0
  40. package/lib/translate/chat-completions.d.ts +120 -0
  41. package/lib/translate/chat-completions.js +363 -0
  42. package/lib/translate/responses.d.ts +49 -5
  43. package/lib/translate/responses.js +40 -7
  44. package/package.json +11 -7
package/lib/auth/store.js CHANGED
@@ -10,7 +10,7 @@ import { chmod, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises'
10
10
  import { dirname } from 'node:path';
11
11
  import { dshHomePath } from '@deepseek-ai/dsh-home-paths';
12
12
  /** Every provider route, in display order. */
13
- export const PROVIDER_IDS = ['codex', 'claude', 'grok'];
13
+ export const PROVIDER_IDS = ['codex', 'claude', 'grok', 'copilot'];
14
14
  /**
15
15
  * Absolute path of the auth store file.
16
16
  * @returns `dshHomePath('plugins', 'subscriptions', 'auth.json')`.
@@ -103,6 +103,38 @@ async function writeStore(store, path) {
103
103
  throw error;
104
104
  }
105
105
  }
106
+ /**
107
+ * One write chain per store path. Every mutation is a read-modify-write of a
108
+ * single JSON file, and the plugin has several independent writers — a login,
109
+ * a logout, and one token refresh per provider adapter, each on its own
110
+ * schedule. Overlapping them unserialized costs whichever provider read the
111
+ * store first its entry.
112
+ *
113
+ * A chain is dropped once nothing is queued behind it, so the map holds an
114
+ * entry only while writes are in flight.
115
+ */
116
+ const writeChains = new Map();
117
+ /**
118
+ * Run one read-modify-write of a store path after every write already queued
119
+ * for it. Callers join the chain synchronously, so call order is write order.
120
+ * @param path - the store file being mutated.
121
+ * @param action - the read-modify-write to run.
122
+ * @returns whatever `action` returns.
123
+ */
124
+ async function serialize(path, action) {
125
+ const previous = writeChains.get(path) ?? Promise.resolve();
126
+ // Both handlers: a failed write must not strand everything queued behind it.
127
+ const next = previous.then(action, action);
128
+ const tail = next.then(() => undefined, () => undefined);
129
+ writeChains.set(path, tail);
130
+ try {
131
+ return await next;
132
+ }
133
+ finally {
134
+ if (writeChains.get(path) === tail)
135
+ writeChains.delete(path);
136
+ }
137
+ }
106
138
  /**
107
139
  * Read one provider's session.
108
140
  * @param provider - the provider route.
@@ -119,9 +151,11 @@ export async function getSession(provider, path = authFilePath()) {
119
151
  * @param path - store file path; defaults to {@link authFilePath}.
120
152
  */
121
153
  export async function saveSession(provider, session, path = authFilePath()) {
122
- const store = await loadStore(path);
123
- store[provider] = session;
124
- await writeStore(store, path);
154
+ return serialize(path, async () => {
155
+ const store = await loadStore(path);
156
+ store[provider] = session;
157
+ await writeStore(store, path);
158
+ });
125
159
  }
126
160
  /**
127
161
  * Delete one provider's session (logout).
@@ -129,9 +163,11 @@ export async function saveSession(provider, session, path = authFilePath()) {
129
163
  * @param path - store file path; defaults to {@link authFilePath}.
130
164
  */
131
165
  export async function deleteSession(provider, path = authFilePath()) {
132
- const store = await loadStore(path);
133
- if (store[provider] === undefined)
134
- return;
135
- delete store[provider];
136
- await writeStore(store, path);
166
+ return serialize(path, async () => {
167
+ const store = await loadStore(path);
168
+ if (store[provider] === undefined)
169
+ return;
170
+ delete store[provider];
171
+ await writeStore(store, path);
172
+ });
137
173
  }
@@ -1,7 +1,7 @@
1
1
  import type { ConnectionHandle } from '@deepseek-ai/dsh-api-remotes/client';
2
2
  import type { SubscriptionsKey } from './locales.js';
3
3
  /** Subscription provider ids, fixed by the node half's OAuth adapters. */
4
- export type SubscriptionProvider = 'codex' | 'claude' | 'grok';
4
+ export type SubscriptionProvider = 'codex' | 'claude' | 'grok' | 'copilot';
5
5
  /** One provider's login state as answered by the `status` endpoint. */
6
6
  export interface ProviderStatus {
7
7
  loggedIn: boolean;
@@ -23,6 +23,23 @@ export interface ProviderUsage {
23
23
  windows?: UsageWindow[];
24
24
  plan?: string;
25
25
  }
26
+ /** `proxyGet` endpoint value: the node half owns this shape (no secrets). */
27
+ export interface ProxyConfigView {
28
+ enabled: boolean;
29
+ url: string;
30
+ username?: string;
31
+ passwordSet: boolean;
32
+ bypass: string[];
33
+ error?: string;
34
+ }
35
+ /** `proxyTest` endpoint value. */
36
+ export interface ProxyTestResult {
37
+ ok: boolean;
38
+ viaProxy: boolean;
39
+ status?: number;
40
+ latencyMs?: number;
41
+ error?: string;
42
+ }
26
43
  /** Injected dependencies of {@link SubscriptionsSection} (slot `inject`). */
27
44
  export interface SubscriptionsSectionInjected {
28
45
  /** Generic logical-RPC caller over the Connection transport. */
@@ -24,6 +24,7 @@ const PROVIDERS = [
24
24
  { id: 'codex', name: 'Codex (ChatGPT)' },
25
25
  { id: 'claude', name: 'Claude' },
26
26
  { id: 'grok', name: 'Grok (X Premium)' },
27
+ { id: 'copilot', name: 'GitHub Copilot' },
27
28
  ];
28
29
  /** Business error returned by the `/subscriptions-auth` channel (error branch message). */
29
30
  class SubscriptionsAuthError extends Error {
@@ -77,6 +78,10 @@ const styles = {
77
78
  border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 12,
78
79
  padding: '12px 14px', display: 'flex', flexDirection: 'column', gap: 6,
79
80
  },
81
+ proxyCard: {
82
+ padding: '12px 14px', display: 'flex', flexDirection: 'column', gap: 6,
83
+ },
84
+ separator: { borderTop: '1px solid var(--dsw-alias-border-l2)' },
80
85
  cardHeader: { display: 'flex', alignItems: 'center', gap: 8 },
81
86
  dot: { width: 8, height: 8, borderRadius: '50%', flexShrink: 0 },
82
87
  name: { fontWeight: 500, fontSize: 14, lineHeight: '22px', color: 'var(--dsw-alias-label-primary)' },
@@ -122,6 +127,45 @@ const styles = {
122
127
  padding: '0 10px', font: 'inherit', fontSize: 14, lineHeight: '22px',
123
128
  background: 'var(--dsw-alias-bg-layer-1)', color: 'var(--dsw-alias-label-primary)',
124
129
  },
130
+ deviceCode: {
131
+ marginTop: 4, display: 'flex', flexDirection: 'column', gap: 6,
132
+ border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8,
133
+ padding: '10px 12px', background: 'var(--dsw-alias-bg-layer-1)',
134
+ },
135
+ deviceCodeText: {
136
+ fontFamily: 'monospace', fontSize: 18, lineHeight: '24px', letterSpacing: 2,
137
+ color: 'var(--dsw-alias-label-primary)', userSelect: 'all',
138
+ },
139
+ proxyField: { display: 'flex', flexDirection: 'column', gap: 4 },
140
+ proxyLabel: { fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-label-secondary)' },
141
+ proxyInput: {
142
+ height: 32, width: '100%', boxSizing: 'border-box',
143
+ border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8,
144
+ padding: '0 10px', font: 'inherit', fontSize: 14, lineHeight: '22px',
145
+ background: 'var(--dsw-alias-bg-layer-1)', color: 'var(--dsw-alias-label-primary)',
146
+ },
147
+ proxyHint: {
148
+ margin: 0, fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-label-tertiary)',
149
+ },
150
+ proxyCheck: {
151
+ display: 'flex', alignItems: 'center', gap: 8,
152
+ fontSize: 13, lineHeight: '20px', color: 'var(--dsw-alias-label-primary)', cursor: 'pointer',
153
+ },
154
+ proxyMessage: { margin: 0, fontSize: 12, lineHeight: '18px' },
155
+ proxyActions: { display: 'flex', gap: 8, alignItems: 'center', justifyContent: 'flex-end', marginTop: 2 },
156
+ modalOverlay: {
157
+ position: 'fixed', inset: 0, zIndex: 1000,
158
+ display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 16,
159
+ background: 'rgba(0, 0, 0, 0.45)',
160
+ },
161
+ modal: {
162
+ width: 460, maxWidth: '100%', maxHeight: '90vh', overflowY: 'auto',
163
+ boxSizing: 'border-box', display: 'flex', flexDirection: 'column', gap: 12,
164
+ padding: '16px 18px', borderRadius: 12,
165
+ background: 'var(--dsw-alias-bg-layer-1)', border: '1px solid var(--dsw-alias-border-l2)',
166
+ },
167
+ modalHeader: { display: 'flex', alignItems: 'center', gap: 8 },
168
+ modalTitle: { fontWeight: 600, fontSize: 15, lineHeight: '22px', color: 'var(--dsw-alias-label-primary)' },
125
169
  };
126
170
  /** Status dot color for one provider state. */
127
171
  function dotColor(status) {
@@ -178,6 +222,24 @@ function usageBarColor(usedPercent) {
178
222
  return 'var(--dsw-alias-state-warn-label)';
179
223
  return 'var(--dsw-alias-state-success-primary)';
180
224
  }
225
+ /** One-line status text of the proxy config card. */
226
+ function proxyStatusText(t, proxy, loadError) {
227
+ if (loadError !== undefined)
228
+ return t('proxyLoadFailed', { message: loadError });
229
+ if (proxy === undefined)
230
+ return t('proxyLoading');
231
+ if (proxy.error !== undefined)
232
+ return t('proxyStatusError', { message: proxy.error });
233
+ if (proxy.enabled)
234
+ return t('proxyStatusEnabled', { url: proxy.url });
235
+ return t('proxyStatusNone');
236
+ }
237
+ /** Feedback-line color of the proxy dialog. */
238
+ function messageColor(tone) {
239
+ return tone === 'error'
240
+ ? 'var(--dsw-alias-state-error-primary)'
241
+ : 'var(--dsw-alias-state-success-primary)';
242
+ }
181
243
  /**
182
244
  * The Subscriptions settings page component.
183
245
  * @param props - the slot inject face ({@link SubscriptionsSectionInjected}).
@@ -189,8 +251,11 @@ export function SubscriptionsSection(props) {
189
251
  const [statuses, setStatuses] = useState({});
190
252
  const [errors, setErrors] = useState({});
191
253
  const [manualDrafts, setManualDrafts] = useState({
192
- codex: '', claude: '', grok: '',
254
+ codex: '', claude: '', grok: '', copilot: '',
193
255
  });
256
+ /** Pending device-flow codes (copilot), shown while the attempt polls. */
257
+ const [deviceCodes, setDeviceCodes] = useState({});
258
+ const [copiedCode, setCopiedCode] = useState(undefined);
194
259
  const [usages, setUsages] = useState({});
195
260
  const [usageErrors, setUsageErrors] = useState({});
196
261
  const [usageLoading, setUsageLoading] = useState({});
@@ -198,6 +263,21 @@ export function SubscriptionsSection(props) {
198
263
  const pollersRef = useRef(new Map());
199
264
  /** Providers with a `usage` call in flight; guards the auto-fetch effect against re-entry. */
200
265
  const usageInflightRef = useRef(new Set());
266
+ /** Proxy config as last answered by `proxyGet`/`proxySet`. */
267
+ const [proxy, setProxy] = useState(undefined);
268
+ const [proxyLoadError, setProxyLoadError] = useState(undefined);
269
+ /** Proxy dialog state (draft fields; the password never pre-fills). */
270
+ const [proxyOpen, setProxyOpen] = useState(false);
271
+ const [proxyEnabled, setProxyEnabled] = useState(false);
272
+ const [proxyUrl, setProxyUrl] = useState('');
273
+ const [proxyUsername, setProxyUsername] = useState('');
274
+ const [proxyPassword, setProxyPassword] = useState('');
275
+ const [proxyClearPassword, setProxyClearPassword] = useState(false);
276
+ const [proxyBypass, setProxyBypass] = useState('');
277
+ const [proxySaving, setProxySaving] = useState(false);
278
+ const [proxyTesting, setProxyTesting] = useState(false);
279
+ const [proxyMessage, setProxyMessage] = useState(undefined);
280
+ const [proxyTestResult, setProxyTestResult] = useState(undefined);
201
281
  const setProviderError = useCallback((provider, message) => {
202
282
  if (!mountedRef.current)
203
283
  return;
@@ -235,8 +315,17 @@ export function SubscriptionsSection(props) {
235
315
  setStatuses(response.providers);
236
316
  for (const { id } of PROVIDERS) {
237
317
  const status = response.providers[id];
238
- if (status.loggedIn || !status.busy)
318
+ if (status.loggedIn || !status.busy) {
239
319
  stopPolling(id);
320
+ // The attempt settled (success, timeout, or cancel): drop the code card.
321
+ setDeviceCodes((prev) => {
322
+ if (prev[id] === undefined)
323
+ return prev;
324
+ const next = { ...prev };
325
+ delete next[id];
326
+ return next;
327
+ });
328
+ }
240
329
  }
241
330
  }, [rpc, stopPolling]);
242
331
  const startPolling = useCallback((provider) => {
@@ -332,11 +421,18 @@ export function SubscriptionsSection(props) {
332
421
  if (typeof response.authorizeUrl !== 'string') {
333
422
  throw new SubscriptionsAuthError(t('loginMissingUrl'));
334
423
  }
335
- window.open(response.authorizeUrl, '_blank', 'noopener');
336
424
  if (!mountedRef.current)
337
425
  return;
338
426
  // Optimistic busy so Cancel and the manual fallback appear before the first poll tick.
339
427
  setStatuses(prev => ({ ...prev, [provider]: { ...prev[provider], busy: true, loggedIn: false } }));
428
+ if (typeof response.userCode === 'string' && response.userCode.length > 0) {
429
+ // Device flow: show the code card instead of opening the page blind —
430
+ // the user copies the code first, then opens the verification page.
431
+ setDeviceCodes(prev => ({ ...prev, [provider]: { userCode: response.userCode, verificationUrl: response.authorizeUrl } }));
432
+ }
433
+ else {
434
+ window.open(response.authorizeUrl, '_blank', 'noopener');
435
+ }
340
436
  startPolling(provider);
341
437
  }
342
438
  catch (error) {
@@ -386,12 +482,117 @@ export function SubscriptionsSection(props) {
386
482
  }
387
483
  await refresh();
388
484
  }, [rpc, t, setProviderError, refresh]);
485
+ const copyDeviceCode = useCallback((provider, userCode) => {
486
+ void navigator.clipboard?.writeText(userCode).then(() => {
487
+ if (!mountedRef.current)
488
+ return;
489
+ setCopiedCode(provider);
490
+ setTimeout(() => {
491
+ if (mountedRef.current) {
492
+ setCopiedCode(current => current === provider ? undefined : current);
493
+ }
494
+ }, 1500);
495
+ }).catch(() => undefined);
496
+ }, []);
497
+ // Proxy configuration: load once on mount; the dialog drives proxySet/proxyTest.
498
+ useEffect(() => {
499
+ if (rpc === undefined)
500
+ return;
501
+ let alive = true;
502
+ void callSubscriptionsAuth(rpc, 'proxyGet', {}).then((view) => {
503
+ if (!alive)
504
+ return;
505
+ setProxy(view);
506
+ setProxyLoadError(undefined);
507
+ }).catch((error) => {
508
+ if (alive)
509
+ setProxyLoadError(messageOf(error));
510
+ });
511
+ return () => { alive = false; };
512
+ }, [rpc]);
513
+ useEffect(() => {
514
+ if (!proxyOpen)
515
+ return;
516
+ const onKey = (event) => {
517
+ if (event.key === 'Escape')
518
+ setProxyOpen(false);
519
+ };
520
+ window.addEventListener('keydown', onKey);
521
+ return () => window.removeEventListener('keydown', onKey);
522
+ }, [proxyOpen]);
523
+ const openProxyDialog = useCallback(() => {
524
+ if (proxy === undefined)
525
+ return;
526
+ setProxyEnabled(proxy.enabled);
527
+ setProxyUrl(proxy.url);
528
+ setProxyUsername(proxy.username ?? '');
529
+ setProxyPassword('');
530
+ setProxyClearPassword(false);
531
+ setProxyBypass(proxy.bypass.join(', '));
532
+ setProxyMessage(undefined);
533
+ setProxyTestResult(undefined);
534
+ setProxyOpen(true);
535
+ }, [proxy]);
536
+ const saveProxy = useCallback(async () => {
537
+ if (rpc === undefined)
538
+ return;
539
+ setProxySaving(true);
540
+ setProxyMessage(undefined);
541
+ try {
542
+ const view = await callSubscriptionsAuth(rpc, 'proxySet', {
543
+ enabled: proxyEnabled,
544
+ url: proxyUrl.trim(),
545
+ username: proxyUsername,
546
+ ...proxyClearPassword ? { password: null } : proxyPassword !== '' ? { password: proxyPassword } : {},
547
+ bypass: proxyBypass.split(/[,\n]/).map(entry => entry.trim()).filter(entry => entry !== ''),
548
+ });
549
+ setProxy(view);
550
+ setProxyLoadError(undefined);
551
+ setProxyMessage({ tone: 'success', text: t('proxySaved') });
552
+ setProxyOpen(false);
553
+ }
554
+ catch (error) {
555
+ setProxyMessage({ tone: 'error', text: t('proxySaveFailed', { message: messageOf(error) }) });
556
+ }
557
+ finally {
558
+ setProxySaving(false);
559
+ }
560
+ }, [rpc, proxyEnabled, proxyUrl, proxyUsername, proxyPassword, proxyClearPassword, proxyBypass, t]);
561
+ const testProxy = useCallback(async () => {
562
+ if (rpc === undefined || proxyTesting)
563
+ return;
564
+ setProxyTesting(true);
565
+ setProxyTestResult(undefined);
566
+ try {
567
+ // Test the dialog's current inputs (they do not need to be saved first);
568
+ // the host builds a throwaway agent for the probe.
569
+ setProxyTestResult(await callSubscriptionsAuth(rpc, 'proxyTest', {
570
+ proxy: {
571
+ url: proxyUrl.trim(),
572
+ ...proxyUsername.trim() !== '' ? { username: proxyUsername.trim() } : {},
573
+ ...proxyPassword !== '' ? { password: proxyPassword } : {},
574
+ },
575
+ }));
576
+ }
577
+ catch (error) {
578
+ setProxyTestResult({ ok: false, viaProxy: false, error: messageOf(error) });
579
+ }
580
+ finally {
581
+ setProxyTesting(false);
582
+ }
583
+ }, [rpc, proxyTesting, proxyUrl, proxyUsername, proxyPassword]);
389
584
  if (rpc === undefined) {
390
585
  return _jsx("p", { style: styles.intro, children: t('unavailable') });
391
586
  }
392
- return (_jsxs("div", { style: styles.section, children: [_jsx("p", { style: styles.intro, children: t('intro') }), PROVIDERS.map(({ id, name }) => {
587
+ return (_jsxs("div", { style: styles.section, children: [_jsx("p", { style: styles.intro, children: t('intro') }), _jsxs("div", { style: styles.proxyCard, children: [_jsxs("div", { style: styles.cardHeader, children: [_jsx("span", { style: {
588
+ ...styles.dot,
589
+ background: proxy?.enabled === true
590
+ ? 'var(--dsw-alias-state-success-primary)'
591
+ : 'var(--dsw-alias-label-dimmed)',
592
+ } }), _jsx("span", { style: styles.name, children: t('proxyTitle') }), _jsx("button", { type: "button", style: { ...styles.button, marginLeft: 'auto', flexShrink: 0 }, onClick: openProxyDialog, children: t('proxyConfigure') })] }), _jsx("p", { style: styles.statusLine, children: proxyStatusText(t, proxy, proxyLoadError) })] }), _jsx("div", { style: styles.separator }), PROVIDERS.map(({ id, name }) => {
393
593
  const status = statuses[id];
394
594
  const busy = status?.busy === true;
595
+ const deviceCode = deviceCodes[id];
395
596
  const usage = usages[id];
396
597
  const usageError = usageErrors[id];
397
598
  // Providers without a usage endpoint answer supported:false — no block.
@@ -401,6 +602,15 @@ export function SubscriptionsSection(props) {
401
602
  const percent = Math.min(100, Math.max(0, window.usedPercent));
402
603
  return (_jsxs("div", { style: styles.usageRow, children: [_jsxs("div", { style: styles.usageMeta, children: [_jsx("span", { children: usageWindowLabel(t, window) }), _jsxs("span", { children: [`${String(Math.round(percent))}%`, window.resetsAt !== undefined
403
604
  && ` · ${t('usageResets', { date: new Date(window.resetsAt).toLocaleString() })}`] })] }), _jsx("div", { style: styles.usageTrack, children: _jsx("div", { style: { ...styles.usageFill, width: `${String(percent)}%`, background: usageBarColor(percent) } }) })] }, index));
404
- })] })), busy && (_jsxs("details", { style: styles.manual, children: [_jsx("summary", { children: t('manualSummary') }), _jsxs("div", { style: styles.manualRow, children: [_jsx("input", { style: styles.manualInput, value: manualDrafts[id], placeholder: t('manualPlaceholder'), onChange: event => setManualDrafts(prev => ({ ...prev, [id]: event.target.value })) }), _jsx("button", { type: "button", style: styles.button, onClick: () => { void submitManual(id); }, children: t('submit') })] })] }))] }, id));
405
- })] }));
605
+ })] })), busy && deviceCode !== undefined && (_jsxs("div", { style: styles.deviceCode, children: [_jsx("span", { style: styles.statusLine, children: t('deviceCodePrompt') }), _jsx("span", { style: styles.deviceCodeText, children: deviceCode.userCode }), _jsxs("div", { style: styles.actions, children: [_jsx("button", { type: "button", style: styles.button, onClick: () => { copyDeviceCode(id, deviceCode.userCode); }, children: copiedCode === id ? t('deviceCodeCopied') : t('deviceCodeCopy') }), _jsx("button", { type: "button", style: styles.button, onClick: () => { window.open(deviceCode.verificationUrl, '_blank', 'noopener'); }, children: t('deviceCodeOpenPage') })] })] })), busy && deviceCode === undefined && (_jsxs("details", { style: styles.manual, children: [_jsx("summary", { children: t('manualSummary') }), _jsxs("div", { style: styles.manualRow, children: [_jsx("input", { style: styles.manualInput, value: manualDrafts[id], placeholder: t('manualPlaceholder'), onChange: event => setManualDrafts(prev => ({ ...prev, [id]: event.target.value })) }), _jsx("button", { type: "button", style: styles.button, onClick: () => { void submitManual(id); }, children: t('submit') })] })] }))] }, id));
606
+ }), proxyOpen && (_jsx("div", { style: styles.modalOverlay, onClick: () => setProxyOpen(false), children: _jsxs("div", { style: styles.modal, onClick: event => event.stopPropagation(), children: [_jsxs("div", { style: styles.modalHeader, children: [_jsx("span", { style: styles.modalTitle, children: t('proxyDialogTitle') }), _jsx("button", { type: "button", style: { ...styles.button, marginLeft: 'auto' }, onClick: () => setProxyOpen(false), children: t('proxyDialogClose') })] }), _jsxs("label", { style: styles.proxyCheck, children: [_jsx("input", { type: "checkbox", checked: proxyEnabled, onChange: event => setProxyEnabled(event.target.checked) }), _jsx("span", { children: t('proxyEnabled') })] }), _jsxs("label", { style: styles.proxyField, children: [_jsx("span", { style: styles.proxyLabel, children: t('proxyUrl') }), _jsx("input", { style: styles.proxyInput, value: proxyUrl, placeholder: t('proxyUrlPlaceholder'), onChange: event => setProxyUrl(event.target.value) }), _jsx("p", { style: styles.proxyHint, children: t('proxyUrlHint') })] }), _jsxs("label", { style: styles.proxyField, children: [_jsx("span", { style: styles.proxyLabel, children: t('proxyUsername') }), _jsx("input", { style: styles.proxyInput, value: proxyUsername, placeholder: t('proxyUsernamePlaceholder'), onChange: event => setProxyUsername(event.target.value) })] }), _jsxs("div", { style: styles.proxyField, children: [_jsx("span", { style: styles.proxyLabel, children: t('proxyPassword') }), _jsx("input", { type: "password", style: styles.proxyInput, value: proxyPassword, placeholder: t('proxyPasswordPlaceholder'), onChange: event => setProxyPassword(event.target.value) }), _jsxs("label", { style: styles.proxyCheck, children: [_jsx("input", { type: "checkbox", checked: proxyClearPassword, onChange: event => setProxyClearPassword(event.target.checked) }), _jsx("span", { children: t('proxyClearPassword') })] })] }), _jsxs("label", { style: styles.proxyField, children: [_jsx("span", { style: styles.proxyLabel, children: t('proxyBypass') }), _jsx("input", { style: styles.proxyInput, value: proxyBypass, placeholder: t('proxyBypassPlaceholder'), onChange: event => setProxyBypass(event.target.value) }), _jsx("p", { style: styles.proxyHint, children: t('proxyBypassHint') })] }), _jsx("p", { style: styles.proxyHint, children: t('proxyNote') }), proxyMessage !== undefined && (_jsx("p", { style: { ...styles.proxyMessage, color: messageColor(proxyMessage.tone) }, children: proxyMessage.text })), proxyTestResult !== undefined && (_jsx("p", { style: {
607
+ ...styles.proxyMessage,
608
+ color: proxyTestResult.ok
609
+ ? 'var(--dsw-alias-state-success-primary)'
610
+ : 'var(--dsw-alias-state-error-primary)',
611
+ }, children: proxyTestResult.ok
612
+ ? (proxyTestResult.viaProxy
613
+ ? t('proxyTestOk', { status: String(proxyTestResult.status), ms: String(proxyTestResult.latencyMs) })
614
+ : t('proxyTestOkDirect', { status: String(proxyTestResult.status), ms: String(proxyTestResult.latencyMs) }))
615
+ : t('proxyTestFail', { message: proxyTestResult.error ?? '' }) })), _jsxs("div", { style: styles.proxyActions, children: [_jsx("button", { type: "button", style: { ...styles.button, ...proxyTesting ? { opacity: 0.5, cursor: 'default' } : {} }, disabled: proxyTesting, onClick: () => { void testProxy(); }, children: proxyTesting ? t('proxyTesting') : t('proxyTest') }), _jsx("button", { type: "button", style: { ...styles.button, ...proxySaving ? { opacity: 0.5, cursor: 'default' } : {} }, disabled: proxySaving, onClick: () => { void saveProxy(); }, children: proxySaving ? t('proxySaving') : t('proxySave') }), _jsx("button", { type: "button", style: styles.button, onClick: () => setProxyOpen(false), children: t('proxyCancel') })] })] }) }))] }));
406
616
  }
@@ -22,6 +22,17 @@ export const inject = ['slots', 'connection', 'locale'];
22
22
  */
23
23
  export function apply(ctx) {
24
24
  ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-plugin-subscriptions: copy dictionaries');
25
+ // Settings-shell nudge: the panel (nav title + header row + section body)
26
+ // sits flush against the panel's top edge; push it down a little to leave
27
+ // breathing room. Scoped by the settings panel's own dialog role + nav child
28
+ // so other aria-modal dialogs (e.g. the attachment lightbox) are untouched.
29
+ ctx.effect(() => {
30
+ const style = document.createElement('style');
31
+ style.setAttribute('data-plugin', 'dsh-plugin-subscriptions');
32
+ style.textContent = 'div[role="dialog"][aria-modal="true"]:has(> nav) { padding-top: 14px; }';
33
+ document.head.appendChild(style);
34
+ return () => style.remove();
35
+ }, 'dsh-plugin-subscriptions: settings panel breathing room');
25
36
  // The client-runtime Context merge types `connection` as the host handle;
26
37
  // in the browser shell the same key holds the full client ConnectionHandle.
27
38
  const connection = ctx.get('connection');
@@ -19,6 +19,10 @@ export declare const en: {
19
19
  manualPlaceholder: string;
20
20
  submit: string;
21
21
  loginMissingUrl: string;
22
+ deviceCodePrompt: string;
23
+ deviceCodeCopy: string;
24
+ deviceCodeCopied: string;
25
+ deviceCodeOpenPage: string;
22
26
  usageTitle: string;
23
27
  usageRefresh: string;
24
28
  usageLoading: string;
@@ -47,6 +51,38 @@ export declare const en: {
47
51
  speedFastDescription: string;
48
52
  commandFast: string;
49
53
  commandFastUnavailable: string;
54
+ proxyTitle: string;
55
+ proxyStatusNone: string;
56
+ proxyStatusEnabled: string;
57
+ proxyStatusError: string;
58
+ proxyConfigure: string;
59
+ proxyDialogTitle: string;
60
+ proxyDialogClose: string;
61
+ proxyEnabled: string;
62
+ proxyUrl: string;
63
+ proxyUrlPlaceholder: string;
64
+ proxyUrlHint: string;
65
+ proxyUsername: string;
66
+ proxyUsernamePlaceholder: string;
67
+ proxyPassword: string;
68
+ proxyPasswordPlaceholder: string;
69
+ proxyClearPassword: string;
70
+ proxyBypass: string;
71
+ proxyBypassPlaceholder: string;
72
+ proxyBypassHint: string;
73
+ proxyTest: string;
74
+ proxyTesting: string;
75
+ proxyTestOk: string;
76
+ proxyTestOkDirect: string;
77
+ proxyTestFail: string;
78
+ proxySave: string;
79
+ proxyCancel: string;
80
+ proxySaving: string;
81
+ proxyLoading: string;
82
+ proxyLoadFailed: string;
83
+ proxySaved: string;
84
+ proxySaveFailed: string;
85
+ proxyNote: string;
50
86
  };
51
87
  /** zh strings, one per {@link en} key. */
52
88
  export declare const zh: {
@@ -68,6 +104,10 @@ export declare const zh: {
68
104
  manualPlaceholder: string;
69
105
  submit: string;
70
106
  loginMissingUrl: string;
107
+ deviceCodePrompt: string;
108
+ deviceCodeCopy: string;
109
+ deviceCodeCopied: string;
110
+ deviceCodeOpenPage: string;
71
111
  usageTitle: string;
72
112
  usageRefresh: string;
73
113
  usageLoading: string;
@@ -96,6 +136,38 @@ export declare const zh: {
96
136
  speedFastDescription: string;
97
137
  commandFast: string;
98
138
  commandFastUnavailable: string;
139
+ proxyTitle: string;
140
+ proxyStatusNone: string;
141
+ proxyStatusEnabled: string;
142
+ proxyStatusError: string;
143
+ proxyConfigure: string;
144
+ proxyDialogTitle: string;
145
+ proxyDialogClose: string;
146
+ proxyEnabled: string;
147
+ proxyUrl: string;
148
+ proxyUrlPlaceholder: string;
149
+ proxyUrlHint: string;
150
+ proxyUsername: string;
151
+ proxyUsernamePlaceholder: string;
152
+ proxyPassword: string;
153
+ proxyPasswordPlaceholder: string;
154
+ proxyClearPassword: string;
155
+ proxyBypass: string;
156
+ proxyBypassPlaceholder: string;
157
+ proxyBypassHint: string;
158
+ proxyTest: string;
159
+ proxyTesting: string;
160
+ proxyTestOk: string;
161
+ proxyTestOkDirect: string;
162
+ proxyTestFail: string;
163
+ proxySave: string;
164
+ proxyCancel: string;
165
+ proxySaving: string;
166
+ proxyLoading: string;
167
+ proxyLoadFailed: string;
168
+ proxySaved: string;
169
+ proxySaveFailed: string;
170
+ proxyNote: string;
99
171
  };
100
172
  /** The Subscriptions namespace key union (en is the key-set source of truth). */
101
173
  export type SubscriptionsKey = keyof typeof en;
@@ -19,6 +19,10 @@ export const en = {
19
19
  manualPlaceholder: 'Paste the callback URL or code',
20
20
  submit: 'Submit',
21
21
  loginMissingUrl: 'login answered without an authorizeUrl',
22
+ deviceCodePrompt: 'Enter this code on the GitHub verification page:',
23
+ deviceCodeCopy: 'Copy code',
24
+ deviceCodeCopied: 'Copied',
25
+ deviceCodeOpenPage: 'Open GitHub verification page',
22
26
  usageTitle: 'Usage',
23
27
  usageRefresh: 'Refresh',
24
28
  usageLoading: 'Loading usage…',
@@ -47,6 +51,38 @@ export const en = {
47
51
  speedFastDescription: '1.5x speed, more usage',
48
52
  commandFast: 'Switch the Codex speed tier (Standard/Fast)',
49
53
  commandFastUnavailable: 'The current model has no fast tier; /fast only works on Codex models whose catalog advertises one',
54
+ proxyTitle: 'Proxy',
55
+ proxyStatusNone: 'Not configured — subscription requests go direct.',
56
+ proxyStatusEnabled: 'Enabled · {url}',
57
+ proxyStatusError: 'Config error: {message}',
58
+ proxyConfigure: 'Configure…',
59
+ proxyDialogTitle: 'Proxy settings',
60
+ proxyDialogClose: 'Close',
61
+ proxyEnabled: 'Route subscription requests through a proxy',
62
+ proxyUrl: 'Proxy URL',
63
+ proxyUrlPlaceholder: 'http://localhost:7890',
64
+ proxyUrlHint: 'HTTP or HTTPS proxy only (Clash/mihomo, v2rayN…); socks is not supported.',
65
+ proxyUsername: 'Username (optional)',
66
+ proxyUsernamePlaceholder: 'Proxy username',
67
+ proxyPassword: 'Password',
68
+ proxyPasswordPlaceholder: 'Leave blank to keep the saved password',
69
+ proxyClearPassword: 'Clear the saved password',
70
+ proxyBypass: 'Bypass hosts',
71
+ proxyBypassPlaceholder: '127.0.0.1, localhost, *.example.com',
72
+ proxyBypassHint: 'Comma-separated hostnames that keep going direct.',
73
+ proxyTest: 'Test',
74
+ proxyTesting: 'Testing…',
75
+ proxyTestOk: 'OK · HTTP {status} · {ms} ms',
76
+ proxyTestOkDirect: 'OK (direct, bypassed) · HTTP {status} · {ms} ms',
77
+ proxyTestFail: 'Failed: {message}',
78
+ proxySave: 'Save',
79
+ proxyCancel: 'Cancel',
80
+ proxySaving: 'Saving…',
81
+ proxyLoading: 'Loading proxy settings…',
82
+ proxyLoadFailed: 'Failed to load proxy settings: {message}',
83
+ proxySaved: 'Saved — new requests use the proxy.',
84
+ proxySaveFailed: 'Save failed: {message}',
85
+ proxyNote: 'Applies to token exchange, model APIs, usage lookups, image/video generation and x_search. The OAuth authorization page opens in your browser and follows the browser/system proxy, not this setting.',
50
86
  };
51
87
  /** zh strings, one per {@link en} key. */
52
88
  export const zh = {
@@ -68,6 +104,10 @@ export const zh = {
68
104
  manualPlaceholder: '粘贴回调 URL 或授权码',
69
105
  submit: '提交',
70
106
  loginMissingUrl: 'login 响应缺少 authorizeUrl',
107
+ deviceCodePrompt: '在 GitHub 验证页面输入此验证码:',
108
+ deviceCodeCopy: '复制验证码',
109
+ deviceCodeCopied: '已复制',
110
+ deviceCodeOpenPage: '打开 GitHub 验证页面',
71
111
  usageTitle: '用量',
72
112
  usageRefresh: '刷新',
73
113
  usageLoading: '用量加载中…',
@@ -96,4 +136,36 @@ export const zh = {
96
136
  speedFastDescription: '约 1.5 倍速度,消耗更多用量',
97
137
  commandFast: '切换 Codex 速度档(标准/快速)',
98
138
  commandFastUnavailable: '当前模型不支持快速档;/fast 仅对目录声明了 fast tier 的 Codex 模型可用',
139
+ proxyTitle: '代理',
140
+ proxyStatusNone: '未配置 —— 订阅请求直连。',
141
+ proxyStatusEnabled: '已启用 · {url}',
142
+ proxyStatusError: '配置错误:{message}',
143
+ proxyConfigure: '配置…',
144
+ proxyDialogTitle: '代理设置',
145
+ proxyDialogClose: '关闭',
146
+ proxyEnabled: '让订阅相关请求走代理',
147
+ proxyUrl: '代理地址',
148
+ proxyUrlPlaceholder: 'http://localhost:7890',
149
+ proxyUrlHint: '仅支持 HTTP/HTTPS 代理(Clash/mihomo、v2rayN 等);不支持 socks。',
150
+ proxyUsername: '用户名(可选)',
151
+ proxyUsernamePlaceholder: '代理用户名',
152
+ proxyPassword: '密码',
153
+ proxyPasswordPlaceholder: '留空则保留已保存的密码',
154
+ proxyClearPassword: '清除已保存的密码',
155
+ proxyBypass: '绕过主机',
156
+ proxyBypassPlaceholder: '127.0.0.1, localhost, *.example.com',
157
+ proxyBypassHint: '逗号分隔的主机名,这些主机保持直连。',
158
+ proxyTest: '测试',
159
+ proxyTesting: '测试中…',
160
+ proxyTestOk: '成功 · HTTP {status} · {ms} ms',
161
+ proxyTestOkDirect: '成功(直连,已绕过)· HTTP {status} · {ms} ms',
162
+ proxyTestFail: '失败:{message}',
163
+ proxySave: '保存',
164
+ proxyCancel: '取消',
165
+ proxySaving: '保存中…',
166
+ proxyLoading: '代理设置加载中…',
167
+ proxyLoadFailed: '代理设置加载失败:{message}',
168
+ proxySaved: '已保存 —— 后续请求将走代理。',
169
+ proxySaveFailed: '保存失败:{message}',
170
+ proxyNote: '作用于 token 交换、模型 API、用量查询、图片/视频生成与 x_search。OAuth 授权页在浏览器中打开,走的是浏览器/系统代理,不受此设置影响。',
99
171
  };