@leo000001/opencode-quota-sidebar 4.0.13 → 4.0.16

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,51 +138,85 @@ 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"];
151
- return providers.map((provider) => {
141
+ return ['no provider activity'];
142
+ const prepared = 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
- ? "-"
156
- : formatApiCost(provider.apiCost);
157
- return showCost ? `${base} ${apiCost.padStart(7, " ")}` : base;
144
+ return {
145
+ label: quotaDisplayLabel({
146
+ providerID: provider.providerID,
147
+ label: provider.providerID,
148
+ status: 'ok',
149
+ checkedAt: 0,
150
+ }),
151
+ requests: shortNumber(provider.assistantMessages),
152
+ tokens: shortNumber(provider.total),
153
+ cached: cache !== undefined ? formatPercent(cache, 0) : '-',
154
+ cost: canonicalProviderID(provider.providerID) === 'github-copilot'
155
+ ? '-'
156
+ : formatApiCost(provider.apiCost),
157
+ };
158
+ });
159
+ const labelWidth = Math.max(...prepared.map((row) => row.label.length));
160
+ const requestWidth = Math.max(...prepared.map((row) => row.requests.length));
161
+ const tokenWidth = Math.max(...prepared.map((row) => row.tokens.length));
162
+ const cachedWidth = Math.max(...prepared.map((row) => row.cached.length));
163
+ const costWidth = Math.max(...prepared.map((row) => row.cost.length));
164
+ return prepared.map((row) => {
165
+ const base = [
166
+ padRight(row.label, labelWidth),
167
+ `${row.requests.padStart(requestWidth)} req`,
168
+ `${row.tokens.padStart(tokenWidth)} tok`,
169
+ `${row.cached.padStart(cachedWidth)} cached`,
170
+ ].join(' ');
171
+ return showCost ? `${base} ${row.cost.padStart(costWidth)}` : base;
158
172
  });
159
173
  }
160
174
  function cliApiCostSummary(usage) {
161
175
  const providers = Object.values(usage.providers);
162
176
  if (providers.length === 0)
163
177
  return formatApiCost(usage.apiCost);
164
- const hasNonCopilot = providers.some((provider) => canonicalProviderID(provider.providerID) !== "github-copilot");
165
- return hasNonCopilot ? formatApiCost(usage.apiCost) : "-";
178
+ const hasNonCopilot = providers.some((provider) => canonicalProviderID(provider.providerID) !== 'github-copilot');
179
+ return hasNonCopilot ? formatApiCost(usage.apiCost) : '-';
166
180
  }
167
181
  function totalsRows(input) {
168
- const left = [`Requests ${input.requests}`, `Tokens ${input.tokens}`];
169
- const right = [
170
- ...(input.cost ? [`API Cost ${input.cost}`] : []),
171
- ...(input.cache ? [`Cache ${input.cache}`] : []),
172
- ];
173
- const metaLeft = input.periods ? `Periods ${input.periods}` : undefined;
174
- 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(" ");
177
- return [row1, ...(row2 ? [row2] : [])];
182
+ const leftLabelWidth = Math.max('Sessions'.length, 'API Cost'.length);
183
+ const leftValueWidth = Math.max(input.sessions.length, input.tokens.length, input.cost?.length ?? 0);
184
+ const rightLabelWidth = 'Requests'.length;
185
+ const rightValueWidth = Math.max(input.requests.length, input.cached?.length ?? 0);
186
+ const leftCell = (label, value) => `${padRight(label, leftLabelWidth)} ${value.padEnd(leftValueWidth, ' ')}`;
187
+ const rightCell = (label, value) => `${padRight(label, rightLabelWidth)} ${value.padEnd(rightValueWidth, ' ')}`;
188
+ const row1 = [
189
+ leftCell('Sessions', input.sessions),
190
+ rightCell('Requests', input.requests),
191
+ ]
192
+ .join(' ')
193
+ .trimEnd();
194
+ const row2 = [
195
+ leftCell('Tokens', input.tokens),
196
+ ...(input.cached ? [rightCell('Cached', input.cached)] : []),
197
+ ]
198
+ .join(' ')
199
+ .trimEnd();
200
+ const row3 = input.cost
201
+ ? leftCell('API Cost', input.cost).trimEnd()
202
+ : undefined;
203
+ return [row1, row2, ...(row3 ? [row3] : [])];
178
204
  }
