@leo000001/opencode-quota-sidebar 1.3.0 → 1.5.0

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/dist/format.js CHANGED
@@ -127,9 +127,22 @@ function fitLine(value, width) {
127
127
  return truncateToCellWidth(safe, width);
128
128
  return `${head}~`;
129
129
  }
130
- function formatApiCostValue(value) {
130
+ function formatCurrency(value, currency) {
131
131
  const safe = Number.isFinite(value) && value > 0 ? value : 0;
132
- return `$${safe.toFixed(2)}`;
132
+ const prefix = typeof currency === 'string' && currency ? currency : '$';
133
+ if (safe === 0)
134
+ return `${prefix}0.00`;
135
+ if (safe < 10)
136
+ return `${prefix}${safe.toFixed(2)}`;
137
+ const one = safe.toFixed(1);
138
+ const trimmed = one.endsWith('.0') ? one.slice(0, -2) : one;
139
+ return `${prefix}${trimmed}`;
140
+ }
141
+ function formatUsd(value) {
142
+ return formatCurrency(value, '$');
143
+ }
144
+ function formatApiCostValue(value) {
145
+ return formatUsd(value);
133
146
  }
134
147
  function formatApiCostLine(value) {
135
148
  return `${formatApiCostValue(value)} as API cost`;
@@ -187,7 +200,10 @@ export function renderSidebarTitle(baseTitle, usage, quotas, config) {
187
200
  return Math.max(max, stringCellWidth(label));
188
201
  }, 0);
189
202
  const quotaItems = visibleQuotas
190
- .flatMap((item) => compactQuotaWide(item, labelWidth))
203
+ .flatMap((item) => compactQuotaWide(item, labelWidth, {
204
+ width,
205
+ wrapLines: config.sidebar.wrapQuotaLines,
206
+ }))
191
207
  .filter((s) => Boolean(s));
192
208
  if (quotaItems.length > 0) {
193
209
  lines.push('');
@@ -200,14 +216,31 @@ export function renderSidebarTitle(baseTitle, usage, quotas, config) {
200
216
  }
201
217
  /**
202
218
  * Multi-window quota format for sidebar.
203
- * Single window: "OpenAI 5h 80% Rst 16:20"
204
- * Multi window: "OpenAI 5h 80% Rst 16:20" + indented next line
205
- * Copilot: "Copilot Monthly 70% Rst 03-01"
219
+ *
220
+ * When wrapLines=false (or content fits):
221
+ * "OpenAI 5h 80% Rst 16:20"
222
+ * " Weekly 70% Rst 03-01"
223
+ *
224
+ * When wrapLines=true and label+content overflows width:
225
+ * "RC-openai"
226
+ * " Daily $349.66/$180 Exp+ 02-27"
227
+ * " Balance $108.88"
206
228
  */
207
- function compactQuotaWide(quota, labelWidth = 0) {
229
+ function compactQuotaWide(quota, labelWidth = 0, options) {
208
230
  const label = sanitizeLine(quotaDisplayLabel(quota));
209
231
  const labelPadded = padEndCells(label, labelWidth);
232
+ const indent = ' '.repeat(labelWidth + 1);
233
+ const detailIndent = ' ';
210
234
  const withLabel = (content) => `${labelPadded} ${content}`;
235
+ const wrap = options?.wrapLines === true && (options?.width || 0) > 0;
236
+ const width = options?.width || 0;
237
+ /** If inline version overflows, break into label-line + indented detail lines. */
238
+ const maybeBreak = (inlineText, detailLines) => {
239
+ const inline = withLabel(inlineText);
240
+ if (!wrap || stringCellWidth(inline) <= width)
241
+ return [inline];
242
+ return [label, ...detailLines.map((d) => `${detailIndent}${d}`)];
243
+ };
211
244
  if (quota.status === 'error')
212
245
  return [withLabel('Remaining ?')];
213
246
  if (quota.status === 'unsupported')
@@ -217,7 +250,7 @@ function compactQuotaWide(quota, labelWidth = 0) {
217
250
  if (quota.status !== 'ok')
218
251
  return [];
219
252
  const balanceText = quota.balance
220
- ? `Balance ${quota.balance.currency}${quota.balance.amount.toFixed(2)}`
253
+ ? `Balance ${formatCurrency(quota.balance.amount, quota.balance.currency)}`
221
254
  : undefined;
222
255
  const renderWindow = (win) => {
223
256
  const showPercent = win.showPercent !== false;
@@ -229,7 +262,7 @@ function compactQuotaWide(quota, labelWidth = 0) {
229
262
  ? [sanitizeLine(win.label), pct]
230
263
  : [sanitizeLine(win.label)]
231
264
  : [pct];
232
- const reset = compactReset(win.resetAt);
265
+ const reset = compactReset(win.resetAt, win.resetLabel);
233
266
  if (reset) {
234
267
  parts.push(`${sanitizeLine(win.resetLabel || 'Rst')} ${reset}`);
235
268
  }
@@ -238,42 +271,64 @@ function compactQuotaWide(quota, labelWidth = 0) {
238
271
  // Multi-window rendering
239
272
  if (quota.windows && quota.windows.length > 0) {
240
273
  const parts = quota.windows.map(renderWindow);
274
+ // Build the detail lines (window texts + optional balance)
275
+ const details = [...parts];
276
+ if (balanceText && !parts.some((p) => p.includes('Balance '))) {
277
+ details.push(balanceText);
278
+ }
279
+ // Try inline first (single window, fits in one line)
241
280
  if (parts.length === 1) {
242
- const first = withLabel(parts[0]);
243
- if (balanceText && !parts[0].includes('Balance ')) {
244
- const indent = ' '.repeat(labelWidth + 1);
245
- return [first, `${indent}${balanceText}`];
281
+ const firstInline = withLabel(parts[0]);
282
+ if (!wrap || stringCellWidth(firstInline) <= width) {
283
+ // Inline fits use classic layout
284
+ const lines = [firstInline];
285
+ if (balanceText && !parts[0].includes('Balance ')) {
286
+ lines.push(`${indent}${balanceText}`);
287
+ }
288
+ return lines;
246
289
  }
247
- return [first];
290
+ // Overflow — break: label on its own line, details indented
291
+ return [label, ...details.map((d) => `${detailIndent}${d}`)];
248
292
  }
249
- const indent = ' '.repeat(labelWidth + 1);
250
- const lines = [
251
- withLabel(parts[0]),
252
- ...parts.slice(1).map((part) => `${indent}${part}`),
253
- ];
254
- const alreadyHasBalance = parts.some((part) => part.includes('Balance '));
255
- if (balanceText && !alreadyHasBalance) {
256
- lines.push(`${indent}${balanceText}`);
293
+ // Multiple windows: try classic inline layout first
294
+ const firstInline = withLabel(parts[0]);
295
+ if (!wrap || stringCellWidth(firstInline) <= width) {
296
+ const lines = [
297
+ firstInline,
298
+ ...parts.slice(1).map((part) => `${indent}${part}`),
299
+ ];
300
+ if (balanceText && !parts.some((p) => p.includes('Balance '))) {
301
+ lines.push(`${indent}${balanceText}`);
302
+ }
303
+ return lines;
257
304
  }
258
- return lines;
305
+ // Overflow — break all
306
+ return [label, ...details.map((d) => `${detailIndent}${d}`)];
259
307
  }
260
308
  if (balanceText) {
261
- return [withLabel(balanceText)];
309
+ return maybeBreak(balanceText, [balanceText]);
262
310
  }
263
311
  // Fallback: single value from top-level remainingPercent
264
312
  const percent = quota.remainingPercent === undefined
265
313
  ? '?'
266
314
  : `${Math.round(quota.remainingPercent)}%`;
267
- const reset = compactReset(quota.resetAt);
268
- return [withLabel(`Remaining ${percent}${reset ? ` Rst ${reset}` : ''}`)];
315
+ const reset = compactReset(quota.resetAt, 'Rst');
316
+ const fallbackText = `Remaining ${percent}${reset ? ` Rst ${reset}` : ''}`;
317
+ return maybeBreak(fallbackText, [fallbackText]);
269
318
  }
270
- function compactReset(iso) {
319
+ function compactReset(iso, resetLabel) {
271
320
  if (!iso)
272
321
  return undefined;
273
322
  const timestamp = Date.parse(iso);
274
323
  if (Number.isNaN(timestamp))
275
324
  return undefined;
276
325
  const value = new Date(timestamp);
326
+ // RightCode subscriptions are displayed as an expiry date (MM-DD), not a time.
327
+ // Using UTC here makes the output stable across time zones for ISO `...Z` input.
328
+ if (typeof resetLabel === 'string' && resetLabel.startsWith('Exp')) {
329
+ const two = (num) => `${num}`.padStart(2, '0');
330
+ return `${two(value.getUTCMonth() + 1)}-${two(value.getUTCDate())}`;
331
+ }
277
332
  const now = new Date();
278
333
  const sameDay = value.getFullYear() === now.getFullYear() &&
279
334
  value.getMonth() === now.getMonth() &&
@@ -317,7 +372,7 @@ export function renderMarkdownReport(period, usage, quotas, options) {
317
372
  rightCodeSubscriptionProviderIDs.has(providerID);
318
373
  if (isSubscription)
319
374
  return '-';
320
- return `$${cost.toFixed(3)}`;
375
+ return formatUsd(cost);
321
376
  };
322
377
  const isSubscriptionMeasuredProvider = (providerID) => {
323
378
  const canonical = canonicalProviderID(providerID);
@@ -329,18 +384,16 @@ export function renderMarkdownReport(period, usage, quotas, options) {
329
384
  const canonical = canonicalProviderID(providerID);
330
385
  if (canonical === 'github-copilot')
331
386
  return '-';
332
- if (!Number.isFinite(apiCost) || apiCost <= 0)
333
- return '$0.00';
334
- return `$${apiCost.toFixed(2)}`;
387
+ return formatUsd(apiCost);
335
388
  };
336
389
  const measuredCostSummaryValue = () => {
337
390
  const providers = Object.values(usage.providers);
338
391
  if (providers.length === 0)
339
- return `$${usage.cost.toFixed(4)}`;
392
+ return formatUsd(usage.cost);
340
393
  const hasNonSubscription = providers.some((provider) => !isSubscriptionMeasuredProvider(provider.providerID));
341
394
  if (!hasNonSubscription)
342
395
  return '-';
343
- return `$${usage.cost.toFixed(4)}`;
396
+ return formatUsd(usage.cost);
344
397
  };
345
398
  const apiCostSummaryValue = () => {
346
399
  const providers = Object.values(usage.providers);
@@ -376,7 +429,7 @@ export function renderMarkdownReport(period, usage, quotas, options) {
376
429
  }
377
430
  if (quota.status === 'ok' && quota.balance) {
378
431
  return [
379
- mdCell(`- ${quota.label}: ${quota.status} | balance ${quota.balance.currency}${quota.balance.amount.toFixed(2)}`),
432
+ mdCell(`- ${quota.label}: ${quota.status} | balance ${formatCurrency(quota.balance.amount, quota.balance.currency)}`),
380
433
  ];
381
434
  }
382
435
  const remaining = quota.remainingPercent === undefined
@@ -455,7 +508,7 @@ export function renderToastMessage(period, usage, quotas, options) {
455
508
  .sort((left, right) => right.apiCost - left.apiCost)
456
509
  .map((provider) => ({
457
510
  label: displayShortLabel(provider.providerID),
458
- value: `$${provider.apiCost.toFixed(2)}`,
511
+ value: formatUsd(provider.apiCost),
459
512
  }));
460
513
  lines.push('');
461
514
  lines.push(fitLine('Cost as API', width));
@@ -475,7 +528,7 @@ export function renderToastMessage(period, usage, quotas, options) {
475
528
  const pct = win.remainingPercent === undefined
476
529
  ? '-'
477
530
  : `${win.remainingPercent.toFixed(1)}%`;
478
- const reset = compactReset(win.resetAt);
531
+ const reset = compactReset(win.resetAt, win.resetLabel);
479
532
  const parts = [win.label];
480
533
  if (showPercent)
481
534
  parts.push(pct);
@@ -489,7 +542,7 @@ export function renderToastMessage(period, usage, quotas, options) {
489
542
  if (item.balance) {
490
543
  pairs.push({
491
544
  label: '',
492
- value: `Balance ${item.balance.currency}${item.balance.amount.toFixed(2)}`,
545
+ value: `Balance ${formatCurrency(item.balance.amount, item.balance.currency)}`,
493
546
  });
494
547
  }
495
548
  return pairs;
@@ -498,14 +551,14 @@ export function renderToastMessage(period, usage, quotas, options) {
498
551
  return [
499
552
  {
500
553
  label: quotaDisplayLabel(item),
501
- value: `Balance ${item.balance.currency}${item.balance.amount.toFixed(2)}`,
554
+ value: `Balance ${formatCurrency(item.balance.amount, item.balance.currency)}`,
502
555
  },
503
556
  ];
504
557
  }
505
558
  const percent = item.remainingPercent === undefined
506
559
  ? '-'
507
560
  : `${item.remainingPercent.toFixed(1)}%`;
508
- const reset = compactReset(item.resetAt);
561
+ const reset = compactReset(item.resetAt, 'Rst');
509
562
  return [
510
563
  {
511
564
  label: quotaDisplayLabel(item),
@@ -51,8 +51,11 @@ function matchesSubscriptionPrefix(providerPrefixes, availablePrefixes) {
51
51
  function formatQuotaValue(value) {
52
52
  if (!Number.isFinite(value))
53
53
  return '0';
54
- const rounded = Number(value.toFixed(2));
55
- return Number.isInteger(rounded) ? `${Math.trunc(rounded)}` : `${rounded}`;
54
+ if (Math.abs(value) >= 10) {
55
+ const one = value.toFixed(1);
56
+ return one.endsWith('.0') ? one.slice(0, -2) : one;
57
+ }
58
+ return value.toFixed(2);
56
59
  }
57
60
  function parseSubscription(value) {
58
61
  const total = asNumber(value.total_quota);
package/dist/storage.js CHANGED
@@ -13,6 +13,7 @@ export const defaultConfig = {
13
13
  width: 36,
14
14
  showCost: true,
15
15
  showQuota: true,
16
+ wrapQuotaLines: true,
16
17
  includeChildren: true,
17
18
  childrenMaxDepth: 6,
18
19
  childrenMaxSessions: 128,
@@ -68,6 +69,7 @@ export async function loadConfig(paths) {
68
69
  width: Math.max(20, Math.min(60, asNumber(sidebar.width, defaultConfig.sidebar.width))),
69
70
  showCost: asBoolean(sidebar.showCost, defaultConfig.sidebar.showCost),
70
71
  showQuota: asBoolean(sidebar.showQuota, defaultConfig.sidebar.showQuota),
72
+ wrapQuotaLines: asBoolean(sidebar.wrapQuotaLines, defaultConfig.sidebar.wrapQuotaLines),
71
73
  includeChildren: asBoolean(sidebar.includeChildren, defaultConfig.sidebar.includeChildren),
72
74
  childrenMaxDepth: Math.max(1, Math.min(32, Math.floor(asNumber(sidebar.childrenMaxDepth, defaultConfig.sidebar.childrenMaxDepth)))),
73
75
  childrenMaxSessions: Math.max(0, Math.min(2000, Math.floor(asNumber(sidebar.childrenMaxSessions, defaultConfig.sidebar.childrenMaxSessions)))),
package/dist/types.d.ts CHANGED
@@ -99,6 +99,8 @@ export type QuotaSidebarConfig = {
99
99
  width: number;
100
100
  showCost: boolean;
101
101
  showQuota: boolean;
102
+ /** When true, wrap long quota lines and indent continuations. */
103
+ wrapQuotaLines: boolean;
102
104
  /** Include descendant subagent sessions in session-scoped usage/quota. */
103
105
  includeChildren: boolean;
104
106
  /** Max descendant traversal depth when includeChildren is enabled. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leo000001/opencode-quota-sidebar",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "OpenCode plugin that shows quota and token usage in session titles",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",