@hystudio/dsh-quota-pill 0.2.5 → 0.2.7

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/client.js CHANGED
@@ -513,6 +513,30 @@ window.__ModuleLoader__.load({
513
513
  credits: null,
514
514
  }];
515
515
  }
516
+ // Prepaid wallets (official DeepSeek): a balance, plus optional bonus
517
+ // credit. Both are money, not percentages, so each needs its own row —
518
+ // collapsing them into one label silently loses the bonus.
519
+ if (v.kind === 'payg' && v.credits && typeof v.credits.remain === 'number') {
520
+ const rows = [{
521
+ key: 'balance',
522
+ label: pick(lang, 'balanceLeft'),
523
+ pct: null,
524
+ reset: '',
525
+ credits: { remain: v.credits.remain, unit: v.credits.unit },
526
+ money: true,
527
+ }];
528
+ if (v.bonus && typeof v.bonus.amount === 'number') {
529
+ rows.push({
530
+ key: 'bonus',
531
+ label: pick(lang, 'bonusBalance'),
532
+ pct: null,
533
+ reset: '',
534
+ credits: { remain: v.bonus.amount, unit: v.bonus.unit },
535
+ money: true,
536
+ });
537
+ }
538
+ return rows;
539
+ }
516
540
  if (v.kind !== 'subscription-quota' || v.remainingPct === null || v.remainingPct === undefined) {
517
541
  return [{ key: 'kind', label: describeQuota(v, lang).text, pct: null, reset: '', credits: null }];
518
542
  }
