@leo000001/opencode-quota-sidebar 1.10.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 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
package/dist/index.js CHANGED
@@ -155,7 +155,6 @@ export async function QuotaSidebarPlugin(input) {
155
155
  markDirty,
156
156
  scheduleSave,
157
157
  renderSidebarTitle,
158
- quotaRuntime,
159
158
  getQuotaSnapshots,
160
159
  summarizeSessionUsageForDisplay,
161
160
  scheduleParentRefreshIfSafe,
@@ -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: ctx.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
- return firstLine.replace(/[\x00-\x1F\x7F-\x9F]/g, ' ').trimEnd() || 'Session';
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 false;
45
- const detail = lines.slice(1).map((line) => line.trim());
46
- return detail.some((line) => {
47
- if (!line)
48
- return false;
49
- if (/^Input\s+\S+\s+Output\s+\S+/.test(line))
50
- return true;
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
  }
@@ -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[]>;
@@ -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).map((id) => deps.quotaRuntime.normalizeProviderID(id))));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leo000001/opencode-quota-sidebar",
3
- "version": "1.10.0",
3
+ "version": "1.11.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",
@@ -2,8 +2,10 @@
2
2
  "sidebar": {
3
3
  "enabled": true,
4
4
  "width": 36,
5
+ "multilineTitle": true,
5
6
  "showCost": true,
6
7
  "showQuota": true,
8
+ "wrapQuotaLines": true,
7
9
  "includeChildren": true,
8
10
  "childrenMaxDepth": 6,
9
11
  "childrenMaxSessions": 128,