@goodandready/dsh-subscriptions 0.4.18 → 0.4.20

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 (2) hide show
  1. package/lib/client.js +248 -416
  2. package/package.json +1 -1
package/lib/client.js CHANGED
@@ -13,7 +13,6 @@ window.__ModuleLoader__.load({
13
13
  const setT = (fn) => { t = fn }
14
14
 
15
15
  const CSS =
16
- // card — same structure as core PluginCard (bash, agent-loop, web-search)
17
16
  '.dsub-card{list-style:none;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;transition:border-color .16s,background .16s}' +
18
17
  '.dsub-card:hover{border-color:var(--dsw-alias-label-dimmed)}' +
19
18
  '.dsub-cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}' +
@@ -24,38 +23,22 @@ window.__ModuleLoader__.load({
24
23
  '.dsub-description{color:var(--dsw-alias-label-secondary);font-size:13px}' +
25
24
  '.dsub-chev{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}' +
26
25
  '.dsub-chevOpen{transform:rotate(180deg)}' +
27
- '.dsub-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}' +
28
- // existing dsub classes kept for SubsSection internals
29
- '.dsub-wrap{display:flex;flex-direction:column;gap:22px;padding:4px 0;max-width:760px}' +
30
- '.dsub-block{display:flex;flex-direction:column;gap:10px}' +
31
- '.dsub-h{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary)}' +
26
+ '.dsub-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:12px}' +
27
+ '.dsub-block{margin-top:10px}' +
28
+ '.dsub-row{display:flex;align-items:center;gap:8px;margin-top:8px}' +
29
+ '.dsub-h{font-size:13.5px;font-weight:600;color:var(--dsw-alias-label-primary)}' +
32
30
  '.dsub-sub{font-size:12px;color:var(--dsw-alias-label-secondary)}' +
33
- '.dsub-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}' +
34
- '.dsub-row input,.dsub-field input,.dsub-row select{background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-primary);border-radius:6px;padding:6px 8px;font-size:13px}' +
35
- '.dsub-grow{flex:1;min-width:180px}' +
36
- '.dsub-field{display:flex;flex-direction:column;gap:6px;padding:12px 0}' +
37
- '.dsub-input{height:34px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px}' +
38
- '.dsub-mini{border:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-primary);border-radius:6px;height:28px;padding:0 8px;cursor:pointer}' +
39
- '.dsub-foot{border-top:1px solid var(--dsw-alias-border-l2);display:flex;justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px}' +
40
- '.dsub-save{appearance:none;font:inherit;cursor:pointer;border:1px solid transparent;border-radius:8px;padding:5px 14px;font-size:13px;background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}' +
31
+ '.dsub-dim{font-size:11.5px;color:var(--dsw-alias-label-tertiary)}' +
32
+ '.dsub-mini{appearance:none;font:inherit;font-size:12px;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:6px;padding:3px 8px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);transition:all .15s}' +
33
+ '.dsub-mini:hover{background:var(--dsw-alias-bg-layer-1);border-color:var(--dsw-alias-label-dimmed)}' +
41
34
  '.dsub-ok{font-size:12px;color:var(--dsw-alias-state-success-primary)}' +
42
35
  '.dsub-bad{font-size:12px;color:var(--dsw-alias-state-error-primary)}' +
43
- '.dsub-badge{font-size:11px;padding:2px 8px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);white-space:nowrap}' +
44
- '.dsub-badge-on{color:var(--dsw-alias-state-success-primary);border-color:currentColor}' +
45
- '.dsub-badge-warn{color:var(--dsw-alias-state-warning-primary);border-color:currentColor}' +
46
- '.dsub-chip{display:inline-flex;align-items:center;gap:5px;height:26px;padding:0 10px;border-radius:999px;font-size:12px;font-weight:600;line-height:1;white-space:nowrap;color:var(--dsw-alias-label-secondary);border:1px solid var(--dsw-alias-border-l2)}' +
47
- '.dsub-chipOn{color:var(--dsw-alias-state-success-primary);border-color:currentColor}' +
48
- '.dsub-chipWarn{color:var(--dsw-alias-state-warning-primary);border-color:currentColor}' +
49
- '.dsub-chipDanger{color:var(--dsw-alias-state-error-primary);border-color:currentColor}' +
50
- '.dsub-chipDot{width:6px;height:6px;border-radius:999px;background:currentColor;flex:none}' +
51
- '.dsub-chipActive{display:inline-flex;align-items:center;gap:6px;height:26px;padding:0 10px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);font-size:12px;font-weight:600;line-height:1;white-space:nowrap;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-3)}' +
52
- '.dsub-chipActiveWarn{color:var(--dsw-alias-state-warning-primary);border-color:currentColor}' +
53
- '.dsub-chipActiveDanger{color:var(--dsw-alias-state-error-primary);border-color:currentColor}' +
54
- '.dsub-chipActiveOk{color:var(--dsw-alias-state-success-primary);border-color:currentColor}' +
55
- '.dsub-chipActiveBar{height:3px;width:24px;background:var(--dsw-alias-bg-layer-1);border-radius:2px;flex:none;overflow:hidden}' +
56
- '.dsub-chipActiveFill{height:100%;border-radius:2px;background:currentColor}' +
57
-
58
- '.dsub-link{background:none;border:none;color:var(--dsw-alias-label-secondary);cursor:pointer;font-size:12px;padding:0}' +
36
+ '.dsub-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:5px 12px;font-size:12.5px;font-weight:500;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);transition:all .15s}' +
37
+ '.dsub-btn:hover{background:var(--dsw-alias-bg-layer-1);border-color:var(--dsw-alias-label-dimmed)}' +
38
+ '.dsub-inp{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 8px;font-size:12px;min-width:180px}' +
39
+ '.dsub-inp:focus{outline:none;border-color:var(--dsw-alias-brand-primary)}' +
40
+ '.dsub-box{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:10px;margin-top:8px;background:var(--dsw-alias-bg-layer-2)}' +
41
+ '.dsub-cooldown{display:inline-block;padding:2px 6px;border-radius:4px;background:var(--dsw-alias-state-warning-primary);color:#000;font-size:11px;font-weight:600}' +
59
42
  '.dsub-bar{position:relative;height:6px;border-radius:3px;background:var(--dsw-alias-bg-layer-1);overflow:hidden;flex:1;min-width:80px}' +
60
43
  '.dsub-barFill{height:100%;border-radius:3px;background:var(--dsw-alias-state-success-primary);transition:width .2s}' +
61
44
  '.dsub-barWarn .dsub-barFill{background:var(--dsw-alias-state-warning-primary)}' +
@@ -64,40 +47,53 @@ window.__ModuleLoader__.load({
64
47
  '.dsub-manual{display:flex;flex-direction:column;gap:6px;margin-top:4px;padding-top:10px;border-top:1px solid var(--dsw-alias-border-l2)}' +
65
48
  '.dsub-verify{font-size:12px;color:var(--dsw-alias-state-warning-primary)}' +
66
49
  '.dsub-verify a{color:var(--dsw-alias-brand-primary)}' +
67
- '.dsub-hud{pointer-events:auto;position:fixed;z-index:10010;width:64px;height:64px;transition:left .2s,top .2s}' +
68
- '.dsub-hud[data-dragging=true]{transition:none}' +
69
- '.dsub-hudBtn{position:relative;width:64px;height:64px;padding:0;border:0;border-radius:50%;cursor:grab;display:grid;place-items:center;user-select:none;touch-action:none;' +
70
- 'background:var(--dsw-alias-bg-layer-3);box-shadow:0 0 0 1px var(--dsw-alias-border-l2),0 8px 24px rgba(0,0,0,.25);backdrop-filter:blur(18px)}' +
71
- '.dsub-hudBtn:hover{transform:scale(1.05)}' +
72
- '.dsub-hudBtn:active{cursor:grabbing}' +
73
- '.dsub-hudRing{position:absolute;inset:4px;width:56px;height:56px;transform:rotate(-90deg)}' +
74
- '.dsub-hudTrack{fill:none;stroke:var(--dsw-alias-border-l2);stroke-width:4}' +
75
- '.dsub-hudVal{fill:none;stroke:var(--dsub-tone,var(--dsw-alias-state-success-primary));stroke-width:4;stroke-linecap:round;transition:stroke-dashoffset .4s}' +
76
- '.dsub-hudCore{position:relative;z-index:2;font-size:14px;font-weight:700;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-primary)}' +
77
- '.dsub-hud[data-collapsed=true] .dsub-hudBtn{opacity:.45}' +
78
- '.dsub-hud[data-collapsed=true]:hover .dsub-hudBtn{opacity:1}' +
79
- '.dsub-hudPanel{pointer-events:none;position:absolute;width:280px;padding:12px;border-radius:14px;background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l2);box-shadow:0 18px 48px rgba(0,0,0,.35);backdrop-filter:blur(28px);opacity:0;transform:translateY(6px);transition:opacity .16s,transform .16s}' +
80
- '.dsub-hud:hover .dsub-hudPanel{opacity:1;transform:none}' +
81
- '.dsub-hud[data-dragging=true] .dsub-hudPanel{opacity:0}' +
82
- '.dsub-hudRow{display:flex;align-items:center;gap:8px;margin-top:8px;font-size:11.5px}' +
83
- '.dsub-hudRow .dsub-bar{min-width:70px}' +
84
- '.dsub-hudName{font-weight:600;min-width:52px}' +
85
- '.dsub-hudPct{margin-left:auto;font-variant-numeric:tabular-nums}' +
86
- '.dsub-hudHead{font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary)}' +
87
- '.dsub-hudHint{margin-top:10px;font-size:10.5px;color:var(--dsw-alias-label-tertiary)}' +
88
- '.dsub-pill{display:inline-flex;align-items:center;gap:6px;height:26px;padding:0 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:999px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-size:11.5px;font-weight:600;cursor:pointer;letter-spacing:.06em}' +
89
- '.dsub-pill:hover{border-color:var(--dsw-alias-label-dimmed)}' +
90
- '.dsub-led{width:7px;height:7px;border-radius:50%;flex:none}' +
91
- '.dsub-ledOk{background:var(--dsw-alias-state-success-primary);box-shadow:0 0 6px var(--dsw-alias-state-success-primary)}' +
92
- '.dsub-ledWarn{background:var(--dsw-alias-state-warning-primary);box-shadow:0 0 6px var(--dsw-alias-state-warning-primary)}' +
93
- '.dsub-ledBad{background:var(--dsw-alias-state-error-primary);box-shadow:0 0 6px var(--dsw-alias-state-error-primary)}' +
94
- '.dsub-ledOff{background:var(--dsw-alias-label-tertiary)}' +
95
- '.dsub-modalWrap{position:fixed;inset:0;z-index:10020;display:grid;place-items:center;background:color-mix(in srgb,#000 45%,transparent)}' +
96
- '.dsub-modal{width:min(560px,92vw);max-height:min(70vh,640px);overflow:auto;border:1px solid var(--dsw-alias-border-l1);border-radius:14px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);box-shadow:0 24px 64px rgba(0,0,0,.4);padding:16px}' +
97
- '.dsub-modalHead{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:12px}' +
98
- '.dsub-modalRow{display:flex;align-items:center;gap:10px;padding:9px 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;margin-top:8px;background:var(--dsw-alias-bg-layer-2)}' +
99
- '.dsub-modalName{font-weight:600;font-size:13px}' +
100
- '.dsub-modalSub{font-size:11.5px;color:var(--dsw-alias-label-secondary)}' +
50
+ '.dsub-pill{display:inline-flex;align-items:center;gap:7px;height:28px;padding:0 11px;border:1px solid var(--dsw-alias-border-l2);border-radius:999px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-size:12px;font-weight:600;cursor:pointer;box-shadow:0 1px 3px rgba(0,0,0,.15),inset 0 1px 0 rgba(255,255,255,.06);transition:all .16s ease;user-select:none}' +
51
+ '.dsub-pill:hover{background:var(--dsw-alias-bg-layer-3);border-color:var(--dsw-alias-label-tertiary);transform:translateY(-1px);box-shadow:0 3px 8px rgba(0,0,0,.25),inset 0 1px 0 rgba(255,255,255,.1)}' +
52
+ '.dsub-pill:active{transform:translateY(0)}' +
53
+ '.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)}' +
54
+ '.dsub-led{width:7px;height:7px;border-radius:50%;flex:none;transition:all .2s ease}' +
55
+ '.dsub-ledOk{background:var(--dsw-alias-state-success-primary,#10b981);box-shadow:0 0 6px rgba(16,185,129,.7)}' +
56
+ '.dsub-ledWarn{background:var(--dsw-alias-state-warning-primary,#f59e0b);box-shadow:0 0 6px rgba(245,158,11,.7)}' +
57
+ '.dsub-ledBad{background:var(--dsw-alias-state-error-primary,#ef4444);box-shadow:0 0 6px rgba(239,68,68,.7)}' +
58
+ '.dsub-ledOff{background:var(--dsw-alias-label-tertiary,#6b7280)}' +
59
+ '.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}' +
60
+ '.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)}' +
61
+ '@keyframes dsubFadeIn{from{opacity:0}to{opacity:1}}' +
62
+ '@keyframes dsubScaleIn{from{opacity:0;transform:scale(.96)}to{opacity:1;transform:scale(1)}}' +
63
+ '.dsub-modalHead{display:flex;align-items:center;justify-content:space-between;gap:12px;padding-bottom:14px;border-bottom:1px solid var(--dsw-alias-border-l2)}' +
64
+ '.dsub-modalTitleWrap{display:flex;align-items:center;gap:10px}' +
65
+ '.dsub-modalIcon{width:30px;height:30px;border-radius:8px;background:var(--dsw-alias-bg-layer-1);display:grid;place-items:center;color:var(--dsw-alias-brand-primary);font-size:14px}' +
66
+ '.dsub-modalBadge{font-size:11px;font-weight:600;padding:2px 7px;border-radius:10px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary)}' +
67
+ '.dsub-heroCard{margin-top:14px;padding:14px 16px;border-radius:12px;background:linear-gradient(135deg,var(--dsw-alias-bg-layer-2) 0%,var(--dsw-alias-bg-layer-3) 100%);border:1px solid var(--dsw-alias-border-l1);box-shadow:inset 0 1px 0 rgba(255,255,255,.05)}' +
68
+ '.dsub-heroHead{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}' +
69
+ '.dsub-heroTitle{font-size:14px;font-weight:700;display:flex;align-items:center;gap:6px}' +
70
+ '.dsub-heroModel{font-size:11.5px;color:var(--dsw-alias-label-secondary);font-family:ui-monospace,monospace}' +
71
+ '.dsub-heroBars{display:flex;flex-direction:column;gap:8px;margin-top:10px}' +
72
+ '.dsub-barLabelRow{display:flex;justify-content:space-between;font-size:11.5px;color:var(--dsw-alias-label-secondary);margin-bottom:3px}' +
73
+ '.dsub-barTrack{height:6px;border-radius:3px;background:var(--dsw-alias-bg-layer-1);overflow:hidden}' +
74
+ '.dsub-barFillGrad{height:100%;border-radius:3px;transition:width .3s ease}' +
75
+ '.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}' +
76
+ '.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}' +
77
+ '.dsub-accountCard:hover{background:var(--dsw-alias-bg-layer-3);border-color:var(--dsw-alias-label-tertiary)}' +
78
+ '.dsub-brandBadge{width:32px;height:32px;border-radius:8px;display:grid;place-items:center;font-size:12px;font-weight:700;flex-shrink:0}' +
79
+ '.dsub-brandCodex{background:rgba(16,185,129,.15);color:#10b981;border:1px solid rgba(16,185,129,.3)}' +
80
+ '.dsub-brandClaude{background:rgba(245,158,11,.15);color:#f59e0b;border:1px solid rgba(245,158,11,.3)}' +
81
+ '.dsub-brandGrok{background:rgba(168,85,247,.15);color:#c084fc;border:1px solid rgba(168,85,247,.3)}' +
82
+ '.dsub-brandAgy{background:rgba(59,130,246,.15);color:#60a5fa;border:1px solid rgba(59,130,246,.3)}' +
83
+ '.dsub-brandOllama{background:rgba(107,114,128,.15);color:#9ca3af;border:1px solid rgba(107,114,128,.3)}' +
84
+ '.dsub-accInfo{flex:1;min-width:0;display:flex;flex-direction:column;gap:3px}' +
85
+ '.dsub-accNameRow{display:flex;align-items:center;justify-content:space-between;gap:8px}' +
86
+ '.dsub-accName{font-size:13px;font-weight:600}' +
87
+ '.dsub-accStatusTag{font-size:10.5px;font-weight:600;padding:1px 6px;border-radius:4px}' +
88
+ '.dsub-statusOk{background:rgba(16,185,129,.15);color:#34d399}' +
89
+ '.dsub-statusWarn{background:rgba(245,158,11,.15);color:#fbbf24}' +
90
+ '.dsub-statusBad{background:rgba(239,68,68,.15);color:#f87171}' +
91
+ '.dsub-statusOff{background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-tertiary)}' +
92
+ '.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)}' +
93
+ '.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}' +
94
+ '.dsub-btnPrimary:hover{opacity:.9}' +
95
+ '.dsub-btnSec{display:inline-flex;align-items:center;gap:5px;padding:5px 10px;border-radius:7px;font-size:11.5px;font-weight:500;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);border:1px solid var(--dsw-alias-border-l2);cursor:pointer;text-decoration:none;transition:all .15s}' +
96
+ '.dsub-btnSec:hover{background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-tertiary)}' +
101
97
  '.dsub-cq{display:inline-flex;align-items:center;gap:6px;font-size:12px;color:var(--dsw-alias-label-secondary);padding:0 6px}' +
102
98
  '.dsub-cqBar{position:relative;width:40px;height:5px;border-radius:3px;background:var(--dsw-alias-bg-layer-1);overflow:hidden}' +
103
99
  '.dsub-cqBarFill{height:100%;border-radius:3px;background:var(--dsw-alias-state-success-primary)}' +
@@ -152,7 +148,15 @@ const cssId = 'dsh-subscriptions/settings.module.css'
152
148
  'diagIssues': 'Open issue tracker',
153
149
  'diagHint': 'The report has no tokens, emails, proxy addresses or personal data.',
154
150
  'subsPill': 'SUBS',
155
- 'subsModalTitle': 'Subscription console',
151
+ 'subsModalTitle': 'Subscription Hub',
152
+ 'subsActiveHero': 'Active Subscription',
153
+ 'subsPoolTitle': 'Provider Accounts',
154
+ 'subsRefresh': 'Refresh',
155
+ 'subsOpenSettings': 'All Settings →',
156
+ 'subsFastMode': 'Fast Mode 1.5x',
157
+ 'subsHealthy': 'Healthy',
158
+ 'subsFree': 'free',
159
+ 'subsUsed': 'used',
156
160
  'subsModalClose': 'Close',
157
161
  'subsLogged': 'connected',
158
162
  'subsNotLogged': 'not connected',
@@ -253,6 +257,14 @@ const cssId = 'dsh-subscriptions/settings.module.css'
253
257
  'diagHint': 'В отчёте нет токенов, email, адресов прокси и персональных данных.',
254
258
  'subsPill': 'SUBS',
255
259
  'subsModalTitle': 'Консоль подписок',
260
+ 'subsActiveHero': 'Активная подписка',
261
+ 'subsPoolTitle': 'Пул аккаунтов',
262
+ 'subsRefresh': 'Обновить',
263
+ 'subsOpenSettings': 'Все настройки →',
264
+ 'subsFastMode': 'Режим Fast 1.5x',
265
+ 'subsHealthy': 'Исправен',
266
+ 'subsFree': 'свободно',
267
+ 'subsUsed': 'израсходовано',
256
268
  'subsModalClose': 'Закрыть',
257
269
  'subsLogged': 'подключен',
258
270
  'subsNotLogged': 'не подключен',
@@ -346,23 +358,7 @@ const cssId = 'dsh-subscriptions/settings.module.css'
346
358
  }
347
359
  const Chevron = ChevronIcon || FallbackChevron
348
360
 
349
- function PluginCard(props) {
350
- const t = (props && props.t) || ((key) => key)
351
- const [open, setOpen] = React.useState(false)
352
- return React.createElement('div', { className: 'dsub-block' },
353
- React.createElement('div', { className: 'dsub-row' },
354
- React.createElement('span', { className: 'dsub-h' }, t('title')),
355
- React.createElement('span', { className: 'dsub-sub' }, t('cardIntro')),
356
- React.createElement('span', { style: { flex: 1 } }),
357
- React.createElement('button', {
358
- type: 'button',
359
- className: 'dsub-mini',
360
- onClick: () => setOpen(!open),
361
- }, open ? t('hide') : t('show')),
362
- ),
363
- open ? React.createElement(SubsSection, props) : null,
364
- )
365
- }
361
+
366
362
 
367
363
  function SubsSection(props) {
368
364
  // Переводчик приходит от слота, потому что в его записи указан locale.
@@ -1118,59 +1114,174 @@ function PluginCard(props) {
1118
1114
 
1119
1115
  // #83: пилл SUBS(N) в шапке сессии со светодиодом здоровья пула и
1120
1116
  // модальной консолью аккаунтов (закрытие: крестик, Escape, клик мимо).
1117
+ function brandBadge(prov) {
1118
+ const p = String(prov || '').toLowerCase()
1119
+ if (p.includes('codex') || p.includes('chatgpt') || p.includes('openai')) return { label: 'OpenAI', cls: 'dsub-brandCodex', icon: '⚡' }
1120
+ if (p.includes('claude') || p.includes('anthropic')) return { label: 'Claude', cls: 'dsub-brandClaude', icon: '✳' }
1121
+ if (p.includes('grok') || p.includes('xai')) return { label: 'Grok', cls: 'dsub-brandGrok', icon: '✦' }
1122
+ if (p.includes('antigravity') || p.includes('google') || p.includes('gemini')) return { label: 'AGY', cls: 'dsub-brandAgy', icon: '◆' }
1123
+ if (p.includes('ollama')) return { label: 'Ollama', cls: 'dsub-brandOllama', icon: '🦙' }
1124
+ return { label: (p.charAt(0).toUpperCase() || 'P'), cls: 'dsub-brandCodex', icon: '●' }
1125
+ }
1126
+
1121
1127
  function SubsPill(props) {
1122
1128
  const t = (props && props.t) || ((k) => k)
1123
- const [state, setState] = React.useState({ logged: [], usage: 0, accounts: [], open: false })
1129
+ const [state, setState] = React.useState({ logged: [], usage: 0, accounts: [], open: false, active: null, loading: false })
1130
+
1131
+ const pull = () => {
1132
+ setState((s) => Object.assign({}, s, { loading: true }))
1133
+ refreshLoggedIn().then((r) => {
1134
+ const logged = r.logged || []
1135
+ fetch('/dsh-subscriptions/config', { cache: 'no-store' })
1136
+ .then((res) => res.json())
1137
+ .then((cfg) => {
1138
+ setState((s) => Object.assign({}, s, { logged, usage: r.usage, accounts: (cfg && cfg.accounts) || [], active: r.active, loading: false }))
1139
+ })
1140
+ .catch(() => {
1141
+ setState((s) => Object.assign({}, s, { logged, usage: r.usage, accounts: [], active: r.active, loading: false }))
1142
+ })
1143
+ }).catch(() => {
1144
+ setState((s) => Object.assign({}, s, { loading: false }))
1145
+ })
1146
+ }
1147
+
1124
1148
  React.useEffect(() => {
1125
- let alive = true
1126
- const pull = () => {
1127
- refreshLoggedIn().then((r) => {
1128
- if (!alive) return
1129
- const logged = r.logged || []
1130
- fetch('/dsh-subscriptions/config', { cache: 'no-store' })
1131
- .then((res) => res.json())
1132
- .then((cfg) => {
1133
- if (!alive) return
1134
- setState({ logged, usage: r.usage, accounts: (cfg && cfg.accounts) || [], open: false })
1135
- })
1136
- .catch(() => { if (alive) setState({ logged, usage: r.usage, accounts: [], open: false }) })
1137
- }).catch(() => {})
1138
- }
1139
1149
  pull()
1140
- const id = setInterval(pull, 60 * 1000)
1141
- return () => { alive = false; clearInterval(id) }
1150
+ const id = setInterval(pull, 25 * 1000)
1151
+ return () => clearInterval(id)
1142
1152
  }, [])
1153
+
1143
1154
  React.useEffect(() => {
1144
1155
  if (!state.open) return undefined
1145
1156
  const onKey = (e) => { if (e.key === 'Escape') setState((s) => Object.assign({}, s, { open: false })) }
1146
1157
  document.addEventListener('keydown', onKey)
1147
1158
  return () => document.removeEventListener('keydown', onKey)
1148
1159
  }, [state.open])
1160
+
1149
1161
  const n = state.logged.length
1162
+ const total = state.accounts.length || n
1150
1163
  const led = n === 0 ? 'dsub-ledOff' : (state.usage >= 90 ? 'dsub-ledBad' : (state.usage >= 50 ? 'dsub-ledWarn' : 'dsub-ledOk'))
1151
1164
  const now = Date.now()
1165
+
1166
+ // Header Pill Label
1167
+ let pillText = t('subsPill') + ' (' + n + ')'
1168
+ if (state.active) {
1169
+ const a = state.active
1170
+ const wins = Array.isArray(a.windows) ? a.windows : []
1171
+ const winStr = wins.map((w) => (w.label || w.id) + ' ' + Math.round(w.usedPercent) + '%').join(' ')
1172
+ const provName = a.provider ? (a.provider.charAt(0).toUpperCase() + a.provider.slice(1)) : ''
1173
+ pillText = (a.provider === 'codex' && a.fastMode ? '⚡ ' : '') + provName + (winStr ? ' · ' + winStr : (a.usagePercent != null ? ' · ' + Math.round(a.usagePercent) + '%' : ''))
1174
+ }
1175
+
1176
+ // Active Hero Card Renderer
1177
+ const renderActiveHero = () => {
1178
+ if (!state.active) return null
1179
+ const a = state.active
1180
+ const b = brandBadge(a.provider)
1181
+ const wins = Array.isArray(a.windows) ? a.windows : []
1182
+ const primaryWin = wins.find((w) => (w.label || w.id) === '5h') || wins[0]
1183
+ const secWin = wins.find((w) => (w.label || w.id) === '7d') || wins[1]
1184
+ const primPct = primaryWin && primaryWin.usedPercent != null ? Math.round(primaryWin.usedPercent) : (a.usagePercent != null ? Math.round(a.usagePercent) : 0)
1185
+ const secPct = secWin && secWin.usedPercent != null ? Math.round(secWin.usedPercent) : null
1186
+
1187
+ return React.createElement('div', { className: 'dsub-heroCard' },
1188
+ React.createElement('div', { className: 'dsub-heroHead' },
1189
+ React.createElement('div', { className: 'dsub-heroTitle' },
1190
+ React.createElement('span', { className: 'dsub-brandBadge ' + b.cls, style: { width: 24, height: 24, fontSize: 11 } }, b.icon),
1191
+ React.createElement('span', null, (a.provider ? a.provider.toUpperCase() : 'LLM') + (a.index ? ' #' + a.index : '')),
1192
+ a.fastMode ? React.createElement('span', { className: 'dsub-pillTag', style: { background: 'rgba(245,158,11,.15)', color: '#fbbf24' } }, '⚡ FAST 1.5x') : null,
1193
+ ),
1194
+ React.createElement('span', { className: 'dsub-heroModel' }, a.model || 'active model'),
1195
+ ),
1196
+ React.createElement('div', { className: 'dsub-heroBars' },
1197
+ React.createElement('div', null,
1198
+ React.createElement('div', { className: 'dsub-barLabelRow' },
1199
+ React.createElement('span', { style: { fontWeight: 600 } }, (primaryWin && primaryWin.label) ? primaryWin.label + ' window' : t('subs5hWindow')),
1200
+ React.createElement('span', { style: { fontVariantNumeric: 'tabular-nums', fontWeight: 600 } }, primPct + '% ' + t('subsUsed') + ' (' + (100 - primPct) + '% ' + t('subsFree') + ')'),
1201
+ ),
1202
+ React.createElement('div', { className: 'dsub-barTrack' },
1203
+ React.createElement('div', {
1204
+ className: 'dsub-barFillGrad',
1205
+ style: {
1206
+ width: Math.min(100, Math.max(0, primPct)) + '%',
1207
+ background: primPct >= 90 ? 'var(--dsw-alias-state-error-primary,#ef4444)' : (primPct >= 50 ? 'var(--dsw-alias-state-warning-primary,#f59e0b)' : 'var(--dsw-alias-state-success-primary,#10b981)'),
1208
+ },
1209
+ }),
1210
+ ),
1211
+ ),
1212
+ secWin ? React.createElement('div', null,
1213
+ React.createElement('div', { className: 'dsub-barLabelRow' },
1214
+ React.createElement('span', null, (secWin.label || '7d') + ' window'),
1215
+ React.createElement('span', { style: { fontVariantNumeric: 'tabular-nums' } }, secPct + '%'),
1216
+ ),
1217
+ React.createElement('div', { className: 'dsub-barTrack' },
1218
+ React.createElement('div', {
1219
+ className: 'dsub-barFillGrad',
1220
+ style: {
1221
+ width: Math.min(100, Math.max(0, secPct)) + '%',
1222
+ background: secPct >= 90 ? '#ef4444' : (secPct >= 50 ? '#f59e0b' : '#3b82f6'),
1223
+ },
1224
+ }),
1225
+ ),
1226
+ ) : null,
1227
+ ),
1228
+ )
1229
+ }
1230
+
1231
+ // Accounts list
1152
1232
  const rows = state.accounts.map((a, i) => {
1153
1233
  const isLogged = state.logged.includes(a.provider)
1154
1234
  const cooled = a.cooldownUntil && a.cooldownUntil > now
1155
1235
  const pct = a.usagePercent != null ? Math.round(a.usagePercent) : null
1156
- return React.createElement('div', { className: 'dsub-modalRow', key: i },
1157
- React.createElement('span', { className: 'dsub-led ' + (pct == null ? 'dsub-ledOff' : (pct >= 90 ? 'dsub-ledBad' : (pct >= 50 ? 'dsub-ledWarn' : 'dsub-ledOk'))) }),
1158
- React.createElement('span', { className: 'dsub-modalName' }, a.provider + (a.index ? ' #' + a.index : '')),
1159
- React.createElement('span', { style: { flex: 1 } }),
1160
- React.createElement('span', { className: 'dsub-modalSub' },
1161
- (isLogged ? t('subsLogged') : t('subsNotLogged')) +
1162
- (cooled ? ' · ' + t('subsCooldown') : '') +
1163
- (pct != null ? ' · ' + pct + '%' : '')
1236
+ const b = brandBadge(a.provider)
1237
+ const isActiveThis = state.active && state.active.provider === a.provider && state.active.index === a.index
1238
+
1239
+ let statusClass = 'dsub-statusOff'
1240
+ let statusText = t('subsNotLogged')
1241
+ if (cooled) {
1242
+ statusClass = 'dsub-statusWarn'
1243
+ statusText = t('subsCooldown')
1244
+ } else if (isLogged) {
1245
+ if (pct >= 90) { statusClass = 'dsub-statusBad'; statusText = '90%+ ' + t('subsUsed') }
1246
+ else if (pct >= 50) { statusClass = 'dsub-statusWarn'; statusText = pct + '% ' + t('subsUsed') }
1247
+ else { statusClass = 'dsub-statusOk'; statusText = (pct != null ? pct + '% · ' : '') + t('subsLogged') }
1248
+ }
1249
+
1250
+ return React.createElement('div', { className: 'dsub-accountCard', key: i },
1251
+ React.createElement('div', { className: 'dsub-brandBadge ' + b.cls }, b.icon),
1252
+ React.createElement('div', { className: 'dsub-accInfo' },
1253
+ React.createElement('div', { className: 'dsub-accNameRow' },
1254
+ React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6 } },
1255
+ React.createElement('span', { className: 'dsub-accName' }, a.provider.toUpperCase() + (a.index ? ' #' + a.index : '')),
1256
+ isActiveThis ? React.createElement('span', { className: 'dsub-pillTag', style: { background: 'rgba(16,185,129,.15)', color: '#34d399' } }, 'ACTIVE') : null,
1257
+ a.proxy ? React.createElement('span', { className: 'dsub-dim', title: a.proxy }, '🌐 proxy') : null,
1258
+ ),
1259
+ React.createElement('span', { className: 'dsub-accStatusTag ' + statusClass }, statusText),
1260
+ ),
1261
+ (isLogged && pct != null) ? React.createElement('div', { className: 'dsub-barTrack', style: { marginTop: 4, height: 4 } },
1262
+ React.createElement('div', {
1263
+ className: 'dsub-barFillGrad',
1264
+ style: {
1265
+ width: Math.min(100, Math.max(0, pct)) + '%',
1266
+ background: pct >= 90 ? '#ef4444' : (pct >= 50 ? '#f59e0b' : '#10b981'),
1267
+ },
1268
+ }),
1269
+ ) : null,
1164
1270
  ),
1165
1271
  )
1166
1272
  })
1273
+
1167
1274
  return React.createElement(React.Fragment, null,
1168
1275
  React.createElement('button', {
1169
- type: 'button', className: 'dsub-pill', 'aria-expanded': state.open ? 'true' : 'false',
1276
+ type: 'button',
1277
+ className: 'dsub-pill',
1278
+ 'aria-expanded': state.open ? 'true' : 'false',
1279
+ title: t('subsPill') + ': ' + (n > 0 ? (n + ' ' + t('connected')) : t('notConnected')),
1170
1280
  onClick: () => setState((s) => Object.assign({}, s, { open: !s.open })),
1171
1281
  },
1172
1282
  React.createElement('span', { className: 'dsub-led ' + led }),
1173
- t('subsPill') + ' (' + n + ')',
1283
+ React.createElement('span', null, pillText),
1284
+ React.createElement('span', { style: { fontSize: 9, opacity: 0.6, marginLeft: 2 } }, '▼'),
1174
1285
  ),
1175
1286
  state.open ? React.createElement('div', {
1176
1287
  className: 'dsub-modalWrap',
@@ -1178,19 +1289,38 @@ function PluginCard(props) {
1178
1289
  },
1179
1290
  React.createElement('div', { className: 'dsub-modal', role: 'dialog', 'aria-label': t('subsModalTitle') },
1180
1291
  React.createElement('div', { className: 'dsub-modalHead' },
1181
- React.createElement('span', { className: 'dsub-h' }, t('subsModalTitle')),
1182
- React.createElement('button', {
1183
- type: 'button', className: 'dsub-mini',
1184
- onClick: () => setState((s) => Object.assign({}, s, { open: false })),
1185
- }, t('subsModalClose') + ' \u00d7'),
1292
+ React.createElement('div', { className: 'dsub-modalTitleWrap' },
1293
+ React.createElement('div', { className: 'dsub-modalIcon' }, '⚡'),
1294
+ React.createElement('div', null,
1295
+ React.createElement('div', { style: { fontWeight: 700, fontSize: 15 } }, t('subsModalTitle')),
1296
+ React.createElement('div', { className: 'dsub-dim' }, n + ' of ' + total + ' ' + t('connected')),
1297
+ ),
1298
+ ),
1299
+ React.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 6 } },
1300
+ React.createElement('button', {
1301
+ type: 'button',
1302
+ className: 'dsub-mini',
1303
+ title: t('subsRefresh'),
1304
+ onClick: () => pull(),
1305
+ }, state.loading ? '…' : '🔄'),
1306
+ React.createElement('button', {
1307
+ type: 'button',
1308
+ className: 'dsub-mini',
1309
+ onClick: () => setState((s) => Object.assign({}, s, { open: false })),
1310
+ }, '✕'),
1311
+ ),
1186
1312
  ),
1187
- rows.length ? rows : React.createElement('div', { className: 'dsub-sub' }, t('subsNotLogged')),
1188
- React.createElement('div', { className: 'dsub-row', style: { marginTop: 12 } },
1313
+ renderActiveHero(),
1314
+ React.createElement('div', { className: 'dsub-poolTitle' }, t('subsPoolTitle')),
1315
+ rows.length ? rows : React.createElement('div', { className: 'dsub-sub', style: { padding: '12px 0' } }, t('subsNotLogged')),
1316
+ React.createElement('div', { className: 'dsub-modalFoot' },
1317
+ React.createElement('span', { className: 'dsub-dim' }, 'DSH Subscriptions'),
1189
1318
  React.createElement('a', {
1190
- className: 'dsub-mini', href: '#', onClick: (e) => {
1319
+ className: 'dsub-btnSec',
1320
+ href: '#',
1321
+ onClick: (e) => {
1191
1322
  e.preventDefault()
1192
1323
  setState((s) => Object.assign({}, s, { open: false }))
1193
- try { document.querySelector('[data-slot-key=\"settings.plugin.item\"],[data-dsub-open]') } catch {}
1194
1324
  try {
1195
1325
  const btn = Array.from(document.querySelectorAll('button')).find((b) => (b.textContent || '').includes(t('title')))
1196
1326
  if (btn) btn.click()
@@ -1203,162 +1333,6 @@ function PluginCard(props) {
1203
1333
  )
1204
1334
  }
1205
1335
 
1206
- // #82: перетаскиваемый HUD-бабл (shell.overlay) с кольцевой шкалой
1207
- // остатка активной подписки, прилипанием к краям и frosted-панелью.
1208
- const HUD_KEY = 'dsh-subscriptions.hud'
1209
- const HUD_SIZE = 64
1210
-
1211
- function hudClamp(pos) {
1212
- const w = window.innerWidth, h = window.innerHeight
1213
- return {
1214
- left: Math.min(Math.max(8, pos.left), Math.max(8, w - HUD_SIZE - 8)),
1215
- top: Math.min(Math.max(8, pos.top), Math.max(8, h - HUD_SIZE - 8)),
1216
- }
1217
- }
1218
-
1219
- function hudLoad() {
1220
- try {
1221
- const raw = JSON.parse(localStorage.getItem(HUD_KEY) || 'null')
1222
- if (raw && typeof raw.left === 'number' && typeof raw.top === 'number') return hudClamp(raw)
1223
- } catch {}
1224
- return hudClamp({ left: window.innerWidth - HUD_SIZE - 24, top: window.innerHeight - HUD_SIZE - 120 })
1225
- }
1226
-
1227
- function hudDock(pos) {
1228
- const w = window.innerWidth, h = window.innerHeight
1229
- const dist = { left: pos.left, right: w - (pos.left + HUD_SIZE), top: pos.top, bottom: h - (pos.top + HUD_SIZE) }
1230
- const TH = 24
1231
- let side = null, best = Infinity
1232
- for (const k of ['left', 'right', 'top', 'bottom']) {
1233
- if (dist[k] <= TH && dist[k] < best) { best = dist[k]; side = k }
1234
- }
1235
- if (!side) return pos
1236
- const next = { left: pos.left, top: pos.top }
1237
- if (side === 'left') next.left = 0
1238
- if (side === 'right') next.left = Math.max(0, w - HUD_SIZE)
1239
- if (side === 'top') next.top = 0
1240
- if (side === 'bottom') next.top = Math.max(0, h - HUD_SIZE)
1241
- return next
1242
- }
1243
-
1244
- function UsageBubble(props) {
1245
- const t = (props && props.t) || ((k) => k)
1246
- const [pos, setPos] = React.useState(() => (typeof window === 'undefined' ? { left: 0, top: 0 } : hudLoad()))
1247
- const [accounts, setAccounts] = React.useState([])
1248
- const [active, setActive] = React.useState(null)
1249
- const [dragging, setDragging] = React.useState(false)
1250
- const [busy, setBusy] = React.useState(false)
1251
- const drag = React.useRef(null)
1252
- React.useEffect(() => {
1253
- let alive = true
1254
- const pull = () => {
1255
- refreshLoggedIn().then((r) => {
1256
- if (!alive) return
1257
- setActive(r.active)
1258
- fetch('/dsh-subscriptions/config', { cache: 'no-store' })
1259
- .then((res) => res.json())
1260
- .then((cfg) => { if (alive) setAccounts((cfg && cfg.accounts) || []) })
1261
- .catch(() => {})
1262
- }).catch(() => {})
1263
- }
1264
- pull()
1265
- const id = setInterval(pull, 60 * 1000)
1266
- return () => { alive = false; clearInterval(id) }
1267
- }, [])
1268
- const onPointerDown = (e) => {
1269
- if (e.button !== 0) return
1270
- e.preventDefault()
1271
- e.currentTarget.setPointerCapture(e.pointerId)
1272
- drag.current = { id: e.pointerId, sx: e.clientX, sy: e.clientY, left: pos.left, top: pos.top, moved: false }
1273
- }
1274
- const onPointerMove = (e) => {
1275
- const d = drag.current
1276
- if (!d || e.pointerId !== d.id) return
1277
- if (Math.abs(e.clientX - d.sx) > 3 || Math.abs(e.clientY - d.sy) > 3) { d.moved = true; setDragging(true) }
1278
- if (!d.moved) return
1279
- setPos(hudClamp({ left: d.left + (e.clientX - d.sx), top: d.top + (e.clientY - d.sy) }))
1280
- }
1281
- const onPointerUp = (e) => {
1282
- const d = drag.current
1283
- if (!d || e.pointerId !== d.id) return
1284
- drag.current = null
1285
- setDragging(false)
1286
- setPos((cur) => {
1287
- const next = d.moved ? hudDock(hudClamp(cur)) : cur
1288
- try { localStorage.setItem(HUD_KEY, JSON.stringify(next)) } catch {}
1289
- return next
1290
- })
1291
- if (!d.moved) {
1292
- setBusy(true)
1293
- refreshLoggedIn().then((r) => {
1294
- setActive(r.active)
1295
- return fetch('/dsh-subscriptions/config', { cache: 'no-store' }).then((res) => res.json())
1296
- }).then((cfg) => setAccounts((cfg && cfg.accounts) || [])).catch(() => {}).finally(() => setBusy(false))
1297
- }
1298
- }
1299
- // Остаток активной подписки: 100 - usagePercent.
1300
- const remaining = active && active.usagePercent != null ? Math.max(0, 100 - active.usagePercent) : null
1301
- const tone = remaining == null ? 'var(--dsw-alias-label-tertiary)'
1302
- : (remaining < 10 ? 'var(--dsw-alias-state-error-primary)'
1303
- : (remaining < 30 ? 'var(--dsw-alias-state-warning-primary)' : 'var(--dsw-alias-state-success-primary)'))
1304
- const R = 26
1305
- const CIRC = 2 * Math.PI * R
1306
- const offset = CIRC * (1 - (remaining == null ? 0 : Math.min(100, remaining)) / 100)
1307
- const rows = accounts.map((a, i) => {
1308
- const pct = a.usagePercent != null ? Math.round(a.usagePercent) : null
1309
- return React.createElement('div', { className: 'dsub-hudRow', key: i },
1310
- React.createElement('span', { className: 'dsub-hudName' }, a.provider + (a.index ? ' #' + a.index : '')),
1311
- React.createElement('span', { className: 'dsub-bar' },
1312
- React.createElement('span', { className: 'dsub-barFill', style: { width: (pct == null ? 0 : Math.min(100, pct)) + '%' } })),
1313
- React.createElement('span', { className: 'dsub-hudPct' }, pct == null ? '—' : pct + '%'),
1314
- )
1315
- })
1316
- if (!createPortal) return null
1317
- return createPortal(React.createElement('div', {
1318
- className: 'dsub-hud',
1319
- 'data-dragging': dragging ? 'true' : undefined,
1320
- style: { left: pos.left + 'px', top: pos.top + 'px' },
1321
- },
1322
- React.createElement('button', {
1323
- type: 'button', className: 'dsub-hudBtn', 'aria-label': t('hudTitle'),
1324
- style: { '--dsub-tone': tone },
1325
- onPointerDown, onPointerMove, onPointerUp, onPointerCancel: onPointerUp,
1326
- },
1327
- React.createElement('svg', { className: 'dsub-hudRing', viewBox: '0 0 56 56', 'aria-hidden': 'true' },
1328
- React.createElement('circle', { className: 'dsub-hudTrack', cx: 28, cy: 28, r: R }),
1329
- React.createElement('circle', {
1330
- className: 'dsub-hudVal', cx: 28, cy: 28, r: R,
1331
- strokeDasharray: String(CIRC), strokeDashoffset: String(offset),
1332
- }),
1333
- ),
1334
- React.createElement('span', { className: 'dsub-hudCore' },
1335
- remaining == null ? '—' : (busy ? '\u2026' : Math.round(remaining) + '%'),
1336
- ),
1337
- ),
1338
- React.createElement('div', { className: 'dsub-hudPanel' },
1339
- React.createElement('div', { className: 'dsub-hudHead' }, t('hudTitle')),
1340
- rows.length ? rows : React.createElement('div', { className: 'dsub-sub' }, '\u2014'),
1341
- React.createElement('div', { className: 'dsub-hudHint' }, t('hudHint')),
1342
- ),
1343
- ), document.body)
1344
- }
1345
-
1346
- function registerUsageBubble(ctx) {
1347
- ctx.effect(() => {
1348
- ctx.slots.inject('shell.overlay', () =>
1349
- ctx.slots.register(
1350
- {
1351
- name: 'shell.overlay',
1352
- id: 'dsh-subscriptions-usage-bubble',
1353
- order: 40,
1354
- locale: NS,
1355
- },
1356
- (props) => React.createElement(UsageBubble, { t }),
1357
- ),
1358
- )
1359
- }, 'dsh-subscriptions: usage bubble')
1360
- }
1361
-
1362
1336
  function registerSubsPill(ctx) {
1363
1337
  ctx.effect(() => {
1364
1338
  ctx.slots.inject('conversation.session.header.actions', () =>
@@ -1391,145 +1365,6 @@ function PluginCard(props) {
1391
1365
  }, 'dsh-subscriptions: composer quota')
1392
1366
  }
1393
1367
 
1394
- // #52: компактный чип в шапке сессии со статусом подключённых подписок.
1395
- function HeaderChip(props) {
1396
- const [state, setState] = React.useState({ loading: true, count: 0, usage: 0, expiry: null, active: null })
1397
- const t = (props && props.t) || ((k) => k)
1398
- React.useEffect(() => {
1399
- const pull = () => {
1400
- refreshLoggedIn().then((r) => {
1401
- // #67: ближайшее окончание подписки в пределах expiryNotifyDays.
1402
- let expiry = null
1403
- const now = Date.now()
1404
- const days = r.expiryNotifyDays || 7
1405
- for (const p of r.logged) {
1406
- const at = r.expiresAt[p]
1407
- if (!at) continue
1408
- const msLeft = at - now
1409
- if (msLeft > 0 && msLeft <= days * 24 * 60 * 60 * 1000) {
1410
- if (!expiry || at < expiry.at) expiry = { at, label: r.labels[p] || p }
1411
- }
1412
- }
1413
- setState({ loading: false, count: r.logged.length, usage: r.usage, expiry, active: r.active })
1414
- }).catch(() => setState({ loading: false, count: 0, usage: 0, expiry: null, active: null }))
1415
- }
1416
- pull()
1417
- const id = setInterval(pull, 60 * 1000)
1418
- return () => clearInterval(id)
1419
- }, [])
1420
- // #69/#72: если есть активная подписка — показываем её (провайдер + индекс + windows с процентами).
1421
- if (state.active) {
1422
- const a = state.active
1423
- const wins = Array.isArray(a.windows) ? a.windows : []
1424
- // макс-процент среди окон для цвета полоски
1425
- let maxPct = -1
1426
- for (const w of wins) {
1427
- if (w && typeof w.usedPercent === 'number' && w.usedPercent > maxPct) maxPct = w.usedPercent
1428
- }
1429
- if (maxPct < 0 && typeof a.usagePercent === 'number') maxPct = a.usagePercent
1430
- const u = maxPct >= 0 ? maxPct : null
1431
- let acls = 'dsub-chipActive'
1432
- if (u != null) acls += u >= 100 ? ' dsub-chipActiveDanger' : (u >= 80 ? ' dsub-chipActiveDanger' : (u >= 50 ? ' dsub-chipActiveWarn' : ' dsub-chipActiveOk'))
1433
- const idx = a.index != null ? ('#' + a.index) : ''
1434
- const plan = a.plan ? (' ' + a.plan) : ''
1435
- // #77: без provider-префикса
1436
- const label = (a.provider === 'codex' && state.fastMode ? '\u26a1' : '') + idx + plan
1437
- // windows: "5h 12% 7d 8%"
1438
- const winLabels = wins.map((w) => (w.label || w.id) + ' ' + Math.round(w.usedPercent) + '%').join(' ')
1439
- return React.createElement('span', {
1440
- className: acls,
1441
- title: a.model ? (prov + idx + ' · ' + a.model) : (prov + idx),
1442
- },
1443
- React.createElement('span', { className: 'dsub-chipDot' }),
1444
- label,
1445
- u != null
1446
- ? React.createElement('span', { className: 'dsub-chipActiveBar' },
1447
- React.createElement('span', { className: 'dsub-chipActiveFill', style: { width: Math.min(100, Math.max(0, u)) + '%' } }))
1448
- : null,
1449
- winLabels || null,
1450
- )
1451
- }
1452
- const on = state.count > 0
1453
- // #66: цвет чипа по максимальному usagePercent (50/80/100).
1454
- const u = state.usage
1455
- let cls = 'dsub-chip'
1456
- if (on) cls += u >= 100 ? ' dsub-chipDanger' : (u >= 80 ? ' dsub-chipDanger' : (u >= 50 ? ' dsub-chipWarn' : ' dsub-chipOn'))
1457
- // #67: надпись об окончании подписки.
1458
- let text = on ? String(state.count) : '—'
1459
- if (state.expiry) {
1460
- const d = new Date(state.expiry.at).toLocaleDateString()
1461
- text = t('expiryLabel') + ': ' + state.expiry.label + ' ' + d
1462
- }
1463
- return React.createElement('span', {
1464
- className: cls,
1465
- title: on ? t('chipActive') + ': ' + state.count : t('none'),
1466
- },
1467
- React.createElement('span', { className: 'dsub-chipDot' }),
1468
- text,
1469
- )
1470
- }
1471
-
1472
- function registerHeaderChip(ctx) {
1473
- ctx.effect(() => {
1474
- ctx.slots.inject('conversation.session.header.utilities', () =>
1475
- ctx.slots.register(
1476
- {
1477
- name: 'conversation.session.header.utilities',
1478
- id: 'dsh-subscriptions-header-chip',
1479
- order: 5,
1480
- locale: NS,
1481
- },
1482
- (props) => React.createElement(HeaderChip, { t }),
1483
- ),
1484
- )
1485
- }, 'dsh-subscriptions: header chip')
1486
- }
1487
-
1488
- function registerComposerSwitch(ctx) {
1489
- ctx.effect(() => {
1490
- const render = () => {
1491
- if (!labelEl) return
1492
- refreshLoggedIn().then((logged) => {
1493
- if (!labelEl) return
1494
- const current = logged.length ? logged[activeIdx % logged.length] : null
1495
- labelEl.textContent = t('switchLabel') + ': ' + (current || t('none'))
1496
- }).catch(() => {})
1497
- }
1498
- ctx.slots.inject('composer.action', () =>
1499
- ctx.slots.register(
1500
- {
1501
- name: 'composer.action',
1502
- id: 'dsh-subscriptions-provider-switch',
1503
- order: 31,
1504
- label: () => {
1505
- // label используется ядром до монтирования; держим актуальным
1506
- setTimeout(render, 0)
1507
- return t('switchLabel')
1508
- },
1509
- },
1510
- (props) => {
1511
- const el = React.createElement('button', {
1512
- onClick: async () => {
1513
- const logged = await refreshLoggedIn()
1514
- if (!logged.length) return
1515
- activeIdx = (activeIdx + 1) % logged.length
1516
- render()
1517
- },
1518
- }, '')
1519
- // Обновить текст в смонтированном элементе
1520
- setTimeout(() => {
1521
- labelEl = el
1522
- render()
1523
- }, 0)
1524
- return el
1525
- },
1526
- ),
1527
- )
1528
- render()
1529
- }, 'dsh-subscriptions: composer provider switch')
1530
- }
1531
-
1532
- // #43: slash-команды входа/выхода/статуса прямо из чата.
1533
1368
  function registerSlashCommands(ctx) {
1534
1369
  ctx.effect(() => {
1535
1370
  const triggers = ctx.get('inputTriggers')
@@ -1595,11 +1430,8 @@ function PluginCard(props) {
1595
1430
  exports.apply = function apply(ctx) {
1596
1431
  setT(ctx.locale.bind(NS))
1597
1432
  registerSettings(ctx)
1598
- registerUsageBubble(ctx)
1599
1433
  registerSubsPill(ctx)
1600
1434
  registerComposerQuota(ctx)
1601
- registerHeaderChip(ctx)
1602
- registerComposerSwitch(ctx)
1603
1435
  registerSlashCommands(ctx)
1604
1436
  }
1605
1437
  return module.exports
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-subscriptions",
3
- "version": "0.4.18",
3
+ "version": "0.4.20",
4
4
  "description": "Use ChatGPT Codex, Claude, Grok, and Antigravity subscriptions as DeepSeek Harness LLM providers via OAuth.",
5
5
  "license": "MIT",
6
6
  "type": "module",