@leo000001/opencode-quota-sidebar 2.0.10 → 2.0.12

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
@@ -67,8 +67,9 @@ Want to add support for another provider (Google Antigravity, Zhipu AI, Firmware
67
67
  - quota lines: quota text like `OpenAI 5h 80% Rst 16:20`; short windows (`5h`, `1d`, `Daily`) show `HH:MM` on same-day resets and `MM-DD HH:MM` when crossing days, while longer windows continue to show `MM-DD`
68
68
  - RightCode daily quota shows `$remaining/$dailyTotal` without trailing percent, and shows balance on the next indented line when available
69
69
  - XYAI daily quota follows the same balance-style layout and prefers the real reset time (for example `XYAI Daily $70.2/$90 Rst 22:18`)
70
- - Desktop automatically switches to a compact single-line title. It keeps `Requests/Input/Output` plus recently used providers from the last `50` requests or last `60` minutes, and expands all windows/balance for those selected providers in short form such as `OAI 5h80 W70` or `RC D88.9/60 B260`
71
- - Web UI currently does not have reliable client detection, so it does not auto-switch to desktop compact mode. If you want GUI-friendly single-line titles there, set `sidebar.multilineTitle=false` manually or use `quota_summary` for details
70
+ - Desktop automatically switches to a compact single-line title. It keeps recently used providers from the last `50` requests or last `60` minutes ahead of `Requests/Input/Output`, and expands all windows/balance for those selected providers in short form such as `OAI 5h80 W70` or `RC D88.9/60 B260`
71
+ - TUI is always rendered as multiline; Desktop is always rendered as compact single-line. This behavior no longer depends on `sidebar.multilineTitle`
72
+ - Web UI currently cannot be reliably detected by the plugin, so it follows the non-desktop multiline path
72
73
  - Session-scoped usage/quota can include descendant subagent sessions (enabled by default via `sidebar.includeChildren=true`). Traversal is bounded by `childrenMaxDepth` (default 6), `childrenMaxSessions` (default 128), and `childrenConcurrency` (default 5); truncation is logged when `OPENCODE_QUOTA_DEBUG=1`. Day/week/month ranges never merge children — only session scope does.
73
74
  - Toast message can include four sections: `Token Usage`, `Cost as API` (per provider), `Provider Cache` (when provider-level cache coverage is available), and `Quota`
74
75
  - Expiry reminders are shown in a separate `Expiry Soon` toast section only for providers with real subscription expiry timestamps, and each session shows that auto-reminder at most once
@@ -249,7 +250,7 @@ Sidebar defaults:
249
250
 
250
251
  - `sidebar.enabled`: `true`
251
252
  - `sidebar.width`: `36` (clamped to `20`-`60`)
252
- - `sidebar.multilineTitle`: `true`
253
+ - `sidebar.multilineTitle`: `true` (legacy compatibility field; TUI/Desktop display mode is now chosen automatically)
253
254
  - `sidebar.showCost`: `true`
254
255
  - `sidebar.showQuota`: `true`
255
256
  - `sidebar.wrapQuotaLines`: `true`
@@ -323,8 +324,7 @@ Other defaults:
323
324
  - `sidebar.showCost` controls API-cost visibility in sidebar title, `quota_summary` markdown report, and toast message.
324
325
  - `quota_summary` follows the same reset compaction rules for short windows in its subscription section (`5h` / `1d` / `Daily` show time, long windows show date, RightCode `Exp` stays date-only).
325
326
  - `sidebar.width` is measured in terminal cells. CJK/emoji truncation is best-effort to avoid sidebar overflow.
326
- - `sidebar.multilineTitle` controls multi-line sidebar layout (default: `true`). Set `false` for compact single-line title.
327
- - Desktop ignores that visual preference and always uses the plugin's compact single-line title mode. TUI still follows `sidebar.multilineTitle`.
327
+ - `sidebar.multilineTitle` is kept for backward compatibility, but current rendering is fixed by client type: TUI uses multiline titles and Desktop uses compact single-line titles.
328
328
  - `sidebar.wrapQuotaLines` controls quota line wrapping and continuation indentation (default: `true`).
329
329
  - `sidebar.includeChildren` controls whether session-scoped usage/quota includes descendant subagent sessions (default: `true`).
330
330
  - `sidebar.childrenMaxDepth` limits how many levels of nested subagents are traversed (default: `6`, clamped 1–32).
@@ -367,9 +367,9 @@ Buzz Balance ¥10.17
367
367
 
368
368
  These examples show the quota block portion of the sidebar title.
369
369
 
370
- ### `sidebar.multilineTitle=true`
370
+ ### TUI layout
371
371
 
372
- This section describes the TUI layout. Desktop uses its own compact single-line format regardless of this setting.
372
+ This section describes the TUI layout. Desktop uses its own compact single-line format and Web UI currently follows the multiline path.
373
373
 
374
374
  0 providers (no quota data):
375
375
 
@@ -456,9 +456,9 @@ Copilot unavailable
456
456
  OpenAI Remaining ?
457
457
  ```
458
458
 
459
- ### `sidebar.multilineTitle=false`
459
+ ### Legacy compact single-line layout
460
460
 
461
- Quota is rendered inline as part of a single-line title:
461
+ For historical reference, the old non-desktop compact layout looked like this:
462
462
 
463
463
  ```text
464
464
  <base> | Req ... | Input ... | Output ... | OpenAI 5h 78%+ | Copilot Monthly 78% | ...
@@ -472,10 +472,10 @@ Mixed with Buzz balance:
472
472
 
473
473
  ### Desktop compact mode
474
474
 
475
- Desktop always uses a compact single-line title. Recently used providers are selected from the last `50` assistant requests or last `60` minutes, and each selected provider expands all of its windows and balances in shorthand:
475
+ Desktop always uses a compact single-line title. Recently used providers are selected from the last `50` assistant requests or last `60` minutes, and each selected provider expands all of its windows and balances in shorthand. To survive upstream Desktop truncation better, quota segments are emitted before usage stats:
476
476
 
477
477
  ```text
478
- <base> | R12 I18.9k O53 | OAI 5h80 W70 | Cop M78 | RC D88.9/60 B260 | Buzz B¥10.2
478
+ <base> | OAI 5h80 W70 | Cop M78 | RC D88.9/60 B260 | Buzz B¥10.2 | R12 I18.9k O53
479
479
  ```
480
480
 
481
481
  Shorthand rules:
@@ -486,6 +486,7 @@ Shorthand rules:
486
486
  - `W70` / `M78` / `D46` = weekly / monthly / daily window remaining percent
487
487
  - `D88.9/60` = daily remaining / daily total
488
488
  - `B260` / `B¥10.2` = balance
489
+ - Order is `base | quota... | usage`, while TUI keeps its multiline usage-first layout.
489
490
 
490
491
  `quota_summary` also supports an optional `includeChildren` flag (only effective for `period=session`) to override the config per call. For `day`/`week`/`month` periods, children are never merged — each session is counted independently.
491
492
 
package/dist/cost.js CHANGED
@@ -8,6 +8,38 @@ const MODEL_COST_RATE_ALIASES = {
8
8
  'kimi-for-coding:k2p5': ['moonshotai-cn:kimi-k2.5'],
9
9
  'kimi-for-coding:kimi-k2-thinking': ['moonshotai-cn:kimi-k2-thinking'],
10
10
  };
11
+ function anthropicModelAliases(modelID) {
12
+ const aliases = [];
13
+ const push = (value) => {
14
+ if (!value)
15
+ return;
16
+ if (!aliases.includes(value))
17
+ aliases.push(value);
18
+ };
19
+ push(modelID);
20
+ const stems = [modelID];
21
+ for (const stem of stems) {
22
+ const withoutVendorPrefix = stem.replace(/^anthropic[/.]/, '');
23
+ push(withoutVendorPrefix);
24
+ push(`anthropic/${withoutVendorPrefix}`);
25
+ const withoutThinkingSuffix = withoutVendorPrefix.replace(/-thinking$/, '');
26
+ push(withoutThinkingSuffix);
27
+ push(`anthropic/${withoutThinkingSuffix}`);
28
+ const withoutLatestSuffix = withoutThinkingSuffix.replace(/-latest$/, '');
29
+ push(withoutLatestSuffix);
30
+ push(`anthropic/${withoutLatestSuffix}`);
31
+ const withoutDateSuffix = withoutLatestSuffix.replace(/-\d{8}$/, '');
32
+ push(withoutDateSuffix);
33
+ push(`anthropic/${withoutDateSuffix}`);
34
+ const dotted = withoutDateSuffix.replace(/(\d)-(\d)(?=-|$)/g, '$1.$2');
35
+ push(dotted);
36
+ push(`anthropic/${dotted}`);
37
+ const hyphenated = withoutDateSuffix.replace(/(\d)\.(\d)(?=-|$)/g, '$1-$2');
38
+ push(hyphenated);
39
+ push(`anthropic/${hyphenated}`);
40
+ }
41
+ return aliases;
42
+ }
11
43
  function normalizeKnownProviderID(providerID) {
12
44
  if (providerID.startsWith('github-copilot'))
13
45
  return 'github-copilot';
@@ -37,9 +69,14 @@ export function modelCostLookupKeys(providerID, modelID) {
37
69
  if (!keys.includes(key))
38
70
  keys.push(key);
39
71
  };
40
- push(modelCostKey(providerID, modelID));
41
- if (canonicalProviderID !== providerID) {
42
- push(modelCostKey(canonicalProviderID, modelID));
72
+ const modelIDs = canonicalProviderID === 'anthropic'
73
+ ? anthropicModelAliases(modelID)
74
+ : [modelID];
75
+ for (const candidateModelID of modelIDs) {
76
+ push(modelCostKey(providerID, candidateModelID));
77
+ if (canonicalProviderID !== providerID) {
78
+ push(modelCostKey(canonicalProviderID, candidateModelID));
79
+ }
43
80
  }
44
81
  for (const key of [...keys]) {
45
82
  for (const alias of MODEL_COST_RATE_ALIASES[key] || []) {
package/dist/format.js CHANGED
@@ -276,7 +276,7 @@ function compactDesktopQuotaSegment(quota) {
276
276
  }
277
277
  return [label, ...parts].filter(Boolean).join(' ');
278
278
  }
279
- function renderDesktopCompactTitle(baseTitle, usage, quotas, config, width) {
279
+ function renderDesktopCompactTitle(baseTitle, usage, quotas, config, _width) {
280
280
  const visibleQuotas = collapseQuotaSnapshots(quotas).filter((q) => ['ok', 'error', 'unsupported', 'unavailable'].includes(q.status));
281
281
  const selectedProviderIDs = new Set(selectDesktopCompactProviderIDs(usage, config));
282
282
  const quotaSegments = visibleQuotas
@@ -284,14 +284,14 @@ function renderDesktopCompactTitle(baseTitle, usage, quotas, config, width) {
284
284
  .map(compactDesktopQuotaSegment)
285
285
  .filter(Boolean);
286
286
  const segments = [
287
- `R${shortNumber(usage.assistantMessages, 1)} I${sidebarNumber(usage.input)} O${sidebarNumber(usage.output)}`,
288
287
  ...quotaSegments,
288
+ `R${shortNumber(usage.assistantMessages, 1)} I${sidebarNumber(usage.input)} O${sidebarNumber(usage.output)}`,
289
289
  ];
290
290
  const detail = segments.join(' | ');
291
+ const safeBase = sanitizeLine(baseTitle) || 'Session';
291
292
  if (!detail)
292
- return fitLine(baseTitle, width);
293
- const safeBase = fitLine(baseTitle, Math.max(8, Math.floor(width * 0.35)));
294
- return fitLine(`${safeBase} | ${detail}`, width);
293
+ return safeBase;
294
+ return `${safeBase} | ${detail}`;
295
295
  }
296
296
  function formatPercent(value, decimals = 1) {
297
297
  const safe = Number.isFinite(value) && value >= 0 ? value : 0;
@@ -407,10 +407,6 @@ export function renderSidebarTitle(baseTitle, usage, quotas, config) {
407
407
  const singleLineBase = safeBaseTitle.split(/\r?\n/, 1)[0] || 'Session';
408
408
  return renderDesktopCompactTitle(singleLineBase, usage, quotas, config, width);
409
409
  }
410
- if (config.sidebar.multilineTitle !== true) {
411
- const singleLineBase = safeBaseTitle.split(/\r?\n/, 1)[0] || 'Session';
412
- return renderSingleLineTitle(singleLineBase, usage, quotas, config, width);
413
- }
414
410
  const cacheMetrics = getCacheCoverageMetrics(usage);
415
411
  const lines = [];
416
412
  for (const line of safeBaseTitle.split(/\r?\n/)) {
@@ -70,15 +70,16 @@ export function createUsageService(deps) {
70
70
  if (!rates)
71
71
  continue;
72
72
  const modelID = typeof modelValue.id === 'string' ? modelValue.id : modelKey;
73
- acc[modelCostKey(rawProviderID, modelID)] = rates;
74
- if (modelKey !== modelID) {
75
- acc[modelCostKey(rawProviderID, modelKey)] = rates;
76
- }
73
+ const lookupKeys = new Set([
74
+ ...modelCostLookupKeys(rawProviderID, modelID),
75
+ ...modelCostLookupKeys(rawProviderID, modelKey),
76
+ ]);
77
77
  if (canonicalProviderID !== rawProviderID) {
78
- acc[modelCostKey(canonicalProviderID, modelID)] = rates;
79
- if (modelKey !== modelID) {
80
- acc[modelCostKey(canonicalProviderID, modelKey)] = rates;
81
- }
78
+ lookupKeys.add(modelCostKey(canonicalProviderID, modelID));
79
+ lookupKeys.add(modelCostKey(canonicalProviderID, modelKey));
80
+ }
81
+ for (const key of lookupKeys) {
82
+ acc[key] = rates;
82
83
  }
83
84
  }
84
85
  return acc;
@@ -332,7 +333,8 @@ export function createUsageService(deps) {
332
333
  return { usage: empty, persist: false };
333
334
  }
334
335
  const modelCostMap = await getModelCostMap();
335
- const staleBillingCache = Boolean(sessionState?.usage) && !isUsageBillingCurrent(sessionState?.usage);
336
+ const staleBillingCache = Boolean(sessionState?.usage) &&
337
+ !isUsageBillingCurrent(sessionState?.usage);
336
338
  const forceRescan = forceRescanSessions.has(sessionID) || staleBillingCache;
337
339
  if (forceRescan)
338
340
  forceRescanSessions.delete(sessionID);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leo000001/opencode-quota-sidebar",
3
- "version": "2.0.10",
3
+ "version": "2.0.12",
4
4
  "description": "OpenCode plugin that shows quota and token usage in session titles",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",