@leo000001/opencode-quota-sidebar 1.8.0 → 1.11.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/README.md +4 -0
- package/dist/format.js +67 -30
- package/dist/index.js +0 -1
- package/dist/providers/core/copilot.js +7 -3
- package/dist/storage.js +2 -0
- package/dist/title.js +54 -17
- package/dist/title_apply.d.ts +0 -3
- package/dist/title_apply.js +1 -1
- package/dist/types.d.ts +6 -0
- package/package.json +1 -1
- package/quota-sidebar.config.example.json +2 -0
package/README.md
CHANGED
|
@@ -171,8 +171,10 @@ Example config:
|
|
|
171
171
|
"sidebar": {
|
|
172
172
|
"enabled": true,
|
|
173
173
|
"width": 36,
|
|
174
|
+
"multilineTitle": true,
|
|
174
175
|
"showCost": true,
|
|
175
176
|
"showQuota": true,
|
|
177
|
+
"wrapQuotaLines": true,
|
|
176
178
|
"includeChildren": true,
|
|
177
179
|
"childrenMaxDepth": 6,
|
|
178
180
|
"childrenMaxSessions": 128,
|
|
@@ -202,6 +204,8 @@ Notes:
|
|
|
202
204
|
|
|
203
205
|
- `sidebar.showCost` controls API-cost visibility in sidebar title, `quota_summary` markdown report, and toast message.
|
|
204
206
|
- `sidebar.width` is measured in terminal cells. CJK/emoji truncation is best-effort to avoid sidebar overflow.
|
|
207
|
+
- `sidebar.multilineTitle` controls multi-line sidebar layout (default: `true`). Set `false` for compact single-line title.
|
|
208
|
+
- `sidebar.wrapQuotaLines` controls quota line wrapping and continuation indentation (default: `true`).
|
|
205
209
|
- `sidebar.includeChildren` controls whether session-scoped usage/quota includes descendant subagent sessions (default: `true`).
|
|
206
210
|
- `sidebar.childrenMaxDepth` limits how many levels of nested subagents are traversed (default: `6`, clamped 1–32).
|
|
207
211
|
- `sidebar.childrenMaxSessions` caps the total number of descendant sessions aggregated (default: `128`, clamped 0–2000).
|
package/dist/format.js
CHANGED
|
@@ -162,6 +162,56 @@ function alignPairs(pairs, indent = ' ') {
|
|
|
162
162
|
return `${indent}${padEndCells(pair.label, labelWidth)} ${pair.value}`;
|
|
163
163
|
});
|
|
164
164
|
}
|
|
165
|
+
function compactQuotaInline(quota) {
|
|
166
|
+
const label = sanitizeLine(quotaDisplayLabel(quota));
|
|
167
|
+
if (quota.status !== 'ok')
|
|
168
|
+
return label;
|
|
169
|
+
if (quota.windows && quota.windows.length > 0) {
|
|
170
|
+
const first = quota.windows[0];
|
|
171
|
+
const showPercent = first.showPercent !== false;
|
|
172
|
+
const firstLabel = sanitizeLine(first.label || '');
|
|
173
|
+
const pct = first.remainingPercent === undefined
|
|
174
|
+
? undefined
|
|
175
|
+
: `${Math.round(first.remainingPercent)}%`;
|
|
176
|
+
const summary = showPercent
|
|
177
|
+
? [firstLabel, pct].filter(Boolean).join(' ')
|
|
178
|
+
: firstLabel.replace(/^Daily\s+/i, '') || firstLabel;
|
|
179
|
+
const hasMore = quota.windows.length > 1 ||
|
|
180
|
+
(quota.balance !== undefined && !summary.includes('Balance '));
|
|
181
|
+
return `${label}${summary ? ` ${summary}` : ''}${hasMore ? '+' : ''}`;
|
|
182
|
+
}
|
|
183
|
+
if (quota.balance) {
|
|
184
|
+
return `${label} ${formatCurrency(quota.balance.amount, quota.balance.currency)}`;
|
|
185
|
+
}
|
|
186
|
+
if (quota.remainingPercent !== undefined) {
|
|
187
|
+
return `${label} ${Math.round(quota.remainingPercent)}%`;
|
|
188
|
+
}
|
|
189
|
+
return label;
|
|
190
|
+
}
|
|
191
|
+
function renderSingleLineTitle(baseTitle, usage, quotas, config, width) {
|
|
192
|
+
const baseBudget = Math.min(16, Math.max(8, Math.floor(width * 0.35)));
|
|
193
|
+
const base = fitLine(baseTitle, baseBudget);
|
|
194
|
+
const segments = [
|
|
195
|
+
`Input ${sidebarNumber(usage.input)} Output ${sidebarNumber(usage.output)}`,
|
|
196
|
+
];
|
|
197
|
+
if (usage.cacheRead > 0) {
|
|
198
|
+
segments.push(`Cache Read ${sidebarNumber(usage.cacheRead)}`);
|
|
199
|
+
}
|
|
200
|
+
if (usage.cacheWrite > 0) {
|
|
201
|
+
segments.push(`Cache Write ${sidebarNumber(usage.cacheWrite)}`);
|
|
202
|
+
}
|
|
203
|
+
if (config.sidebar.showCost && usage.apiCost > 0) {
|
|
204
|
+
segments.push(formatApiCostLine(usage.apiCost));
|
|
205
|
+
}
|
|
206
|
+
if (config.sidebar.showQuota) {
|
|
207
|
+
const visibleQuotas = collapseQuotaSnapshots(quotas).filter((q) => ['ok', 'error', 'unsupported', 'unavailable'].includes(q.status));
|
|
208
|
+
segments.push(...visibleQuotas.map(compactQuotaInline));
|
|
209
|
+
}
|
|
210
|
+
const detail = segments.filter(Boolean).join(' | ');
|
|
211
|
+
if (!detail)
|
|
212
|
+
return fitLine(baseTitle, width);
|
|
213
|
+
return fitLine(`${base} | ${detail}`, width);
|
|
214
|
+
}
|
|
165
215
|
/**
|
|
166
216
|
* Render sidebar title with multi-line token breakdown.
|
|
167
217
|
*
|
|
@@ -175,8 +225,11 @@ function alignPairs(pairs, indent = ' ') {
|
|
|
175
225
|
*/
|
|
176
226
|
export function renderSidebarTitle(baseTitle, usage, quotas, config) {
|
|
177
227
|
const width = Math.max(8, Math.floor(config.sidebar.width || 36));
|
|
178
|
-
const lines = [];
|
|
179
228
|
const safeBaseTitle = stripAnsi(baseTitle || 'Session').split(/\r?\n/, 1)[0] || 'Session';
|
|
229
|
+
if (config.sidebar.multilineTitle !== true) {
|
|
230
|
+
return renderSingleLineTitle(safeBaseTitle, usage, quotas, config, width);
|
|
231
|
+
}
|
|
232
|
+
const lines = [];
|
|
180
233
|
lines.push(fitLine(safeBaseTitle, width));
|
|
181
234
|
lines.push('');
|
|
182
235
|
// Input / Output line
|
|
@@ -217,9 +270,13 @@ export function renderSidebarTitle(baseTitle, usage, quotas, config) {
|
|
|
217
270
|
/**
|
|
218
271
|
* Multi-window quota format for sidebar.
|
|
219
272
|
*
|
|
220
|
-
* When
|
|
273
|
+
* When provider has a single detail line and it fits:
|
|
221
274
|
* "OpenAI 5h 80% Rst 16:20"
|
|
222
|
-
*
|
|
275
|
+
*
|
|
276
|
+
* When provider has multiple detail lines (multi-window or balance + window):
|
|
277
|
+
* "OpenAI"
|
|
278
|
+
* " 5h 80% Rst 16:20"
|
|
279
|
+
* " Weekly 70% Rst 03-01"
|
|
223
280
|
*
|
|
224
281
|
* When wrapLines=true and label+content overflows width:
|
|
225
282
|
* "RC-openai"
|
|
@@ -229,7 +286,6 @@ export function renderSidebarTitle(baseTitle, usage, quotas, config) {
|
|
|
229
286
|
function compactQuotaWide(quota, labelWidth = 0, options) {
|
|
230
287
|
const label = sanitizeLine(quotaDisplayLabel(quota));
|
|
231
288
|
const labelPadded = padEndCells(label, labelWidth);
|
|
232
|
-
const indent = ' '.repeat(labelWidth + 1);
|
|
233
289
|
const detailIndent = ' ';
|
|
234
290
|
const withLabel = (content) => `${labelPadded} ${content}`;
|
|
235
291
|
const wrap = options?.wrapLines === true && (options?.width || 0) > 0;
|
|
@@ -276,34 +332,15 @@ function compactQuotaWide(quota, labelWidth = 0, options) {
|
|
|
276
332
|
if (balanceText && !parts.some((p) => p.includes('Balance '))) {
|
|
277
333
|
details.push(balanceText);
|
|
278
334
|
}
|
|
279
|
-
//
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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;
|
|
289
|
-
}
|
|
290
|
-
// Overflow — break: label on its own line, details indented
|
|
335
|
+
// Keep a unified wrapped layout for providers that have multiple detail
|
|
336
|
+
// lines so OpenAI/Copilot/others match the RightCode multi-line style,
|
|
337
|
+
// regardless of wrapLines.
|
|
338
|
+
if (details.length > 1) {
|
|
291
339
|
return [label, ...details.map((d) => `${detailIndent}${d}`)];
|
|
292
340
|
}
|
|
293
|
-
//
|
|
294
|
-
const
|
|
295
|
-
|
|
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;
|
|
304
|
-
}
|
|
305
|
-
// Overflow — break all
|
|
306
|
-
return [label, ...details.map((d) => `${detailIndent}${d}`)];
|
|
341
|
+
// Single detail line: keep inline unless width wrapping requires a break.
|
|
342
|
+
const single = details[0];
|
|
343
|
+
return maybeBreak(single, [single]);
|
|
307
344
|
}
|
|
308
345
|
if (balanceText) {
|
|
309
346
|
return maybeBreak(balanceText, [balanceText]);
|
package/dist/index.js
CHANGED
|
@@ -2,11 +2,15 @@ import { isRecord, swallow } from '../../helpers.js';
|
|
|
2
2
|
import { asNumber, configuredProviderEnabled, fetchWithTimeout, normalizePercent, toIso, } from '../common.js';
|
|
3
3
|
async function fetchCopilotQuota(ctx) {
|
|
4
4
|
const checkedAt = Date.now();
|
|
5
|
+
const sourceProviderID = typeof ctx.sourceProviderID === 'string' && ctx.sourceProviderID
|
|
6
|
+
? ctx.sourceProviderID
|
|
7
|
+
: ctx.providerID;
|
|
8
|
+
const enterprise = sourceProviderID.startsWith('github-copilot-enterprise');
|
|
5
9
|
const base = {
|
|
6
|
-
providerID:
|
|
10
|
+
providerID: sourceProviderID,
|
|
7
11
|
adapterID: 'github-copilot',
|
|
8
|
-
label: 'GitHub Copilot',
|
|
9
|
-
shortLabel: 'Copilot',
|
|
12
|
+
label: enterprise ? 'GitHub Copilot Enterprise' : 'GitHub Copilot',
|
|
13
|
+
shortLabel: enterprise ? 'Copilot Ent' : 'Copilot',
|
|
10
14
|
sortOrder: 20,
|
|
11
15
|
};
|
|
12
16
|
if (!ctx.auth) {
|
package/dist/storage.js
CHANGED
|
@@ -11,6 +11,7 @@ export const defaultConfig = {
|
|
|
11
11
|
sidebar: {
|
|
12
12
|
enabled: true,
|
|
13
13
|
width: 36,
|
|
14
|
+
multilineTitle: true,
|
|
14
15
|
showCost: true,
|
|
15
16
|
showQuota: true,
|
|
16
17
|
wrapQuotaLines: true,
|
|
@@ -64,6 +65,7 @@ export async function loadConfig(paths) {
|
|
|
64
65
|
sidebar: {
|
|
65
66
|
enabled: asBoolean(sidebar.enabled, base.sidebar.enabled),
|
|
66
67
|
width: Math.max(20, Math.min(60, asNumber(sidebar.width, base.sidebar.width))),
|
|
68
|
+
multilineTitle: asBoolean(sidebar.multilineTitle, base.sidebar.multilineTitle ?? true),
|
|
67
69
|
showCost: asBoolean(sidebar.showCost, base.sidebar.showCost),
|
|
68
70
|
showQuota: asBoolean(sidebar.showQuota, base.sidebar.showQuota),
|
|
69
71
|
wrapQuotaLines: asBoolean(sidebar.wrapQuotaLines, base.sidebar.wrapQuotaLines),
|
package/dist/title.js
CHANGED
|
@@ -1,6 +1,51 @@
|
|
|
1
|
+
function sanitizeTitleFragment(value) {
|
|
2
|
+
return stripAnsi(value)
|
|
3
|
+
.replace(/[\x00-\x1F\x7F-\x9F]/g, ' ')
|
|
4
|
+
.trimEnd();
|
|
5
|
+
}
|
|
6
|
+
function isStrongDecoratedDetail(line) {
|
|
7
|
+
if (!line)
|
|
8
|
+
return false;
|
|
9
|
+
if (/^Input\s+\S+\s+Output(?:\s+\S+)?/.test(line))
|
|
10
|
+
return true;
|
|
11
|
+
if (/^Cache\s+(Read|Write)\s+\S+/.test(line))
|
|
12
|
+
return true;
|
|
13
|
+
if (/^\$\S+\s+as API cost\b/.test(line))
|
|
14
|
+
return true;
|
|
15
|
+
// Single-line compact mode compatibility.
|
|
16
|
+
if (/^I(?:nput)?\s+\$?\d[\d.,]*[kKmM]?\s+O(?:utput)?\s+\$?\d[\d.,]*[kKmM]?$/.test(line))
|
|
17
|
+
return true;
|
|
18
|
+
if (/^C(?:ache\s*)?R(?:ead)?\s+\$?\d[\d.,]*[kKmM]?$/.test(line))
|
|
19
|
+
return true;
|
|
20
|
+
if (/^C(?:ache\s*)?W(?:rite)?\s+\$?\d[\d.,]*[kKmM]?$/.test(line))
|
|
21
|
+
return true;
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
function isQuotaLikeProviderDetail(line) {
|
|
25
|
+
if (!line)
|
|
26
|
+
return false;
|
|
27
|
+
if (!/^(OpenAI|Copilot|Anthropic|RightCode|RC)\b/.test(line))
|
|
28
|
+
return false;
|
|
29
|
+
return /\b(Rst|Exp\+?|Balance|Remaining)\b|\d{1,3}%/.test(line);
|
|
30
|
+
}
|
|
31
|
+
function decoratedSingleLineBase(line) {
|
|
32
|
+
const parts = sanitizeTitleFragment(line)
|
|
33
|
+
.split(/\s*\|\s*/)
|
|
34
|
+
.map((part) => part.trim());
|
|
35
|
+
if (parts.length < 2)
|
|
36
|
+
return undefined;
|
|
37
|
+
const details = parts.slice(1);
|
|
38
|
+
if (!details.some((detail) => isStrongDecoratedDetail(detail))) {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
return parts[0] || 'Session';
|
|
42
|
+
}
|
|
1
43
|
export function normalizeBaseTitle(title) {
|
|
2
44
|
const firstLine = stripAnsi(title).split(/\r?\n/, 1)[0] || 'Session';
|
|
3
|
-
|
|
45
|
+
const decoratedBase = decoratedSingleLineBase(firstLine);
|
|
46
|
+
if (decoratedBase)
|
|
47
|
+
return decoratedBase;
|
|
48
|
+
return sanitizeTitleFragment(firstLine) || 'Session';
|
|
4
49
|
}
|
|
5
50
|
export function stripAnsi(value) {
|
|
6
51
|
// Remove terminal escape sequences. Sidebar titles must be plain text.
|
|
@@ -40,20 +85,12 @@ export function canonicalizeTitleForCompare(value) {
|
|
|
40
85
|
*/
|
|
41
86
|
export function looksDecorated(title) {
|
|
42
87
|
const lines = stripAnsi(title).split(/\r?\n/);
|
|
43
|
-
if (lines.length < 2)
|
|
44
|
-
return
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
if (/^Cache\s+(Read|Write)\s+\S+/.test(line))
|
|
52
|
-
return true;
|
|
53
|
-
if (/^\$\S+\s+as API cost/.test(line))
|
|
54
|
-
return true;
|
|
55
|
-
if (/^(OpenAI|Copilot|Anthropic|RightCode|RC)\b/.test(line))
|
|
56
|
-
return true;
|
|
57
|
-
return false;
|
|
58
|
-
});
|
|
88
|
+
if (lines.length < 2) {
|
|
89
|
+
return Boolean(decoratedSingleLineBase(lines[0] || ''));
|
|
90
|
+
}
|
|
91
|
+
const detail = lines
|
|
92
|
+
.slice(1)
|
|
93
|
+
.map((line) => sanitizeTitleFragment(line).trim());
|
|
94
|
+
return (detail.some((line) => isStrongDecoratedDetail(line)) ||
|
|
95
|
+
detail.some((line) => isQuotaLikeProviderDetail(line)));
|
|
59
96
|
}
|
package/dist/title_apply.d.ts
CHANGED
|
@@ -10,9 +10,6 @@ export declare function createTitleApplicator(deps: {
|
|
|
10
10
|
markDirty: (dateKey: string | undefined) => void;
|
|
11
11
|
scheduleSave: () => void;
|
|
12
12
|
renderSidebarTitle: (baseTitle: string, usage: UsageSummary, quotas: QuotaSnapshot[], config: QuotaSidebarConfig) => string;
|
|
13
|
-
quotaRuntime: {
|
|
14
|
-
normalizeProviderID: (providerID: string) => string;
|
|
15
|
-
};
|
|
16
13
|
getQuotaSnapshots: (providerIDs: string[], options?: {
|
|
17
14
|
allowDefault?: boolean;
|
|
18
15
|
}) => Promise<QuotaSnapshot[]>;
|
package/dist/title_apply.js
CHANGED
|
@@ -56,7 +56,7 @@ export function createTitleApplicator(deps) {
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
const usage = await deps.summarizeSessionUsageForDisplay(sessionID, deps.config.sidebar.includeChildren);
|
|
59
|
-
const quotaProviders = Array.from(new Set(Object.keys(usage.providers)
|
|
59
|
+
const quotaProviders = Array.from(new Set(Object.keys(usage.providers)));
|
|
60
60
|
const quotas = deps.config.sidebar.showQuota && quotaProviders.length > 0
|
|
61
61
|
? await deps.getQuotaSnapshots(quotaProviders)
|
|
62
62
|
: [];
|
package/dist/types.d.ts
CHANGED
|
@@ -97,6 +97,12 @@ export type QuotaSidebarConfig = {
|
|
|
97
97
|
sidebar: {
|
|
98
98
|
enabled: boolean;
|
|
99
99
|
width: number;
|
|
100
|
+
/**
|
|
101
|
+
* When true, render multi-line decorated session titles.
|
|
102
|
+
* Enabled by default for clearer token/quota layout in sidebar.
|
|
103
|
+
* Set false to keep a compact single-line title.
|
|
104
|
+
*/
|
|
105
|
+
multilineTitle?: boolean;
|
|
100
106
|
showCost: boolean;
|
|
101
107
|
showQuota: boolean;
|
|
102
108
|
/** When true, wrap long quota lines and indent continuations. */
|
package/package.json
CHANGED