@@ -1250,7 +1274,7 @@ window.__ModuleLoader__.load({
1250
1274
  }
1251
1275
 
1252
1276
  function WindowRow(props) {
1253
- const { label, pct, reset, credits, lang, stale } = props;
1277
+ const { label, pct, reset, credits, lang, stale, money: isMoney } = props;
1254
1278
  const isNum = typeof pct === 'number' && Number.isFinite(pct);
1255
1279
  const tweened = useTween(isNum ? pct : null, 450);
1256
1280
  // 首帧 tween 还没拿到起点时 shown 是 null;这时按目标值渲染静态读数,
@@ -1271,7 +1295,14 @@ window.__ModuleLoader__.load({
1271
1295
  h('div', { className: 'quota-pill-win-main' },
1272
1296
  h('div', { className: 'quota-pill-win-text' },
1273
1297
  h('span', { className: 'quota-pill-win-label', title: label }, label),
1274
- reset ? h('span', { className: 'quota-pill-win-reset', title: reset }, reset) : null,
1298
+ // Reserve the reset line even when there is nothing to show.
1299
+ // Conditionally omitting it made rows with a reset taller than those
1300
+ // without one, so the column of numbers did not line up.
1301
+ h('span', {
1302
+ className: 'quota-pill-win-reset',
1303
+ title: reset || undefined,
1304
+ style: reset ? undefined : { visibility: 'hidden' },
1305
+ }, reset || '\u00a0'),
1275
1306
  ),
1276
1307
  h('div', { className: 'quota-pill-win-gauge' },
1277
1308
  showCredits
@@ -1281,7 +1312,7 @@ window.__ModuleLoader__.load({
1281
1312
  ? `${fmtAmount(credits.remain)} / ${fmtAmount(credits.total)}`
1282
1313
  + (credits.unit ? ' ' + String(credits.unit) : '')
1283
1314
  : undefined,
1284
- }, fmtCompact(credits.remain))
1315
+ }, isMoney ? money(credits.remain, credits.unit) : fmtCompact(credits.remain))
1285
1316
  : isNum
1286
1317
  ? h('span', {
1287
1318
  className: 'quota-pill-win-pct',
@@ -1293,7 +1324,7 @@ window.__ModuleLoader__.load({
1293
1324
  style: { color: remainingColor(shown) },
1294
1325
  }, typeof shown === 'number' ? `${shown.toFixed(shown < 10 ? 1 : 0)}%` : '')
1295
1326
  : null,
1296
- showCredits && unitLabel(credits.unit, lang)
1327
+ showCredits && !isMoney && unitLabel(credits.unit, lang)
1297
1328
  ? h('span', { className: 'quota-pill-win-unit' }, unitLabel(credits.unit, lang))
1298
1329
  : null,
1299
1330
 
@@ -1313,7 +1344,7 @@ window.__ModuleLoader__.load({
1313
1344
  h('div', { className: 'quota-pill-group-head' }, name),
1314
1345
  // 组内行 key 带组名,避免不同 provider 的同名窗口(都是 5h/weekly)
1315
1346
  // 被 React 当成同一个组件实例,导致 tween 跨 provider 插值。
1316
- entries.map((e) => h(WindowRow, { key: `${name}|${e.key}`, label: e.label, pct: e.pct, reset: e.reset, credits: e.credits, lang, stale })),
1347
+ entries.map((e) => h(WindowRow, { key: `${name}|${e.key}`, label: e.label, pct: e.pct, reset: e.reset, credits: e.credits, lang, stale, money: e.money })),
1317
1348
  );
1318
1349
  }
1319
1350
 
@@ -1388,7 +1419,7 @@ window.__ModuleLoader__.load({
1388
1419
  // key 带 provider:换 provider 时强制重挂载,让 tween 从「空」开始,
1389
1420
  // 而不是把 A 的 5h 平滑滑到 B 的 5h——那是两个不同账户的额度,
1390
1421
  // 中间的数字不代表任何真实状态。
1391
- entries.map((e) => h(WindowRow, { key: `${providerName}|${e.key}`, label: e.label, pct: e.pct, reset: e.reset, credits: e.credits, lang, stale })),
1422
+ entries.map((e) => h(WindowRow, { key: `${providerName}|${e.key}`, label: e.label, pct: e.pct, reset: e.reset, credits: e.credits, lang, stale, money: e.money })),
1392
1423
  ),
1393
1424
  h('dl', { className: 'quota-pill-stats' },
1394
1425
  data.account ? statRow(pick(lang, 'account'), String(data.account)) : null,
@@ -1504,6 +1535,8 @@ window.__ModuleLoader__.load({
1504
1535
  '.quota-pill-win-unit{flex:none;margin-left:3px;color:var(--dsw-alias-label-tertiary)}',
1505
1536
 
1506
1537
  '.quota-pill-win-ring{flex:none;display:inline-flex}',
1538
+ // 固定行高:没有重置时间的行也要占住这一行,数字才对得齐
1539
+ '.quota-pill-win-reset{min-height:14px}',
1507
1540
  // Sidecar not running: show the fix instead of a bare dash.
1508
1541
  '.quota-pill-offline{display:flex;flex-direction:column;gap:6px;padding:10px 2px}',
1509
1542
  '.quota-pill-offline-title{font-weight:600;color:var(--dsw-alias-label-primary,inherit)}',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hystudio/dsh-quota-pill",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Quota pill for DeepSeek Harness: remaining quota for every LLM subscription you have, in one collapsible pill",
@@ -11,6 +11,27 @@ import { getJson } from './http.js';
11
11
  const clamp = (n, lo, hi) => Math.max(lo, Math.min(hi, n));
12
12
  const round2 = (n) => Math.round(n * 100) / 100;
13
13
 
14
+ /**
15
+ * Normalise a reset timestamp to ISO, or null when there is none.
16
+ *
17
+ * Upstreams are inconsistent: some send ISO strings, some epoch milliseconds,
18
+ * and some use `0` to mean "this window has no reset pending" (Command Code's
19
+ * five-hour window does exactly that). Treating 0 as a date would render 1970.
20
+ */
21
+ function isoReset(v) {
22
+ if (v == null || v === '' || v === 0) return null;
23
+ if (typeof v === 'string') {
24
+ // Already ISO-ish: keep as-is so the client formats it consistently.
25
+ const t = Date.parse(v);
26
+ return Number.isFinite(t) ? new Date(t).toISOString() : null;
27
+ }
28
+ const n = Number(v);
29
+ if (!Number.isFinite(n) || n <= 0) return null;
30
+ // Seconds vs milliseconds: anything below ~year 33658 in ms is really seconds.
31
+ const ms = n < 1e11 ? n * 1000 : n;
32
+ return new Date(ms).toISOString();
33
+ }
34
+
14
35
  /** Expand `products` (window → used%) into the `windows` rows the pill renders. */
15
36
  function toWindows(products, resetByKey = {}) {
16
37
  const out = [];
@@ -217,7 +238,7 @@ export async function commandcode() {
217
238
  const pct = round2((Number(used) / Number(cap)) * 100);
218
239
  products[label] = pct;
219
240
  ratios.push(100 - pct);
220
- if (tight === null || pct > tight[1]) tight = [key, pct, w.resetAt];
241
+ if (tight === null || pct > tight[1]) tight = [key, pct, isoReset(w.resetAt)];
221
242
  }
222
243
 
223
244
  const monthlyLeft = cred.monthlyCredits;
@@ -237,14 +258,21 @@ export async function commandcode() {
237
258
 
238
259
  // Plan identity (GOAT / Pro / Max) lives on a sibling endpoint; failure there
239
260
  // must not lose the quota we already have.
261
+ // Plan identity and the monthly period live on a sibling endpoint, nested under
262
+ // `data`. Getting this wrong is silent: the quota still renders, just with a
263
+ // null plan and a monthly window that cannot show when it renews.
240
264
  let plan = null;
265
+ let monthlyReset = null;
241
266
  try {
242
267
  const sub = await getJson('https://api.commandcode.ai/alpha/billing/subscriptions', { token });
243
- if (sub.ok && sub.data) {
244
- plan = sub.data.plan || (sub.data.subscription && sub.data.subscription.plan) || null;
245
- }
268
+ const d = sub.ok && sub.data ? (sub.data.data || {}) : {};
269
+ plan = d.planId || null;
270
+ // currentPeriodEnd is the real monthly renewal — better than deriving it
271
+ // from the weekly window, which was my earlier guess.
272
+ monthlyReset = d.currentPeriodEnd || null;
246
273
  } catch {
247
274
  plan = null;
275
+ monthlyReset = null;
248
276
  }
249
277
 
250
278
  const remaining = round2(Math.min(...ratios));
@@ -261,8 +289,12 @@ export async function commandcode() {
261
289
  const r = {};
262
290
  for (const [label, key] of [['5h', 'fiveHour'], ['weekly', 'weekly']]) {
263
291
  const w = win[key] || {};
264
- if (w.resetAt) r[label] = w.resetAt;
292
+ const iso = isoReset(w.resetAt);
293
+ if (iso) r[label] = iso;
265
294
  }
295
+ // The monthly ceiling is derived, so its reset must come from the
296
+ // subscription period; the credits endpoint does not carry one.
297
+ if (monthlyReset) r.monthly = monthlyReset;
266
298
  return r;
267
299
  })()),
268
300
  raw: { source: 'api.commandcode.ai/alpha/billing/credits', plan },
@@ -72,6 +72,14 @@ function isoFromEpoch(sec) {
72
72
  }
73
73
 
74
74
 
75
+ /** Same reset for every product row — xAI reports one billing period for all. */
76
+ function fillReset(products, resetAt) {
77
+ const out = {};
78
+ if (!resetAt) return out;
79
+ for (const key of Object.keys(products || {})) out[key] = resetAt;
80
+ return out;
81
+ }
82
+
75
83
  /**
76
84
  * Expand `products` (window → used%) into the `windows` array the client
77
85
  * renders. The client shows one row per window with its own reset time, so an
@@ -142,7 +150,10 @@ export async function xai(model) {
142
150
  window: period.type || null,
143
151
  resetAt: period.end || cfg.billingPeriodEnd || null,
144
152
  products,
145
- windows: toWindows(products, {}),
153
+ // Every per-product row shares the account's billing period, so they all
154
+ // carry the same reset. Leaving it empty made those rows look like they
155
+ // never expire while the summary right above them showed a date.
156
+ windows: toWindows(products, fillReset(products, period.end || cfg.billingPeriodEnd)),
146
157
  raw: { source: 'billing?format=credits', config: cfg },
147
158
  };
148
159
  }
@@ -175,7 +186,7 @@ export async function xai(model) {
175
186
  window: period.type || null,
176
187
  resetAt: period.end || cfg.billingPeriodEnd || null,
177
188
  products,
178
- windows: toWindows(products, {}),
189
+ windows: toWindows(products, fillReset(products, period.end || cfg.billingPeriodEnd)),
179
190
  detail: {
180
191
  tokens: `${rl.remaining_tokens}/${rl.limit_tokens}`,
181
192
  requests: `${rl.remaining_requests}/${rl.limit_requests}`,