@goodandready/dsh-subscriptions 0.6.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -8,7 +8,7 @@ window.__ModuleLoader__.load({
8
8
  try { return require('react-dom').createPortal } catch { return null }
9
9
  })()
10
10
 
11
- // Модульный переводчик: доступен всем подпискам компонентов и хелперам.
11
+ // Modular translator: shared by component subscriptions and helpers.
12
12
  let t = (key) => key
13
13
  const setT = (fn) => { t = fn }
14
14
 
@@ -87,9 +87,9 @@ window.__ModuleLoader__.load({
87
87
  '.dsub-pill:active{transform:translateY(0)}' +
88
88
  '.dsub-pillTag{padding:1px 5px;border-radius:4px;font-size:10.5px;font-weight:700;letter-spacing:.02em;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary)}' +
89
89
  '.dsub-led{width:7px;height:7px;border-radius:50%;flex:none;transition:all .2s ease}' +
90
- '.dsub-ledOk{background:var(--dsw-alias-state-success-primary,#10b981);box-shadow:0 0 6px rgba(16,185,129,.7)}' +
91
- '.dsub-ledWarn{background:var(--dsw-alias-state-warning-primary,#f59e0b);box-shadow:0 0 6px rgba(245,158,11,.7)}' +
92
- '.dsub-ledBad{background:var(--dsw-alias-state-error-primary,#ef4444);box-shadow:0 0 6px rgba(239,68,68,.7)}' +
90
+ '.dsub-ledOk{background:var(--dsw-alias-state-success-primary,#10b981);box-shadow:0 0 6px color-mix(in srgb,var(--dsw-alias-state-success-primary) 70%,transparent)}' +
91
+ '.dsub-ledWarn{background:var(--dsw-alias-state-warning-primary,#f59e0b);box-shadow:0 0 6px color-mix(in srgb,var(--dsw-alias-state-warning-primary) 70%,transparent)}' +
92
+ '.dsub-ledBad{background:var(--dsw-alias-state-error-primary,#ef4444);box-shadow:0 0 6px color-mix(in srgb,var(--dsw-alias-state-error-primary) 70%,transparent)}' +
93
93
  '.dsub-ledOff{background:var(--dsw-alias-label-tertiary,#6b7280)}' +
94
94
  '.dsub-modalWrap{position:fixed;inset:0;z-index:10020;display:grid;place-items:center;background:rgba(0,0,0,.65);backdrop-filter:blur(10px);animation:dsubFadeIn .16s ease-out}' +
95
95
  '.dsub-modal{width:min(520px,94vw);max-height:min(85vh,720px);overflow-y:auto;border:1px solid var(--dsw-alias-border-l1);border-radius:16px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);box-shadow:0 24px 64px rgba(0,0,0,.6),0 0 0 1px rgba(255,255,255,.07);padding:20px;animation:dsubScaleIn .18s cubic-bezier(.16,1,.3,1)}' +
@@ -103,12 +103,16 @@ window.__ModuleLoader__.load({
103
103
  '.dsub-heroHead{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}' +
104
104
  '.dsub-heroTitle{font-size:14px;font-weight:700;display:flex;align-items:center;gap:6px}' +
105
105
  '.dsub-heroModel{font-size:11.5px;color:var(--dsw-alias-label-secondary);font-family:ui-monospace,monospace}' +
106
+ '.dsub-select{appearance:none;height:30px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);border-radius:6px;padding:0 24px 0 8px;font-size:12px;cursor:pointer}' +
107
+ '.dsub-details{margin-top:12px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:8px 12px;background:var(--dsw-alias-bg-layer-2)}' +
108
+ '.dsub-summary{cursor:pointer;font-weight:600;font-size:12.5px;color:var(--dsw-alias-label-secondary);user-select:none}' +
109
+ '.dsub-summary:hover{color:var(--dsw-alias-label-primary)}' +
106
110
  '.dsub-heroBars{display:flex;flex-direction:column;gap:8px;margin-top:10px}' +
107
111
  '.dsub-barLabelRow{display:flex;justify-content:space-between;font-size:11.5px;color:var(--dsw-alias-label-secondary);margin-bottom:3px}' +
108
112
  '.dsub-barTrack{height:6px;border-radius:3px;background:var(--dsw-alias-bg-layer-1);overflow:hidden}' +
109
113
  '.dsub-barFillGrad{height:100%;border-radius:3px;transition:width .3s ease}' +
110
114
  '.dsub-panel-box{margin-top:10px;padding:12px 14px;border-radius:10px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);display:flex;flex-direction:column;gap:8px}' +
111
- '.dsub-panel-warn{border-color:rgba(245,158,11,0.4);background:rgba(245,158,11,0.05)}' +
115
+ '.dsub-panel-warn{border-color:color-mix(in srgb,var(--dsw-alias-state-warning-primary) 40%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-warning-primary) 5%,transparent)}' +
112
116
  '.dsub-panel-title{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:space-between}' +
113
117
  '.dsub-panel-desc{font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.4}' +
114
118
  '.dsub-usage-grid{display:flex;flex-direction:column;gap:8px;margin-top:4px}' +
@@ -117,14 +121,14 @@ window.__ModuleLoader__.load({
117
121
  '.dsub-usage-track{width:100%;height:8px;border-radius:999px;background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l2);overflow:hidden}' +
118
122
  '.dsub-usage-fill{height:100%;border-radius:999px;transition:width .3s ease}' +
119
123
  '.dsub-usage-meta{display:flex;justify-content:space-between;align-items:center;font-size:11px;color:var(--dsw-alias-label-secondary)}' +
120
- '.dsub-tag-ok{border-color:rgba(16,185,129,0.3);color:var(--dsw-alias-state-success-primary);background:rgba(16,185,129,0.08);padding:1px 7px;border-radius:999px;font-size:11px;font-weight:600;border-style:solid;border-width:1px}' +
121
- '.dsub-tag-warn{border-color:rgba(245,158,11,0.4);color:var(--dsw-alias-state-warning-primary);background:rgba(245,158,11,0.08);padding:1px 7px;border-radius:999px;font-size:11px;font-weight:600;border-style:solid;border-width:1px}' +
122
- '.dsub-tag-bad{border-color:rgba(239,68,68,0.4);color:var(--dsw-alias-state-error-primary);background:rgba(239,68,68,0.08);padding:1px 7px;border-radius:999px;font-size:11px;font-weight:600;border-style:solid;border-width:1px}' +
124
+ '.dsub-tag-ok{border-color:color-mix(in srgb,var(--dsw-alias-state-success-primary) 30%,transparent);color:var(--dsw-alias-state-success-primary);background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 8%,transparent);padding:1px 7px;border-radius:999px;font-size:11px;font-weight:600;border-style:solid;border-width:1px}' +
125
+ '.dsub-tag-warn{border-color:color-mix(in srgb,var(--dsw-alias-state-warning-primary) 40%,transparent);color:var(--dsw-alias-state-warning-primary);background:color-mix(in srgb,var(--dsw-alias-state-warning-primary) 8%,transparent);padding:1px 7px;border-radius:999px;font-size:11px;font-weight:600;border-style:solid;border-width:1px}' +
126
+ '.dsub-tag-bad{border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 40%,transparent);color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 8%,transparent);padding:1px 7px;border-radius:999px;font-size:11px;font-weight:600;border-style:solid;border-width:1px}' +
123
127
  '.dsub-slot-meta-row{display:flex;flex-wrap:wrap;align-items:center;gap:10px;margin-top:12px;padding-top:10px;border-top:1px solid var(--dsw-alias-border-l2);font-size:12px}' +
124
128
  '.dsub-ref-tag{display:inline-flex;align-items:center;gap:5px;font-size:11.5px;color:var(--dsw-alias-label-secondary)}' +
125
129
  '.dsub-ref-tag code{padding:2px 6px;border-radius:4px;background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);font-family:ui-monospace,monospace;color:var(--dsw-alias-label-primary);font-size:11px}' +
126
- '.dsub-btn-danger{border-color:rgba(239,68,68,0.4)!important;color:var(--dsw-alias-state-error-primary)!important}' +
127
- '.dsub-btn-danger:hover:not(:disabled){background:rgba(239,68,68,0.12)!important}' +
130
+ '.dsub-btn-danger{border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 40%,transparent)!important;color:var(--dsw-alias-state-error-primary)!important}' +
131
+ '.dsub-btn-danger:hover:not(:disabled){background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 12%,transparent)!important}' +
128
132
  '.dsub-poolTitle{margin-top:16px;margin-bottom:8px;font-size:11.5px;font-weight:700;color:var(--dsw-alias-label-secondary);text-transform:uppercase;letter-spacing:.05em}' +
129
133
  '.dsub-accountCard{display:flex;align-items:center;gap:12px;padding:10px 14px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2);margin-top:6px;transition:border-color .15s,background .15s}' +
130
134
  '.dsub-accountCard:hover{background:var(--dsw-alias-bg-layer-3);border-color:var(--dsw-alias-label-tertiary)}' +
@@ -142,9 +146,9 @@ window.__ModuleLoader__.load({
142
146
  '.dsub-accNameRow{display:flex;align-items:center;justify-content:space-between;gap:8px}' +
143
147
  '.dsub-accName{font-size:13px;font-weight:600}' +
144
148
  '.dsub-accStatusTag{font-size:10.5px;font-weight:600;padding:1px 6px;border-radius:4px}' +
145
- '.dsub-statusOk{background:rgba(16,185,129,.15);color:#34d399}' +
146
- '.dsub-statusWarn{background:rgba(245,158,11,.15);color:#fbbf24}' +
147
- '.dsub-statusBad{background:rgba(239,68,68,.15);color:#f87171}' +
149
+ '.dsub-statusOk{background:color-mix(in srgb,var(--dsw-alias-state-success-primary) 15%,transparent);color:var(--dsw-alias-state-success-primary)}' +
150
+ '.dsub-statusWarn{background:color-mix(in srgb,var(--dsw-alias-state-warning-primary) 15%,transparent);color:var(--dsw-alias-state-warning-primary)}' +
151
+ '.dsub-statusBad{background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 15%,transparent);color:var(--dsw-alias-state-error-primary)}' +
148
152
  '.dsub-statusOff{background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-tertiary)}' +
149
153
  '.dsub-modalFoot{display:flex;align-items:center;justify-content:space-between;margin-top:16px;padding-top:12px;border-top:1px solid var(--dsw-alias-border-l2)}' +
150
154
  '.dsub-btnPrimary{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;border-radius:8px;font-size:12px;font-weight:600;background:var(--dsw-alias-brand-primary,#3b82f6);color:#fff;border:0;cursor:pointer;transition:opacity .15s}' +
@@ -169,9 +173,9 @@ const cssId = 'dsh-subscriptions/settings.module.css'
169
173
  document.head.appendChild(tag)
170
174
  }
171
175
 
172
- // Строки карточки живут в реестре локалей: так их переводит отдельный
173
- // пакет, не трогая код этого плагина. Английский язык по умолчанию,
174
- // на него же приходится откат, если перевода нет.
176
+ // Card strings live in the locale registry: a separate package translates
177
+ // them without touching this plugin's code. English is the default,
178
+ // and the fallback when no translation exists.
175
179
  const NS = 'dsh-subscriptions'
176
180
 
177
181
  const INSTRUCTIONS = {
@@ -308,6 +312,37 @@ const cssId = 'dsh-subscriptions/settings.module.css'
308
312
  'useOriginHint': 'Leave off unless you registered your own OAuth client for this origin. Vendor CLI clients usually require their published redirect URI plus the paste step.',
309
313
  'privacyMask': 'Mask emails and account identifiers',
310
314
  'privacyMaskHint': 'For demos and screen sharing: emails show as j***n@example.com.',
315
+ 'autoLoopback': 'Auto Loopback',
316
+ 'autoLoopbackHint': 'Listen on local callback port to catch OAuth tokens automatically without manual copy-paste.',
317
+ 'hideDeprecated': 'Hide deprecated models',
318
+ 'hideDeprecatedHint': 'Hide older or superseded model variants from model dropdowns.',
319
+ 'composerQuota': 'Composer quota indicator',
320
+ 'composerQuotaHint': 'Display subscription usage indicator directly in the composer toolbar.',
321
+ 'composerQuotaOff': 'Disabled',
322
+ 'composerQuotaPercent': 'Percentage',
323
+ 'composerQuotaBar': 'Progress Bar',
324
+ 'composerQuotaForecast': 'Remaining Forecast',
325
+ 'expiryNotifyDays': 'Expiry warning threshold (days)',
326
+ 'expiryNotifyDaysHint': 'Notify when subscription renewal or token expiration is within this many days (0 to disable).',
327
+ 'codexFastMode': 'Codex fast mode (1.5x)',
328
+ 'codexFastModeHint': 'Enable high-throughput mode for OpenAI Codex models if supported by subscription.',
329
+ 'codexVerbosity': 'Codex streaming verbosity',
330
+ 'codexVerbosityHint': 'Control streaming chunking detail level from the Codex provider.',
331
+ 'codexVerbosityDefault': 'Default',
332
+ 'codexVerbosityLow': 'Low',
333
+ 'codexVerbosityMedium': 'Medium',
334
+ 'codexVerbosityHigh': 'High',
335
+ 'ollamaFallback': 'Ollama local fallback',
336
+ 'ollamaFallbackHint': 'Automatically route queries to a local Ollama instance when cloud quota is exhausted.',
337
+ 'ollamaBaseUrl': 'Ollama Base URL',
338
+ 'ollamaFallbackModel': 'Ollama Fallback Model',
339
+ 'advancedSettings': 'Advanced Settings & Monitoring',
340
+ 'cooldownMs': 'Failure cooldown (minutes)',
341
+ 'cooldownMsHint': 'How long to pause a failing or rate-limited account before retrying.',
342
+ 'probeIntervalMin': 'Health probe interval (minutes)',
343
+ 'probeIntervalMinHint': 'Interval for background checks to refresh account status and quota balance.',
344
+ 'notifyLimits': 'Notify on rate limits',
345
+ 'notifyLimitsHint': 'Show notification alerts when account usage reaches quota ceiling.',
311
346
  'resetCredits': 'Reset credits',
312
347
  'resetCreditsHint': 'ChatGPT reset cards: emergency reset for the 5-hour window. Requires a 5s confirmation.',
313
348
  'resetCreditsAction': 'Reset ChatGPT Limit',
@@ -421,130 +456,6 @@ const cssId = 'dsh-subscriptions/settings.module.css'
421
456
  'windowPrimary': 'Primary window',
422
457
  'windowSecondary': 'Secondary window',
423
458
  }
424
- const ru = {
425
- 'check': 'Проверить',
426
- 'checking': 'Проверяю…',
427
- 'importToken': 'Сохранить',
428
- 'importTokenPlace': 'Вставить токен или API-ключ',
429
- 'importLocalCli': '📥 Из CLI',
430
- 'importLocalCliTitle': 'Автоматически считать токен из установленного на сервере CLI',
431
- 'importLocalSuccess': 'Токен успешно загружен из локального CLI!',
432
- 'howToGetToken': '❓ Инструкция',
433
- 'howToGetTokenTitle': 'Как получить токен или API-ключ для этого провайдера',
434
- 'instructionsTitle': 'Инструкция по получению токена',
435
- 'close': 'Закрыть',
436
- 'reconnectRequired': 'Нужно переподключить',
437
- 'manualTitle': 'Если браузер не вернулся сам',
438
- 'windowPrimary': 'Основное окно',
439
- 'windowSecondary': 'Дополнительное окно',
440
- 'notConnected': 'Не подключено',
441
- 'verifyAccount': 'Требуется проверка',
442
- 'coolingDown': 'Пауза после лимита',
443
- 'usageFull': 'Лимит исчерпан',
444
- 'connected': 'Подключено',
445
- 'title': 'Подписки',
446
- 'cardIntro': 'Аккаунты подписок, ротация и лимиты.',
447
- 'show': 'Показать',
448
- 'hide': 'Скрыть',
449
- 'intro': 'Вход по обычной пользовательской подписке. Токены остаются в хранилище учётных данных харнесса, браузер их обратно не читает. Если после «Подключить» провайдер увёл на localhost или на свою страницу, вставьте сюда адрес перехода или код.',
450
- 'useOrigin': 'Использовать адрес этого веб-интерфейса как redirect_uri',
451
- 'useOriginHint': 'Включайте, только если зарегистрировали собственного клиента OAuth на этот адрес. Штатным консольным клиентам провайдеров нужен их опубликованный адрес возврата и вставка кода вручную.',
452
- 'privacyMask': 'Маскировать email и учётные записи',
453
- 'privacyMaskHint': 'Для демонстрации экрана: email отображается как j***n@example.com.',
454
- 'resetCredits': 'Карты сброса',
455
- 'resetCreditsHint': 'Карты сброса квоты ChatGPT: показывает, сколько доступно. Списание одной карты — осознанное действие с 5-секундным подтверждением.',
456
- 'resetAvailable': 'Доступно попыток сброса',
457
- 'resetExpires': 'ближайшая истекает',
458
- 'resetAck': 'Я понимаю, что будет списана 1 попытка',
459
- 'resetWait': 'кнопка активна через',
460
- 'resetReady': 'готово',
461
- 'resetGo': 'Сбросить',
462
- 'resetBusy': 'сбрасываю…',
463
- 'resetDone': 'квота сброшена',
464
- 'resetNothing': 'сервер сообщает: сброс не нужен (попытка не списана)',
465
- 'resetNoCredit': 'нет пригодной карты (не списано)',
466
- 'resetRedeemed': 'эта карта уже использована ранее',
467
- 'diagGenerate': 'Сгенерировать диагностический отчёт',
468
- 'diagCopy': 'Скопировать отчёт',
469
- 'diagIssues': 'Открыть трекер задач',
470
- 'diagHint': 'В отчёте нет токенов, email, адресов прокси и персональных данных.',
471
- 'subsPill': 'SUBS',
472
- 'subsModalTitle': 'Консоль подписок',
473
- 'subsActiveHero': 'Активная подписка',
474
- 'subsPoolTitle': 'Пул аккаунтов',
475
- 'subsRefresh': 'Обновить',
476
- 'subsOpenSettings': 'Все настройки →',
477
- 'subsFastMode': 'Режим Fast 1.5x',
478
- 'subsHealthy': 'Исправен',
479
- 'subsFree': 'свободно',
480
- 'subsUsed': 'израсходовано',
481
- 'subsModalClose': 'Закрыть',
482
- 'subsLogged': 'подключен',
483
- 'subsNotLogged': 'не подключен',
484
- 'subsCooldown': 'кулдаун',
485
- 'subsOpenSettings': 'Открыть настройки',
486
- 'hudTitle': 'Остаток подписки',
487
- 'settingsLoading': 'Загрузка настроек…',
488
- 'settingsUnavailable': 'Настройки недоступны',
489
- 'settingsRetry': 'Повторить',
490
- 'hudHint': 'перетащи · прилипает к краям · клик — обновить',
491
- 'fcCalibrating': 'калибровка…',
492
- 'fcIdle': 'нет расхода',
493
- 'fcHours': 'ч',
494
- 'fcMinutes': 'мин',
495
- 'reconnect': 'Переподключить',
496
- 'connect': 'Подключить',
497
- 'disconnect': 'Отключить',
498
- 'removeSlot': 'Убрать место',
499
- 'pastePlaceholder': 'Адрес перехода или код',
500
- 'submitCode': 'Отправить код',
501
- 'proxyPlaceholder': 'Прокси аккаунта (http://, https://, socks5://) - необязательно',
502
- 'proxyCheck': 'Проверить прокси',
503
- 'proxyOk': 'прокси ок',
504
- 'proxyFail': 'прокси недоступен',
505
- 'deviceLogin': 'Вход по коду',
506
- 'deviceHint': 'Headless: откройте ссылку, введите код, держите вкладку открытой.',
507
- 'deviceCopy': 'Скопировать код',
508
- 'devicePending': 'Ожидание подтверждения',
509
- 'deviceAuthorized': 'Вход выполнен',
510
- 'deviceExpired': 'Истёк - начните заново',
511
- 'verifyPrefix': 'Google требует однократной проверки аккаунта. ',
512
- 'verifyLink': 'Открыть страницу проверки',
513
- 'verifySuffix': ' затем подключитесь заново.',
514
- 'addAccount': '+ Добавить аккаунт',
515
- 'save': 'Сохранить',
516
- 'saved': 'Сохранено',
517
- 'loading': 'Загрузка…',
518
- 'accountLabel': 'Аккаунт',
519
- 'plan': 'Тариф',
520
- 'storedAs': 'Хранится как',
521
- 'switchLabel': 'Провайдер',
522
- 'chipActive': 'Подписки',
523
- 'expiryLabel': 'окончание',
524
- 'slashLogin': 'Вход в провайдера подписки',
525
- 'slashLogout': 'Выйти из провайдера подписки',
526
- 'slashStatus': 'Статус подписок',
527
- 'none': 'нет',
528
- 'forecast': '≈',
529
- 'resetCredits': 'Сброс лимитов ChatGPT',
530
- 'resetCreditsHint': 'Экстренный сброс 5-часового окна ChatGPT Plus/Pro. Требует 5 секунд подтверждения перед списанием.',
531
- 'resetCreditsAction': 'Сбросить лимит ChatGPT',
532
- 'resetAvailable': 'Доступно сбросов',
533
- 'resetExpires': 'срок действия',
534
- 'resetAck': 'Я понимаю, что одна попытка сброса будет списана',
535
- 'resetWait': 'подтверждение через',
536
- 'resetReady': 'готово к сбросу',
537
- 'resetGo': 'Сбросить лимит сейчас',
538
- 'resetBusy': 'Сброс…',
539
- 'resetDone': 'Лимит успешно сброшен',
540
- 'resetNothing': 'Лимит не требует сброса',
541
- 'resetNoCredit': 'Нет доступных попыток сброса',
542
- 'resetRedeemed': 'Попытка уже использована',
543
- 'windowPrimary': 'Окно 5 часов',
544
- 'windowSecondary': 'Окно 7 дней',
545
- 'usageLimitsTitle': 'Лимиты использования',
546
- 'slotDetailsTitle': 'Параметры слота',
547
- }
548
459
 
549
460
  // #51: live countdown to the quota window reset (account.quota.resetAt).
550
461
  function cleanErrorMessage(raw) {
@@ -633,9 +544,9 @@ const cssId = 'dsh-subscriptions/settings.module.css'
633
544
  return t('connected')
634
545
  }
635
546
 
636
- // Карточка во вкладке «Плагины» рисует свой заголовок и сворачивание:
637
- // ядро даёт только рамку списка.
638
- // Ядровый значок раскрытия. Без защищённого require вместо IconChevronDownOutline14 может упасть вся клиентская половина.
547
+ // The card in the Plugins tab draws its own header and collapse toggle:
548
+ // the core only provides the list frame.
549
+ // Core chevron icon. Without a guarded require, a missing IconChevronDownOutline14 could take down the whole client half.
639
550
  let ChevronIcon = null
640
551
  try {
641
552
  const primitives = require('@deepseek-ai/dsh-client-ui-primitives')
@@ -653,7 +564,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
653
564
 
654
565
 
655
566
  function SubsSection(props) {
656
- // Переводчик приходит от слота, потому что в его записи указан locale.
567
+ // The translator comes from the slot because its registration declares locale.
657
568
  const t = (props && props.t) || ((key) => key)
658
569
  const [draft, setDraft] = React.useState(null)
659
570
  const [accounts, setAccounts] = React.useState([])
@@ -852,7 +763,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
852
763
  setPaste((p) => Object.assign({}, p, { [key]: '' }))
853
764
  }
854
765
 
855
- // #88: проверить прокси аккаунта реальным запросом с замером задержки.
766
+ // #88: probe the account proxy with a real request and measure latency.
856
767
  const doProxyCheck = async (provider, index) => {
857
768
  const key = provider + ':' + index
858
769
  setChecking((c) => Object.assign({}, c, { ['proxy:' + key]: true }))
@@ -971,6 +882,146 @@ const cssId = 'dsh-subscriptions/settings.module.css'
971
882
  ),
972
883
  React.createElement('div', { className: 'dsub-sub' },
973
884
  t('privacyMaskHint')),
885
+ React.createElement('label', { className: 'dsub-row' },
886
+ React.createElement('input', {
887
+ type: 'checkbox',
888
+ checked: !!draft.autoLoopback,
889
+ onChange: (e) => setDraft((d) => Object.assign({}, d, { autoLoopback: e.target.checked })),
890
+ }),
891
+ React.createElement('span', null, t('autoLoopback')),
892
+ ),
893
+ React.createElement('div', { className: 'dsub-sub' },
894
+ t('autoLoopbackHint')),
895
+ React.createElement('label', { className: 'dsub-row' },
896
+ React.createElement('input', {
897
+ type: 'checkbox',
898
+ checked: !!draft.hideDeprecatedModels,
899
+ onChange: (e) => setDraft((d) => Object.assign({}, d, { hideDeprecatedModels: e.target.checked })),
900
+ }),
901
+ React.createElement('span', null, t('hideDeprecated')),
902
+ ),
903
+ React.createElement('div', { className: 'dsub-sub' },
904
+ t('hideDeprecatedHint')),
905
+ React.createElement('div', { className: 'dsub-row' },
906
+ React.createElement('span', { className: 'dsub-h' }, t('composerQuota')),
907
+ React.createElement('select', {
908
+ className: 'dsub-select',
909
+ value: draft.composerQuota || 'percent',
910
+ onChange: (e) => setDraft((d) => Object.assign({}, d, { composerQuota: e.target.value })),
911
+ },
912
+ React.createElement('option', { value: 'off' }, t('composerQuotaOff')),
913
+ React.createElement('option', { value: 'percent' }, t('composerQuotaPercent')),
914
+ React.createElement('option', { value: 'bar' }, t('composerQuotaBar')),
915
+ React.createElement('option', { value: 'forecast' }, t('composerQuotaForecast')),
916
+ ),
917
+ ),
918
+ React.createElement('div', { className: 'dsub-sub' },
919
+ t('composerQuotaHint')),
920
+ React.createElement('div', { className: 'dsub-row' },
921
+ React.createElement('span', { className: 'dsub-h' }, t('expiryNotifyDays')),
922
+ React.createElement('input', {
923
+ type: 'number',
924
+ className: 'dsub-inp',
925
+ style: { width: '80px', minWidth: '80px' },
926
+ min: 0,
927
+ max: 90,
928
+ value: draft.expiryNotifyDays != null ? draft.expiryNotifyDays : 7,
929
+ onChange: (e) => setDraft((d) => Object.assign({}, d, { expiryNotifyDays: Number(e.target.value) || 0 })),
930
+ }),
931
+ ),
932
+ React.createElement('div', { className: 'dsub-sub' },
933
+ t('expiryNotifyDaysHint')),
934
+ React.createElement('label', { className: 'dsub-row' },
935
+ React.createElement('input', {
936
+ type: 'checkbox',
937
+ checked: !!draft.codexFastMode,
938
+ onChange: (e) => setDraft((d) => Object.assign({}, d, { codexFastMode: e.target.checked })),
939
+ }),
940
+ React.createElement('span', null, t('codexFastMode')),
941
+ ),
942
+ React.createElement('div', { className: 'dsub-sub' },
943
+ t('codexFastModeHint')),
944
+ React.createElement('div', { className: 'dsub-row' },
945
+ React.createElement('span', { className: 'dsub-h' }, t('codexVerbosity')),
946
+ React.createElement('select', {
947
+ className: 'dsub-select',
948
+ value: draft.codexVerbosity || 'default',
949
+ onChange: (e) => setDraft((d) => Object.assign({}, d, { codexVerbosity: e.target.value })),
950
+ },
951
+ React.createElement('option', { value: 'default' }, t('codexVerbosityDefault')),
952
+ React.createElement('option', { value: 'low' }, t('codexVerbosityLow')),
953
+ React.createElement('option', { value: 'medium' }, t('codexVerbosityMedium')),
954
+ React.createElement('option', { value: 'high' }, t('codexVerbosityHigh')),
955
+ ),
956
+ ),
957
+ React.createElement('div', { className: 'dsub-sub' },
958
+ t('codexVerbosityHint')),
959
+ React.createElement('label', { className: 'dsub-row' },
960
+ React.createElement('input', {
961
+ type: 'checkbox',
962
+ checked: !!draft.ollamaFallback,
963
+ onChange: (e) => setDraft((d) => Object.assign({}, d, { ollamaFallback: e.target.checked })),
964
+ }),
965
+ React.createElement('span', null, t('ollamaFallback')),
966
+ ),
967
+ React.createElement('div', { className: 'dsub-sub' },
968
+ t('ollamaFallbackHint')),
969
+ draft.ollamaFallback ? React.createElement('div', { className: 'dsub-row' },
970
+ React.createElement('input', {
971
+ type: 'text',
972
+ className: 'dsub-inp dsub-grow',
973
+ placeholder: 'http://localhost:11434',
974
+ value: draft.ollamaBaseUrl || '',
975
+ onChange: (e) => setDraft((d) => Object.assign({}, d, { ollamaBaseUrl: e.target.value })),
976
+ }),
977
+ React.createElement('input', {
978
+ type: 'text',
979
+ className: 'dsub-inp dsub-grow',
980
+ placeholder: 'llama3:latest',
981
+ value: draft.ollamaFallbackModel || '',
982
+ onChange: (e) => setDraft((d) => Object.assign({}, d, { ollamaFallbackModel: e.target.value })),
983
+ }),
984
+ ) : null,
985
+ React.createElement('details', { className: 'dsub-details' },
986
+ React.createElement('summary', { className: 'dsub-summary' }, t('advancedSettings')),
987
+ React.createElement('div', { className: 'dsub-block' },
988
+ React.createElement('div', { className: 'dsub-row' },
989
+ React.createElement('span', { className: 'dsub-h' }, t('cooldownMs')),
990
+ React.createElement('input', {
991
+ type: 'number',
992
+ className: 'dsub-inp',
993
+ style: { width: '80px', minWidth: '80px' },
994
+ min: 1,
995
+ max: 120,
996
+ value: Math.round((draft.cooldownMs || 600000) / 60000),
997
+ onChange: (e) => setDraft((d) => Object.assign({}, d, { cooldownMs: (Number(e.target.value) || 10) * 60000 })),
998
+ }),
999
+ ),
1000
+ React.createElement('div', { className: 'dsub-sub' }, t('cooldownMsHint')),
1001
+ React.createElement('div', { className: 'dsub-row' },
1002
+ React.createElement('span', { className: 'dsub-h' }, t('probeIntervalMin')),
1003
+ React.createElement('input', {
1004
+ type: 'number',
1005
+ className: 'dsub-inp',
1006
+ style: { width: '80px', minWidth: '80px' },
1007
+ min: 1,
1008
+ max: 1440,
1009
+ value: draft.probeIntervalMin != null ? draft.probeIntervalMin : 15,
1010
+ onChange: (e) => setDraft((d) => Object.assign({}, d, { probeIntervalMin: Number(e.target.value) || 15 })),
1011
+ }),
1012
+ ),
1013
+ React.createElement('div', { className: 'dsub-sub' }, t('probeIntervalMinHint')),
1014
+ React.createElement('label', { className: 'dsub-row' },
1015
+ React.createElement('input', {
1016
+ type: 'checkbox',
1017
+ checked: draft.notifyLimits !== false,
1018
+ onChange: (e) => setDraft((d) => Object.assign({}, d, { notifyLimits: e.target.checked })),
1019
+ }),
1020
+ React.createElement('span', null, t('notifyLimits')),
1021
+ ),
1022
+ React.createElement('div', { className: 'dsub-sub' }, t('notifyLimitsHint')),
1023
+ ),
1024
+ ),
974
1025
  ),
975
1026
  React.createElement('div', { className: 'dsub-block' },
976
1027
  React.createElement('div', { className: 'dsub-row' },
@@ -1331,8 +1382,8 @@ const cssId = 'dsh-subscriptions/settings.module.css'
1331
1382
  )
1332
1383
  }
1333
1384
 
1334
- // Карточка во вкладке «Плагины» как карточки bash/agent-loop/web-search:
1335
- // заголовок с именем, описанием и шевроном, сворачивание, каёмка.
1385
+ // The card in the Plugins tab follows the bash/agent-loop/web-search cards:
1386
+ // titled header with description and chevron, collapsible, bordered.
1336
1387
  function PluginCard(props) {
1337
1388
  const t = (props && props.t) || ((key) => key)
1338
1389
  const [open, setOpen] = React.useState(false)
@@ -1358,14 +1409,14 @@ const cssId = 'dsh-subscriptions/settings.module.css'
1358
1409
  }
1359
1410
 
1360
1411
  function registerSettings(ctx) {
1361
- // Язык может принести не только плагин: словарные пакеты объявляют
1362
- // русский для чужих пространств. Ядро на повторное объявление той же
1363
- // пары «пространство + язык» бросает исключение, и незащищённый вызов
1364
- // уносил с собой весь плагин в интерфейсе это выглядело как «Failed to
1365
- // load plugins» с перечнем ни в чём не повинных соседей.
1412
+ // Languages can come from more than this plugin: dictionary packages declare
1413
+ // Russian for foreign namespaces. The core throws on re-declaring the same
1414
+ // namespace+language pair, and an unguarded call used to take down the
1415
+ // whole plugin - in the UI it surfaced as Failed to
1416
+ // load plugins listing perfectly innocent neighbours.
1366
1417
  //
1367
- // Поэтому каждый язык объявляется отдельно и по-хорошему: заняли до нас
1368
- // уступаем, свой английский при этом всё равно встаёт на место.
1418
+ // So each language is declared separately and politely: if someone got
1419
+ // there first we yield, while our own English still lands.
1369
1420
  const addLocale = (locale, dictionary) => {
1370
1421
  try {
1371
1422
  return ctx.locale.register(NS, locale, dictionary)
@@ -1374,17 +1425,16 @@ const cssId = 'dsh-subscriptions/settings.module.css'
1374
1425
  }
1375
1426
  }
1376
1427
  ctx.effect(() => {
1377
- const undo = [addLocale('en', en), addLocale('ru', ru)]
1428
+ const undo = [addLocale('en', en)]
1378
1429
  return () => { for (const off of undo) off() }
1379
1430
  }, 'dsh-subscriptions: словари')
1380
- // Подписи вне компонента берут переводчик, привязанный к namespace.
1431
+ // Labels outside a component take the translator bound to the namespace.
1381
1432
  setT(ctx.locale.bind(NS))
1382
- // Штатное место вкладка «Плагины»: карточка со своим заголовком и
1383
- // сворачиванием вместо строки в боковом списке. Ключ регистрации обязан
1384
- // равняться пространству настроек, иначе вкладка молча не покажет слот.
1385
- let moved = false
1433
+ // The canonical place is the Plugins tab: a card with its own header
1434
+ // and collapse instead of a row in the side list. The registration key must
1435
+ // equal the settings namespace, otherwise the tab silently hides the slot.
1386
1436
  try {
1387
- moved = !!ctx.slots.inject('settings.plugin.item', () => ctx.slots.register(
1437
+ ctx.slots.inject('settings.plugin.item', () => ctx.slots.register(
1388
1438
  {
1389
1439
  name: 'settings.plugin.item',
1390
1440
  key: NS,
@@ -1393,32 +1443,13 @@ const cssId = 'dsh-subscriptions/settings.module.css'
1393
1443
  },
1394
1444
  PluginCard,
1395
1445
  ))
1396
- } catch { moved = false }
1397
- if (moved) return
1398
- // #244: Fallback for harness cores before settings.plugin.item slot was
1399
- // introduced (< 0.1.2-rc.1). Once all production profiles run a core
1400
- // that has settings.plugin.item, this block can be safely removed.
1401
- // Tracked in Gitea #244 — do not remove without verifying minimum core.
1402
- try {
1403
- ctx.slots.inject('settings.section', () => ctx.slots.register(
1404
- {
1405
- name: 'settings.section',
1406
- id: '@goodandready/dsh-subscriptions',
1407
- order: 28,
1408
- locale: NS,
1409
- label: () => t('title'),
1410
- inject: () => ({ ctx: ctx }),
1411
- },
1412
- SubsSection,
1413
- ))
1414
1446
  } catch (e) {
1415
- // Neither slot system available — settings panel will not render.
1416
- // This is expected on headless / non-web profiles.
1447
+ // Slot system not available — expected on headless / non-web profiles.
1417
1448
  }
1418
1449
  }
1419
1450
 
1420
- // Горячий переключатель провайдера в строке композера: клик циклично
1421
- // меняет активного провайдера подписки для следующего запроса.
1451
+ // Hot provider switcher in the composer row: a click cycles
1452
+ // the active subscription provider for the next request.
1422
1453
  let activeIdx = 0
1423
1454
  let labelEl = null
1424
1455
  const ORDER = ['codex', 'claude', 'grok', 'antigravity']
@@ -1443,8 +1474,8 @@ const cssId = 'dsh-subscriptions/settings.module.css'
1443
1474
  } catch { return { logged: [], usage: 0, expiresAt: {}, labels: {}, expiryNotifyDays: 7, fastMode: false, composerQuota: 'off', active: null } }
1444
1475
  }
1445
1476
 
1446
- // #84: компактный индикатор квоты активной подписки в области ввода.
1447
- // Прогноз (runway) считается по скользящему окну сэмплов /status.
1477
+ // #84: compact quota indicator of the active subscription in the input area.
1478
+ // The runway forecast uses a rolling window of /status samples.
1448
1479
  const fcSamples = { windows: {} }
1449
1480
 
1450
1481
  function fcObserve(key, remaining, now) {
@@ -1528,8 +1559,8 @@ const cssId = 'dsh-subscriptions/settings.module.css'
1528
1559
  return React.createElement('span', { className: 'dsub-cq', title: t('forecast') + ' · ' + state.active.provider }, value)
1529
1560
  }
1530
1561
 
1531
- // #83: пилл SUBS(N) в шапке сессии со светодиодом здоровья пула и
1532
- // модальной консолью аккаунтов (закрытие: крестик, Escape, клик мимо).
1562
+ // #83: SUBS(N) pill in the session header with a pool health LED and
1563
+ // a modal accounts console (close: X button, Escape, outside click).
1533
1564
  function brandBadge(prov) {
1534
1565
  const p = String(prov || '').toLowerCase()
1535
1566
  if (p.includes('codex') || p.includes('chatgpt') || p.includes('openai')) return { label: 'OpenAI', cls: 'dsub-brandCodex', icon: '⚡' }
@@ -1617,7 +1648,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
1617
1648
  React.createElement('div', { className: 'dsub-heroTitle' },
1618
1649
  React.createElement('span', { className: 'dsub-brandBadge ' + b.cls, style: { width: 24, height: 24, fontSize: 11 } }, b.icon),
1619
1650
  React.createElement('span', null, (a.provider ? a.provider.toUpperCase() : 'LLM') + (a.index ? ' #' + a.index : '')),
1620
- a.fastMode ? React.createElement('span', { className: 'dsub-pillTag', style: { background: 'rgba(245,158,11,.15)', color: '#fbbf24' } }, '⚡ FAST 1.5x') : null,
1651
+ a.fastMode ? React.createElement('span', { className: 'dsub-pillTag', style: { background: 'color-mix(in srgb,var(--dsw-alias-state-warning-primary) 15%,transparent)', color: 'var(--dsw-alias-state-warning-primary)' } }, '⚡ FAST 1.5x') : null,
1621
1652
  ),
1622
1653
  React.createElement('span', { className: 'dsub-heroModel' }, a.model || 'active model'),
1623
1654
  ),
@@ -1681,7 +1712,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
1681
1712
  React.createElement('div', { className: 'dsub-accNameRow' },
1682
1713
  React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6 } },
1683
1714
  React.createElement('span', { className: 'dsub-accName' }, a.provider.toUpperCase() + (a.index ? ' #' + a.index : '')),
1684
- isActiveThis ? React.createElement('span', { className: 'dsub-pillTag', style: { background: 'rgba(16,185,129,.15)', color: '#34d399' } }, 'ACTIVE') : null,
1715
+ isActiveThis ? React.createElement('span', { className: 'dsub-pillTag', style: { background: 'color-mix(in srgb,var(--dsw-alias-state-success-primary) 15%,transparent)', color: 'var(--dsw-alias-state-success-primary)' } }, 'ACTIVE') : null,
1685
1716
  a.proxy ? React.createElement('span', { className: 'dsub-dim', title: a.proxy }, '🌐 proxy') : null,
1686
1717
  ),
1687
1718
  React.createElement('span', { className: 'dsub-accStatusTag ' + statusClass }, statusText),
package/lib/crypto.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { scryptSync, createCipheriv, createDecipheriv, randomBytes } from "node:crypto"
2
2
 
3
- // AES-256-GCM с ключом из passphrase (scrypt, без хранения соли отдельно
4
- // соль в начале блока).
3
+ // AES-256-GCM with a passphrase-derived key (scrypt, no separately stored salt -
4
+ // the salt is stored at the head of the block).
5
5
  export function encryptWithPassphrase(plainText, passphrase) {
6
6
  const salt = randomBytes(16)
7
7
  const key = scryptSync(String(passphrase), salt, 32)
@@ -9,13 +9,13 @@ export function encryptWithPassphrase(plainText, passphrase) {
9
9
  const cipher = createCipheriv("aes-256-gcm", key, iv)
10
10
  const encrypted = Buffer.concat([cipher.update(String(plainText), "utf8"), cipher.final()])
11
11
  const tag = cipher.getAuthTag()
12
- // формат: magic + salt + iv + tag + data
12
+ // format: magic + salt + iv + tag + data
13
13
  return "DSHE1:" + Buffer.concat([salt, iv, tag, encrypted]).toString("base64")
14
14
  }
15
15
 
16
16
  export function decryptWithPassphrase(payload, passphrase) {
17
17
  const raw = String(payload || "")
18
- if (!raw.startsWith("DSHE1:")) throw new Error("неизвестный формат экспорта")
18
+ if (!raw.startsWith("DSHE1:")) throw new Error("unknown export format")
19
19
  const buf = Buffer.from(raw.slice(6), "base64")
20
20
  const salt = buf.subarray(0, 16)
21
21
  const iv = buf.subarray(16, 28)
@@ -121,7 +121,7 @@ export function googleLicenseMessage(status, bodyText) {
121
121
  const api = parseGoogleApiError(bodyText)
122
122
  const msg = String(api?.message || bodyText || '')
123
123
  if (/do not have a valid license|#3501|PERMISSION_DENIED/i.test(msg)) {
124
- return 'У аккаунта Google нет активной лицензии Gemini Code Assist / Antigravity (#3501), либо не привязан проект Google Cloud. Проверьте подписку на https://one.google.com или подключите другой Google аккаунт.'
124
+ return 'Google account has no active Gemini Code Assist / Antigravity license (#3501), or no Google Cloud project is attached. Check the subscription at https://one.google.com or connect a different Google account.'
125
125
  }
126
126
  return ''
127
127
  }
package/lib/health.js CHANGED
@@ -1,5 +1,5 @@
1
- // Health-счёт аккаунта для умной ротации.
2
- // Числа как в dsh-key-rotation pool.js: свитч 5, исчерпание 10, битый ключ 15.
1
+ // Per-account health score for smart rotation.
2
+ // Numbers mirror dsh-key-rotation pool.js: switch 5, exhaustion 10, bad key 15.
3
3
  const PENALTY = { switch: 5, exhaust: 10, broken: 15 }
4
4
 
5
5
  export function emptyHealth() {