@leo000001/opencode-quota-sidebar 4.0.13 → 4.0.15

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.
@@ -1,6 +1,6 @@
1
- import type { QuotaSnapshot } from "./types.js";
2
- import { type UsageSummary } from "./usage.js";
3
- import type { HistoryUsageResult } from "./usage_service.js";
1
+ import type { QuotaSnapshot } from './types.js';
2
+ import { type UsageSummary } from './usage.js';
3
+ import type { HistoryUsageResult } from './usage_service.js';
4
4
  export declare function renderCliDashboard(input: {
5
5
  label: string;
6
6
  usage: UsageSummary;
@@ -14,4 +14,4 @@ export declare function renderCliHistoryDashboard(input: {
14
14
  width?: number;
15
15
  showCost?: boolean;
16
16
  }): string;
17
- export declare function cliCurrentLabel(period: "day" | "week" | "month"): "Today" | "This Week" | "This Month";
17
+ export declare function cliCurrentLabel(period: 'day' | 'week' | 'month'): "Today" | "This Week" | "This Month";
@@ -1,8 +1,8 @@
1
- import { canonicalProviderID, collapseQuotaSnapshots, quotaDisplayLabel, } from "./quota_render.js";
2
- import { getCacheCoverageMetrics, getProviderCacheCoverageMetrics, } from "./usage.js";
1
+ import { canonicalProviderID, collapseQuotaSnapshots, quotaDisplayLabel, } from './quota_render.js';
2
+ import { getCacheCoverageMetrics, getProviderCacheCoverageMetrics, } from './usage.js';
3
3
  function shortNumber(value, decimals = 1) {
4
4
  if (!Number.isFinite(value) || value < 0)
5
- return "0";
5
+ return '0';
6
6
  if (value >= 1_000_000)
7
7
  return `${(value / 1_000_000).toFixed(decimals)}m`;
8
8
  if (value >= 1000) {
@@ -16,63 +16,51 @@ function shortNumber(value, decimals = 1) {
16
16
  }
17
17
  function formatCurrency(value, currency) {
18
18
  const safe = Number.isFinite(value) ? value : 0;
19
- const prefix = typeof currency === "string" && currency ? currency : "$";
19
+ const prefix = typeof currency === 'string' && currency ? currency : '$';
20
20
  if (safe === 0)
21
21
  return `${prefix}0.00`;
22
22
  if (safe < 10 && safe > -10)
23
- return `${safe < 0 ? "-" : ""}${prefix}${Math.abs(safe).toFixed(2)}`;
24
- const rounded = Math.abs(safe).toFixed(1).replace(/\.0$/, "");
25
- return `${safe < 0 ? "-" : ""}${prefix}${rounded}`;
23
+ return `${safe < 0 ? '-' : ''}${prefix}${Math.abs(safe).toFixed(2)}`;
24
+ const rounded = Math.abs(safe).toFixed(1).replace(/\.0$/, '');
25
+ return `${safe < 0 ? '-' : ''}${prefix}${rounded}`;
26
26
  }
27
27
  function formatApiCost(value) {
28
- return formatCurrency(value, "$");
28
+ return formatCurrency(value, '$');
29
29
  }
30
30
  function formatPercent(value, decimals = 1) {
31
31
  const safe = Number.isFinite(value) && value >= 0 ? value : 0;
32
32
  const pct = (safe * 100).toFixed(decimals);
33
- return `${pct.replace(/\.0+$/, "").replace(/(\.\d*[1-9])0+$/, "$1")}%`;
33
+ return `${pct.replace(/\.0+$/, '').replace(/(\.\d*[1-9])0+$/, '$1')}%`;
34
34
  }
35
35
  function compactCountdown(iso) {
36
36
  if (!iso)
37
- return "n/a";
37
+ return 'n/a';
38
38
  const timestamp = Date.parse(iso);
39
39
  if (Number.isNaN(timestamp))
40
- return "n/a";
40
+ return 'n/a';
41
41
  const remainingMs = timestamp - Date.now();
42
42
  if (!Number.isFinite(remainingMs))
43
- return "n/a";
43
+ return 'n/a';
44
44
  if (remainingMs <= 0)
45
- return "0m";
45
+ return '0m';
46
46
  const totalMinutes = Math.max(1, Math.floor(remainingMs / 60_000));
47
47
  if (totalMinutes < 60)
48
48
  return `${totalMinutes}m`;
49
49
  if (totalMinutes < 24 * 60) {
50
50
  const hours = Math.floor(totalMinutes / 60);
51
51
  const minutes = totalMinutes % 60;
52
- return `${hours}h${`${minutes}`.padStart(2, "0")}m`;
52
+ return `${hours}h${`${minutes}`.padStart(2, '0')}m`;
53
53
  }
54
54
  const days = Math.floor(totalMinutes / (24 * 60));
55
55
  const hours = Math.floor((totalMinutes % (24 * 60)) / 60);
56
- return `${days}D${`${hours}`.padStart(2, "0")}h`;
56
+ return `${days}D${`${hours}`.padStart(2, '0')}h`;
57
57
  }
58
58
  function gauge(value, width = 10) {
59
59
  if (value === undefined || !Number.isFinite(value))
60
- return `${"".repeat(width)} n/a`;
60
+ return `${''.repeat(width)} n/a`;
61
61
  const ratio = Math.max(0, Math.min(1, value / 100));
62
62
  const filled = Math.max(value > 0 ? 1 : 0, Math.round(ratio * width));
63
- return `${"".repeat(filled)}${"".repeat(width - filled)} ${`${Math.round(value)}`.padStart(3, " ")}%`;
64
- }
65
- function formatDelta(current, previous, format) {
66
- if (previous === undefined)
67
- return `${format(current)} now`;
68
- if (!Number.isFinite(previous) || previous < 0)
69
- return `${format(current)} now`;
70
- if (previous === 0)
71
- return `${format(current)} now, ${current === 0 ? "flat" : "new"}`;
72
- const delta = ((current - previous) / previous) * 100;
73
- const rounded = Math.abs(delta) >= 10 ? delta.toFixed(0) : delta.toFixed(1);
74
- const normalized = rounded.replace(/\.0$/, "");
75
- return `${format(current)} now, ${delta > 0 ? "+" : ""}${normalized}%`;
63
+ return `${''.repeat(filled)}${''.repeat(width - filled)} ${`${Math.round(value)}`.padStart(3, ' ')}%`;
76
64
  }
77
65
  function clip(value, width) {
78
66
  return value.length <= width
@@ -85,58 +73,61 @@ function centerLine(value, width) {
85
73
  return clipped;
86
74
  const left = Math.floor((width - clipped.length) / 2);
87
75
  const right = width - clipped.length - left;
88
- return `${" ".repeat(left)}${clipped}${" ".repeat(right)}`;
76
+ return `${' '.repeat(left)}${clipped}${' '.repeat(right)}`;
89
77
  }
90
78
  function padRight(value, width) {
91
- return clip(value, width).padEnd(width, " ");
79
+ return clip(value, width).padEnd(width, ' ');
92
80
  }
93
- function box(title, lines, width = 78) {
81
+ function box(title, lines, maxWidth = 78) {
94
82
  const longestLine = lines.reduce((max, line) => Math.max(max, line.length), 0);
95
- const inner = Math.max(48, width, title.length, longestLine);
83
+ // `maxWidth` is a hard ceiling. We intentionally avoid a fixed minimum so
84
+ // the CLI rules shrink to the rendered content instead of protruding past it.
85
+ const contentWidth = Math.max(title.length, longestLine);
86
+ const inner = Math.max(1, Math.min(maxWidth, contentWidth));
96
87
  const top = centerLine(title, inner);
97
- const rule = "".repeat(inner);
88
+ const rule = ''.repeat(inner);
98
89
  const body = lines.map((line) => clip(line, inner));
99
- return [top, rule, ...body, rule].join("\n");
90
+ return [top, rule, ...body, rule].join('\n');
100
91
  }
101
92
  function currentLabel(period) {
102
- if (period === "day")
103
- return "Today";
104
- if (period === "week")
105
- return "This Week";
106
- return "This Month";
93
+ if (period === 'day')
94
+ return 'Today';
95
+ if (period === 'week')
96
+ return 'This Week';
97
+ return 'This Month';
107
98
  }
108
99
  function historyLabel(result) {
109
- if (result.period === "day")
100
+ if (result.period === 'day')
110
101
  return `Daily since ${result.since.raw}`;
111
- if (result.period === "week")
102
+ if (result.period === 'week')
112
103
  return `Weekly since ${result.since.raw}`;
113
104
  return `Monthly since ${result.since.raw}`;
114
105
  }
115
106
  function quotaRows(quotas) {
116
- const visible = collapseQuotaSnapshots(quotas).filter((item) => item.status === "ok" || item.status === "error");
107
+ const visible = collapseQuotaSnapshots(quotas).filter((item) => item.status === 'ok' || item.status === 'error');
117
108
  if (visible.length === 0)
118
- return ["no provider quota data available"];
109
+ return ['no provider quota data available'];
119
110
  return visible.flatMap((quota) => {
120
- const label = quotaDisplayLabel(quota).padEnd(11, " ");
121
- if (quota.status === "error") {
122
- return [`${label} error${quota.note ? ` · ${quota.note}` : ""}`];
111
+ const label = quotaDisplayLabel(quota).padEnd(11, ' ');
112
+ if (quota.status === 'error') {
113
+ return [`${label} error${quota.note ? ` · ${quota.note}` : ''}`];
123
114
  }
124
115
  if (quota.windows && quota.windows.length > 0) {
125
116
  const lines = quota.windows.map((win) => {
126
- const detail = padRight(win.label || "quota", 18);
117
+ const detail = padRight(win.label || 'quota', 18);
127
118
  if (win.showPercent === false) {
128
119
  return `${label}${detail} ${compactCountdown(win.resetAt)}`;
129
120
  }
130
121
  return `${label}${detail} [${gauge(win.remainingPercent)}] ${compactCountdown(win.resetAt)}`;
131
122
  });
132
123
  if (quota.balance) {
133
- lines.push(`${label}${padRight("balance", 18)} ${formatCurrency(quota.balance.amount, quota.balance.currency)}`);
124
+ lines.push(`${label}${padRight('balance', 18)} ${formatCurrency(quota.balance.amount, quota.balance.currency)}`);
134
125
  }
135
126
  return lines;
136
127
  }
137
128
  if (quota.balance) {
138
129
  return [
139
- `${label}${padRight("balance", 18)} ${formatCurrency(quota.balance.amount, quota.balance.currency)}`,
130
+ `${label}${padRight('balance', 18)} ${formatCurrency(quota.balance.amount, quota.balance.currency)}`,
140
131
  ];
141
132
  }
142
133
  return [
@@ -147,22 +138,22 @@ function quotaRows(quotas) {
147
138
  function providerRows(usage, showCost) {
148
139
  const providers = Object.values(usage.providers).sort((a, b) => b.total - a.total);
149
140
  if (providers.length === 0)
150
- return ["no provider activity"];
141
+ return ['no provider activity'];
151
142
  return providers.map((provider) => {
152
143
  const cache = getProviderCacheCoverageMetrics(provider).cachedRatio;
153
- const base = `${quotaDisplayLabel({ providerID: provider.providerID, label: provider.providerID, status: "ok", checkedAt: 0 }).padEnd(10, " ")} ${shortNumber(provider.assistantMessages).padStart(4, " ")} req ${shortNumber(provider.total).padStart(7, " ")} tok ${(cache !== undefined ? formatPercent(cache, 0) : "-").padStart(4, " ")} cache`;
154
- const apiCost = canonicalProviderID(provider.providerID) === "github-copilot"
155
- ? "-"
144
+ const base = `${quotaDisplayLabel({ providerID: provider.providerID, label: provider.providerID, status: 'ok', checkedAt: 0 }).padEnd(10, ' ')} ${shortNumber(provider.assistantMessages).padStart(4, ' ')} req ${shortNumber(provider.total).padStart(7, ' ')} tok ${(cache !== undefined ? formatPercent(cache, 0) : '-').padStart(4, ' ')} cache`;
145
+ const apiCost = canonicalProviderID(provider.providerID) === 'github-copilot'
146
+ ? '-'
156
147
  : formatApiCost(provider.apiCost);
157
- return showCost ? `${base} ${apiCost.padStart(7, " ")}` : base;
148
+ return showCost ? `${base} ${apiCost.padStart(7, ' ')}` : base;
158
149
  });
159
150
  }
160
151
  function cliApiCostSummary(usage) {
161
152
  const providers = Object.values(usage.providers);
162
153
  if (providers.length === 0)
163
154
  return formatApiCost(usage.apiCost);
164
- const hasNonCopilot = providers.some((provider) => canonicalProviderID(provider.providerID) !== "github-copilot");
165
- return hasNonCopilot ? formatApiCost(usage.apiCost) : "-";
155
+ const hasNonCopilot = providers.some((provider) => canonicalProviderID(provider.providerID) !== 'github-copilot');
156
+ return hasNonCopilot ? formatApiCost(usage.apiCost) : '-';
166
157
  }
167
158
  function totalsRows(input) {
168
159
  const left = [`Requests ${input.requests}`, `Tokens ${input.tokens}`];
@@ -172,23 +163,23 @@ function totalsRows(input) {
172
163
  ];
173
164
  const metaLeft = input.periods ? `Periods ${input.periods}` : undefined;
174
165
  const metaRight = input.current ? `Current ${input.current}` : undefined;
175
- const row1 = [left[0], left[1], ...right].join(" ");
176
- const row2 = [metaLeft, metaRight].filter(Boolean).join(" ");
166
+ const row1 = [left[0], left[1], ...right].join(' ');
167
+ const row2 = [metaLeft, metaRight].filter(Boolean).join(' ');
177
168
  return [row1, ...(row2 ? [row2] : [])];
178
169
  }
179
170
  function trendBar(value, maxValue, width = 20) {
180
171
  if (!Number.isFinite(value) || value <= 0 || maxValue <= 0) {
181
- return "".repeat(width);
172
+ return ''.repeat(width);
182
173
  }
183
174
  const filled = Math.max(1, Math.round((value / maxValue) * width));
184
- return `${"".repeat(filled)}${"".repeat(width - filled)}`;
175
+ return `${''.repeat(filled)}${''.repeat(width - filled)}`;
185
176
  }
186
177
  function trendMetricBlock(input) {
187
178
  const visibleRows = input.rows.slice(-Math.min(8, input.rows.length));
188
179
  const values = visibleRows.map(input.pick);
189
180
  const maxValue = Math.max(...values, 0);
190
181
  const currentValue = input.current ? input.pick(input.current) : 0;
191
- const displayLabels = visibleRows.map((row) => `${row.range.shortLabel}${row.range.isCurrent ? "*" : ""}`);
182
+ const displayLabels = visibleRows.map((row) => `${row.range.shortLabel}${row.range.isCurrent ? '*' : ''}`);
192
183
  const labelWidth = Math.max(8, Math.min(28, Math.max(...displayLabels.map((label) => label.length), 8)));
193
184
  return [
194
185
  `${input.label} ${input.format(currentValue)}`,
@@ -200,29 +191,29 @@ function trendMetricBlock(input) {
200
191
  ];
201
192
  }
202
193
  export function renderCliDashboard(input) {
203
- const width = input.width ?? 78;
194
+ const maxWidth = input.width ?? 78;
204
195
  const showCost = input.showCost !== false;
205
196
  const cache = getCacheCoverageMetrics(input.usage).cachedRatio;
206
197
  return box(`opencode-quota · ${input.label}`, [
207
- "QUOTA",
198
+ 'QUOTA',
208
199
  ...quotaRows(input.quotas),
209
- "",
210
- "TOTALS",
200
+ '',
201
+ 'TOTALS',
211
202
  ...totalsRows({
212
203
  requests: shortNumber(input.usage.assistantMessages),
213
204
  tokens: shortNumber(input.usage.total),
214
205
  ...(showCost ? { cost: cliApiCostSummary(input.usage) } : {}),
215
- cache: cache !== undefined ? formatPercent(cache, 1) : "-",
206
+ cache: cache !== undefined ? formatPercent(cache, 1) : '-',
216
207
  periods: `${input.usage.sessionCount}`,
217
208
  }),
218
209
  `Input ${shortNumber(input.usage.input)} Output ${shortNumber(input.usage.output)}`,
219
- "",
220
- "PROVIDERS",
210
+ '',
211
+ 'PROVIDERS',
221
212
  ...providerRows(input.usage, showCost),
222
- ], width);
213
+ ], maxWidth);
223
214
  }
224
215
  export function renderCliHistoryDashboard(input) {
225
- const width = input.width ?? 78;
216
+ const maxWidth = input.width ?? 78;
226
217
  const showCost = input.showCost !== false;
227
218
  const rows = input.result.rows;
228
219
  const current = [...rows].reverse().find((row) => row.range.isCurrent) || rows.at(-1);
@@ -231,23 +222,23 @@ export function renderCliHistoryDashboard(input) {
231
222
  const cache = getCacheCoverageMetrics(input.result.total).cachedRatio;
232
223
  const trendBlocks = [
233
224
  ...trendMetricBlock({
234
- label: "Requests",
225
+ label: 'Requests',
235
226
  rows,
236
227
  current,
237
228
  pick: (row) => row.usage.assistantMessages,
238
229
  format: (value) => shortNumber(value),
239
230
  }),
240
- "",
231
+ '',
241
232
  ...trendMetricBlock({
242
- label: "Tokens",
233
+ label: 'Tokens',
243
234
  rows,
244
235
  current,
245
236
  pick: (row) => row.usage.total,
246
237
  format: (value) => shortNumber(value),
247
238
  }),
248
- "",
239
+ '',
249
240
  ...trendMetricBlock({
250
- label: "Cache",
241
+ label: 'Cache',
251
242
  rows,
252
243
  current,
253
244
  pick: (row) => getCacheCoverageMetrics(row.usage).cachedRatio ?? 0,
@@ -255,9 +246,9 @@ export function renderCliHistoryDashboard(input) {
255
246
  }),
256
247
  ...(showCost
257
248
  ? [
258
- "",
249
+ '',
259
250
  ...trendMetricBlock({
260
- label: "API Cost",
251
+ label: 'API Cost',
261
252
  rows,
262
253
  current,
263
254
  pick: (row) => row.usage.apiCost,
@@ -267,25 +258,25 @@ export function renderCliHistoryDashboard(input) {
267
258
  : []),
268
259
  ];
269
260
  return box(`opencode-quota · ${historyLabel(input.result)}`, [
270
- "QUOTA",
261
+ 'QUOTA',
271
262
  ...quotaRows(input.quotas),
272
- "",
273
- "TOTALS",
263
+ '',
264
+ 'TOTALS',
274
265
  ...totalsRows({
275
266
  requests: shortNumber(input.result.total.assistantMessages),
276
267
  tokens: shortNumber(input.result.total.total),
277
268
  ...(showCost ? { cost: cliApiCostSummary(input.result.total) } : {}),
278
- cache: cache !== undefined ? formatPercent(cache, 1) : "-",
269
+ cache: cache !== undefined ? formatPercent(cache, 1) : '-',
279
270
  periods: `${rows.length}`,
280
- current: current?.range.shortLabel || "-",
271
+ current: current?.range.shortLabel || '-',
281
272
  }),
282
- "",
283
- "PROVIDERS",
273
+ '',
274
+ 'PROVIDERS',
284
275
  ...providerRows(input.result.total, showCost),
285
- "",
286
- "TREND",
276
+ '',
277
+ 'TREND',
287
278
  ...trendBlocks,
288
- ], width);
279
+ ], maxWidth);
289
280
  }
290
281
  export function cliCurrentLabel(period) {
291
282
  return currentLabel(period);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leo000001/opencode-quota-sidebar",
3
- "version": "4.0.13",
3
+ "version": "4.0.15",
4
4
  "description": "OpenCode plugin that shows quota and token usage in TUI sidebar panels and compact session titles",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",