@leo000001/opencode-quota-sidebar 1.3.0 → 1.4.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
@@ -187,7 +187,10 @@ export function renderSidebarTitle(baseTitle, usage, quotas, config) {
187
187
  return Math.max(max, stringCellWidth(label));
188
188
  }, 0);
189
189
  const quotaItems = visibleQuotas
190
- .flatMap((item) => compactQuotaWide(item, labelWidth))
190
+ .flatMap((item) => compactQuotaWide(item, labelWidth, {
191
+ width,
192
+ wrapLines: config.sidebar.wrapQuotaLines,
193
+ }))
191
194
  .filter((s) => Boolean(s));
192
195
  if (quotaItems.length > 0) {
193
196
  lines.push('');
@@ -200,14 +203,31 @@ export function renderSidebarTitle(baseTitle, usage, quotas, config) {
200
203
  }
201
204
  /**
202
205
  * 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"
206
+ *
207
+ * When wrapLines=false (or content fits):
208
+ * "OpenAI 5h 80% Rst 16:20"
209
+ * " Weekly 70% Rst 03-01"
210
+ *
211
+ * When wrapLines=true and label+content overflows width:
212
+ * "RC-openai"
213
+ * " Daily $349.66/$180 Exp+ 02-27"
214
+ * " Balance $108.88"
206
215
  */
207
- function compactQuotaWide(quota, labelWidth = 0) {
216
+ function compactQuotaWide(quota, labelWidth = 0, options) {
208
217
  const label = sanitizeLine(quotaDisplayLabel(quota));
209
218
  const labelPadded = padEndCells(label, labelWidth);
219
+ const indent = ' '.repeat(labelWidth + 1);
220
+ const detailIndent = ' ';
210
221
  const withLabel = (content) => `${labelPadded} ${content}`;
222
+ const wrap = options?.wrapLines === true && (options?.width || 0) > 0;
223
+ const width = options?.width || 0;
224
+ /** If inline version overflows, break into label-line + indented detail lines. */
225
+ const maybeBreak = (inlineText, detailLines) => {
226
+ const inline = withLabel(inlineText);
227
+ if (!wrap || stringCellWidth(inline) <= width)
228
+ return [inline];
229
+ return [label, ...detailLines.map((d) => `${detailIndent}${d}`)];
230
+ };
211
231
  if (quota.status === 'error')
212
232
  return [withLabel('Remaining ?')];
213
233
  if (quota.status === 'unsupported')
@@ -229,7 +249,7 @@ function compactQuotaWide(quota, labelWidth = 0) {
229
249
  ? [sanitizeLine(win.label), pct]
230
250
  : [sanitizeLine(win.label)]
231
251
  : [pct];
232
- const reset = compactReset(win.resetAt);
252
+ const reset = compactReset(win.resetAt, win.resetLabel);
233
253
  if (reset) {
234
254
  parts.push(`${sanitizeLine(win.resetLabel || 'Rst')} ${reset}`);
235
255
  }
@@ -238,42 +258,64 @@ function compactQuotaWide(quota, labelWidth = 0) {
238
258
  // Multi-window rendering
239
259
  if (quota.windows && quota.windows.length > 0) {
240
260
  const parts = quota.windows.map(renderWindow);
261
+ // Build the detail lines (window texts + optional balance)
262
+ const details = [...parts];
263
+ if (balanceText && !parts.some((p) => p.includes('Balance '))) {
264
+ details.push(balanceText);
265
+ }
266
+ // Try inline first (single window, fits in one line)
241
267
  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}`];
268
+ const firstInline = withLabel(parts[0]);
269
+ if (!wrap || stringCellWidth(firstInline) <= width) {
270
+ // Inline fits use classic layout
271
+ const lines = [firstInline];
272
+ if (balanceText && !parts[0].includes('Balance ')) {
273
+ lines.push(`${indent}${balanceText}`);
274
+ }
275
+ return lines;
246
276
  }
247
- return [first];
277
+ // Overflow — break: label on its own line, details indented
278
+ return [label, ...details.map((d) => `${detailIndent}${d}`)];
248
279
  }
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}`);
280
+ // Multiple windows: try classic inline layout first
281
+ const firstInline = withLabel(parts[0]);
282
+ if (!wrap || stringCellWidth(firstInline) <= width) {
283
+ const lines = [
284
+ firstInline,
285
+ ...parts.slice(1).map((part) => `${indent}${part}`),
286
+ ];
287
+ if (balanceText && !parts.some((p) => p.includes('Balance '))) {
288
+ lines.push(`${indent}${balanceText}`);
289
+ }
290
+ return lines;
257
291
  }
258
- return lines;
292
+ // Overflow — break all
293
+ return [label, ...details.map((d) => `${detailIndent}${d}`)];
259
294
  }
260
295
  if (balanceText) {
261
- return [withLabel(balanceText)];
296
+ return maybeBreak(balanceText, [balanceText]);
262
297
  }
263
298
  // Fallback: single value from top-level remainingPercent
264
299
  const percent = quota.remainingPercent === undefined
265
300
  ? '?'
266
301
  : `${Math.round(quota.remainingPercent)}%`;
267
- const reset = compactReset(quota.resetAt);
268
- return [withLabel(`Remaining ${percent}${reset ? ` Rst ${reset}` : ''}`)];
302
+ const reset = compactReset(quota.resetAt, 'Rst');
303
+ const fallbackText = `Remaining ${percent}${reset ? ` Rst ${reset}` : ''}`;
304
+ return maybeBreak(fallbackText, [fallbackText]);
269
305
  }
270
- function compactReset(iso) {
306
+ function compactReset(iso, resetLabel) {
271
307
  if (!iso)
272
308
  return undefined;
273
309
  const timestamp = Date.parse(iso);
274
310
  if (Number.isNaN(timestamp))
275
311
  return undefined;
276
312
  const value = new Date(timestamp);
313
+ // RightCode subscriptions are displayed as an expiry date (MM-DD), not a time.
314
+ // Using UTC here makes the output stable across time zones for ISO `...Z` input.
315
+ if (typeof resetLabel === 'string' && resetLabel.startsWith('Exp')) {
316
+ const two = (num) => `${num}`.padStart(2, '0');
317
+ return `${two(value.getUTCMonth() + 1)}-${two(value.getUTCDate())}`;
318
+ }
277
319
  const now = new Date();
278
320
  const sameDay = value.getFullYear() === now.getFullYear() &&
279
321
  value.getMonth() === now.getMonth() &&
@@ -475,7 +517,7 @@ export function renderToastMessage(period, usage, quotas, options) {
475
517
  const pct = win.remainingPercent === undefined
476
518
  ? '-'
477
519
  : `${win.remainingPercent.toFixed(1)}%`;
478
- const reset = compactReset(win.resetAt);
520
+ const reset = compactReset(win.resetAt, win.resetLabel);
479
521
  const parts = [win.label];
480
522
  if (showPercent)
481
523
  parts.push(pct);
@@ -505,7 +547,7 @@ export function renderToastMessage(period, usage, quotas, options) {
505
547
  const percent = item.remainingPercent === undefined
506
548
  ? '-'
507
549
  : `${item.remainingPercent.toFixed(1)}%`;
508
- const reset = compactReset(item.resetAt);
550
+ const reset = compactReset(item.resetAt, 'Rst');
509
551
  return [
510
552
  {
511
553
  label: quotaDisplayLabel(item),
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.4.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",