179
- function trendBar(value, maxValue, width = 20) {
205
+ function trendBar(value, maxValue, width = 16) {
180
206
  if (!Number.isFinite(value) || value <= 0 || maxValue <= 0) {
181
- return "".repeat(width);
207
+ return ''.repeat(width);
182
208
  }
183
209
  const filled = Math.max(1, Math.round((value / maxValue) * width));
184
- return `${"".repeat(filled)}${"".repeat(width - filled)}`;
210
+ return `${''.repeat(filled)}${''.repeat(width - filled)}`;
185
211
  }
186
212
  function trendMetricBlock(input) {
187
213
  const visibleRows = input.rows.slice(-Math.min(8, input.rows.length));
188
214
  const values = visibleRows.map(input.pick);
189
215
  const maxValue = Math.max(...values, 0);
190
- const currentValue = input.current ? input.pick(input.current) : 0;
191
- const displayLabels = visibleRows.map((row) => `${row.range.shortLabel}${row.range.isCurrent ? "*" : ""}`);
192
- const labelWidth = Math.max(8, Math.min(28, Math.max(...displayLabels.map((label) => label.length), 8)));
216
+ const displayLabels = visibleRows.map((row) => `${row.range.shortLabel}${row.range.isCurrent ? '*' : ''}`);
217
+ const labelWidth = Math.max(6, Math.min(20, Math.max(...displayLabels.map((label) => label.length), 6)));
193
218
  return [
194
- `${input.label} ${input.format(currentValue)}`,
219
+ input.label,
195
220
  ...visibleRows.map((row, index) => {
196
221
  const value = input.pick(row);
197
222
  const tag = padRight(displayLabels[index], labelWidth);
@@ -200,54 +225,59 @@ function trendMetricBlock(input) {
200
225
  ];
201
226
  }
202
227
  export function renderCliDashboard(input) {
203
- const width = input.width ?? 78;
228
+ const maxWidth = input.width ?? 78;
204
229
  const showCost = input.showCost !== false;
205
230
  const cache = getCacheCoverageMetrics(input.usage).cachedRatio;
206
231
  return box(`opencode-quota · ${input.label}`, [
207
- "QUOTA",
232
+ 'QUOTA',
208
233
  ...quotaRows(input.quotas),
209
- "",
210
- "TOTALS",
234
+ '',
235
+ 'TOTALS',
211
236
  ...totalsRows({
237
+ sessions: `${input.usage.sessionCount}`,
212
238
  requests: shortNumber(input.usage.assistantMessages),
213
239
  tokens: shortNumber(input.usage.total),
214
240
  ...(showCost ? { cost: cliApiCostSummary(input.usage) } : {}),
215
- cache: cache !== undefined ? formatPercent(cache, 1) : "-",
216
- periods: `${input.usage.sessionCount}`,
241
+ cached: cache !== undefined ? formatPercent(cache, 1) : '-',
217
242
  }),
218
- `Input ${shortNumber(input.usage.input)} Output ${shortNumber(input.usage.output)}`,
219
- "",
220
- "PROVIDERS",
243
+ '',
244
+ 'PROVIDERS',
221
245
  ...providerRows(input.usage, showCost),
222
- ], width);
246
+ ], maxWidth);
223
247
  }
224
248
  export function renderCliHistoryDashboard(input) {
225
- const width = input.width ?? 78;
249
+ const maxWidth = input.width ?? 78;
226
250
  const showCost = input.showCost !== false;
227
251
  const rows = input.result.rows;
228
252
  const current = [...rows].reverse().find((row) => row.range.isCurrent) || rows.at(-1);
229
- const currentIndex = current ? rows.indexOf(current) : -1;
230
- const previous = currentIndex > 0 ? rows[currentIndex - 1] : undefined;
231
253
  const cache = getCacheCoverageMetrics(input.result.total).cachedRatio;
232
254
  const trendBlocks = [
233
255
  ...trendMetricBlock({
234
- label: "Requests",
256
+ label: 'Sessions',
257
+ rows,
258
+ current,
259
+ pick: (row) => row.usage.sessionCount,
260
+ format: (value) => shortNumber(value),
261
+ }),
262
+ '',
263
+ ...trendMetricBlock({
264
+ label: 'Requests',
235
265
  rows,
236
266
  current,
237
267
  pick: (row) => row.usage.assistantMessages,
238
268
  format: (value) => shortNumber(value),
239
269
  }),
240
- "",
270
+ '',
241
271
  ...trendMetricBlock({
242
- label: "Tokens",
272
+ label: 'Tokens',
243
273
  rows,
244
274
  current,
245
275
  pick: (row) => row.usage.total,
246
276
  format: (value) => shortNumber(value),
247
277
  }),
248
- "",
278
+ '',
249
279
  ...trendMetricBlock({
250
- label: "Cache",
280
+ label: 'Cached',
251
281
  rows,
252
282
  current,
253
283
  pick: (row) => getCacheCoverageMetrics(row.usage).cachedRatio ?? 0,
@@ -255,9 +285,9 @@ export function renderCliHistoryDashboard(input) {
255
285
  }),
256
286
  ...(showCost
257
287
  ? [
258
- "",
288
+ '',
259
289
  ...trendMetricBlock({
260
- label: "API Cost",
290
+ label: 'API Cost',
261
291
  rows,
262
292
  current,
263
293
  pick: (row) => row.usage.apiCost,
@@ -267,25 +297,24 @@ export function renderCliHistoryDashboard(input) {
267
297
  : []),
268
298
  ];
269
299
  return box(`opencode-quota · ${historyLabel(input.result)}`, [
270
- "QUOTA",
300
+ 'QUOTA',
271
301
  ...quotaRows(input.quotas),
272
- "",
273
- "TOTALS",
302
+ '',
303
+ 'TOTALS',
274
304
  ...totalsRows({
305
+ sessions: `${input.result.total.sessionCount}`,
275
306
  requests: shortNumber(input.result.total.assistantMessages),
276
307
  tokens: shortNumber(input.result.total.total),
277
308
  ...(showCost ? { cost: cliApiCostSummary(input.result.total) } : {}),
278
- cache: cache !== undefined ? formatPercent(cache, 1) : "-",
279
- periods: `${rows.length}`,
280
- current: current?.range.shortLabel || "-",
309
+ cached: cache !== undefined ? formatPercent(cache, 1) : '-',
281
310
  }),
282
- "",
283
- "PROVIDERS",
311
+ '',
312
+ 'PROVIDERS',
284
313
  ...providerRows(input.result.total, showCost),
285
- "",
286
- "TREND",
314
+ '',
315
+ 'TREND',
287
316
  ...trendBlocks,
288
- ], width);
317
+ ], maxWidth);
289
318
  }
290
319
  export function cliCurrentLabel(period) {
291
320
  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.16",
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",