@leo000001/opencode-quota-sidebar 2.0.11 → 2.0.14

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
@@ -55,28 +55,25 @@ Want to add support for another provider (Google Antigravity, Zhipu AI, Firmware
55
55
 
56
56
  ## Features
57
57
 
58
- - TUI session title becomes multiline in sidebar:
58
+ - TUI session title uses a compact multiline sidebar layout:
59
59
  - line 1: original session title
60
60
  - line 2: blank separator
61
- - line 3: Requests
62
- - line 4: Input/Output tokens
63
- - line 5: Cache Read tokens (only if non-zero)
64
- - line 6: Cache Write tokens (only if non-zero)
65
- - next lines: `Cache Coverage` (read/write cache models) and `Cache Read Coverage` (read-only cache models) when enough cache telemetry is available; mixed sessions can show both
66
- - next line: `$X.XX as API cost` (equivalent API billing for subscription-auth providers)
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
- - RightCode daily quota shows `$remaining/$dailyTotal` without trailing percent, and shows balance on the next indented line when available
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 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`
61
+ - line 3: compact usage tokens such as `R3 I16.3k O916`
62
+ - line 4+: compact cache tokens such as `CW300 CR31.4k Cd66%`
63
+ - optional cost line: `Est$0.12`
64
+ - quota lines also use compact tokens, for example `XYAI D$31.3/$90 R22:39` or `OAI 5h80 R22:18 W70 R04-03`
65
+ - short windows (`5h`, `1d`, `Daily`) still show same-day resets as `HH:MM` and cross-day resets as `MM-DD HH:MM`; longer windows continue to show `MM-DD`
66
+ - long quota content wraps across extra compact lines instead of dropping fields from the sidebar, and continuation lines align to the quota content column
67
+ - Desktop automatically switches to a compact monitoring-style single-line title. It keeps recently used providers from the last `50` requests or last `60` minutes, expands all windows/balance for those selected providers in short form such as `OAI 5h80 R16:20 W70 R04-03` or `RC D88.9/60 B260`, and keeps only summary usage signals such as `Cd66%` and `Est$0.12`
68
+ - TUI is always rendered as compact multiline; Desktop is always rendered as compact monitoring-style single-line. This behavior no longer depends on `sidebar.multilineTitle`
72
69
  - Web UI currently cannot be reliably detected by the plugin, so it follows the non-desktop multiline path
73
70
  - 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.
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`
71
+ - Toast message can include four sections: `Token Usage`, `Cost as API` (per provider), `Provider Cache` (when provider-level cached ratios are available), and `Quota`
75
72
  - 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
76
- - `quota_summary` markdown / toast also include `Cache Coverage` and `Cache Read Coverage` summary lines when available
73
+ - `quota_summary` markdown / toast also include `Cached` summary lines when cache activity is available
77
74
  - Quota snapshots are de-duplicated before rendering to avoid repeated provider lines
78
75
  - Custom tools:
79
- - `quota_summary` — generate usage report for session/day/week/month (markdown + toast)
76
+ - `quota_summary` — generate usage report for session/day/week/month (full markdown report + toast). The markdown report and toast keep the full human-readable wording; they do not switch to compact sidebar tokens.
80
77
  - `quota_show` — toggle sidebar title display on/off (state persists across sessions)
81
78
  - After startup, titles are restored immediately when persisted display mode is OFF; when persisted display mode is ON, touched titles refresh on startup and the rest update on the next relevant session/message event or when `quota_show` is toggled
82
79
  - Quota connectors:
@@ -121,10 +118,10 @@ The plugin stores lightweight global state and date-partitioned session chunks.
121
118
  - `createdAt`
122
119
  - `parentID` (when the session is a subagent child session)
123
120
  - `expiryToastShown` (session-level dedupe for automatic expiry reminders)
124
- - cached usage summary used by `quota_summary`, including session-level and provider-level `cacheBuckets` for cache coverage reporting
121
+ - cached usage summary used by `quota_summary`, including session-level and provider-level `cacheBuckets` for cached-ratio reporting and legacy cache classification
125
122
  - incremental aggregation cursor
126
123
 
127
- Notes on cache coverage persistence:
124
+ Notes on cache bucket persistence:
128
125
 
129
126
  - Older cached usage written before `cacheBuckets` existed can only be approximated from top-level `cache_read` / `cache_write` totals.
130
127
  - In those legacy cases, mixed read-only + read-write cache traffic may be attributed to a single fallback bucket until the session is recomputed from messages.
@@ -208,6 +205,8 @@ You can add these command templates in `opencode.json` so you can run `/qday`, `
208
205
  }
209
206
  ```
210
207
 
208
+ When calling `quota_summary`, make sure the client shows the returned markdown report directly to the user. The tool already returns the full report body; do not replace it with a compact summary.
209
+
211
210
  ## Configuration files
212
211
 
213
212
  Recommended global config:
@@ -324,7 +323,7 @@ Other defaults:
324
323
  - `sidebar.showCost` controls API-cost visibility in sidebar title, `quota_summary` markdown report, and toast message.
325
324
  - `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).
326
325
  - `sidebar.width` is measured in terminal cells. CJK/emoji truncation is best-effort to avoid sidebar overflow.
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.
326
+ - `sidebar.multilineTitle` is kept for backward compatibility, but current rendering is fixed by client type: TUI uses multiline titles and Desktop uses compact monitoring-style single-line titles.
328
327
  - `sidebar.wrapQuotaLines` controls quota line wrapping and continuation indentation (default: `true`).
329
328
  - `sidebar.includeChildren` controls whether session-scoped usage/quota includes descendant subagent sessions (default: `true`).
330
329
  - `sidebar.childrenMaxDepth` limits how many levels of nested subagents are traversed (default: `6`, clamped 1–32).
@@ -360,7 +359,7 @@ The adapter also tolerates `https://buzzai.cc/v1`, but `https://buzzai.cc` is th
360
359
  With that setup, the sidebar/toast quota line will look like:
361
360
 
362
361
  ```text
363
- Buzz Balance ¥10.17
362
+ Buzz B¥10.17
364
363
  ```
365
364
 
366
365
  ## Rendering examples
@@ -369,7 +368,7 @@ These examples show the quota block portion of the sidebar title.
369
368
 
370
369
  ### TUI layout
371
370
 
372
- This section describes the TUI layout. Desktop uses its own compact single-line format and Web UI currently follows the multiline path.
371
+ This section describes the TUI layout. Desktop uses its own compact monitoring-style single-line format and Web UI currently follows the multiline path.
373
372
 
374
373
  0 providers (no quota data):
375
374
 
@@ -380,113 +379,95 @@ This section describes the TUI layout. Desktop uses its own compact single-line
380
379
  1 provider, 1 window (fits):
381
380
 
382
381
  ```text
383
- Copilot Monthly 78% Rst 04-01
382
+ Cop M78 R04-01
384
383
  ```
385
384
 
386
385
  1 provider, multi-window (for example OpenAI 5h + Weekly):
387
386
 
388
387
  ```text
389
- OpenAI
390
- 5h 78% Rst 05:05
391
- Weekly 73% Rst 03-12
388
+ OAI 5h78 R05:05 W73 R03-12
389
+ ```
390
+
391
+ 1 provider, multi-window on narrow width:
392
+
393
+ ```text
394
+ OAI 5h78 R05:05
395
+ W73 R03-12
392
396
  ```
393
397
 
394
398
  1 provider, short window crossing into the next day:
395
399
 
396
400
  ```text
397
- Anthropic
398
- 5h 0% Rst 03-10 01:00
399
- Weekly 46% Rst 03-15
401
+ Ant 5h0 R03-10 01:00 W46 R03-15
400
402
  ```
401
403
 
402
404
  2+ providers (even if each provider is single-window):
403
405
 
404
406
  ```text
405
- OpenAI
406
- 5h 78% Rst 05:05
407
- Copilot
408
- Monthly 78% Rst 04-01
407
+ OAI 5h78 R05:05
408
+ Cop M78 R04-01
409
409
  ```
410
410
 
411
411
  2+ providers mixed (multi-window + single-window):
412
412
 
413
413
  ```text
414
- OpenAI
415
- 5h 78% Rst 05:05
416
- Weekly 73% Rst 03-12
417
- Copilot
418
- Monthly 78% Rst 04-01
414
+ OAI 5h78 R05:05 W73 R03-12
415
+ Cop M78 R04-01
419
416
  ```
420
417
 
421
418
  2+ providers mixed (window providers + Buzz balance):
422
419
 
423
420
  ```text
424
- OpenAI
425
- 5h 78% Rst 05:05
426
- Copilot
427
- Monthly 78% Rst 04-01
428
- Buzz Balance ¥10.2
421
+ OAI 5h78 R05:05
422
+ Cop M78 R04-01
423
+ Buzz B¥10.2
429
424
  ```
430
425
 
431
426
  Balance-style quota:
432
427
 
433
428
  ```text
434
- RC Balance $260
429
+ RC B260
435
430
  ```
436
431
 
437
432
  Buzz balance quota:
438
433
 
439
434
  ```text
440
- Buzz Balance ¥10.17
435
+ Buzz B¥10.17
441
436
  ```
442
437
 
443
438
  Multi-detail quota (window + balance):
444
439
 
445
440
  ```text
446
- RC
447
- Daily $88.9/$60 Exp 02-27
448
- Balance $260
441
+ RC D$88.9/$60 E02-27 B260
449
442
  ```
450
443
 
451
444
  Provider status / quota (examples):
452
445
 
453
446
  ```text
454
- Anthropic 5h 80%+
455
- Copilot unavailable
456
- OpenAI Remaining ?
457
- ```
458
-
459
- ### Legacy compact single-line layout
460
-
461
- For historical reference, the old non-desktop compact layout looked like this:
462
-
463
- ```text
464
- <base> | Req ... | Input ... | Output ... | OpenAI 5h 78%+ | Copilot Monthly 78% | ...
465
- ```
466
-
467
- Mixed with Buzz balance:
468
-
469
- ```text
470
- <base> | Req ... | Input ... | Output ... | OpenAI 5h 78%+ | Copilot Monthly 78% | Buzz Balance ¥10.2
447
+ Ant 5h80
448
+ Cop unavailable
449
+ OAI ?
471
450
  ```
472
451
 
473
452
  ### Desktop compact mode
474
453
 
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:
454
+ Desktop always uses a compact monitoring-style 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 summary signals:
476
455
 
477
456
  ```text
478
- <base> | OAI 5h80 W70 | Cop M78 | RC D88.9/60 B260 | Buzz B¥10.2 | R12 I18.9k O53
457
+ <base> | OAI 5h80 R16:20 W70 R04-03 | Cop M78 R04-01 | RC D88.9/60 B260 | Buzz B¥10.2 | Cd66% | Est$0.12
479
458
  ```
480
459
 
481
460
  Shorthand rules:
482
461
 
483
- - `R12` = 12 requests
484
- - `I18.9k` / `O53` = input / output tokens
485
462
  - `5h80` = `5h 80%`
486
463
  - `W70` / `M78` / `D46` = weekly / monthly / daily window remaining percent
464
+ - `R16:20` / `R04-03` = reset time/date for that quota window
487
465
  - `D88.9/60` = daily remaining / daily total
488
466
  - `B260` / `B¥10.2` = balance
489
- - Order is `base | quota... | usage`, while TUI keeps its multiline usage-first layout.
467
+ - `Cd66%` = cached ratio (`cache.read / (input + cache.read)`)
468
+ - `Est$0.12` = equivalent API cost estimate
469
+ - Desktop omits `R/I/O/CR/CW`; TUI keeps the full compact multiline breakdown.
470
+ - Order is `base | quota... | usage-summary`, while TUI keeps its multiline usage-first layout.
490
471
 
491
472
  `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.
492
473
 
package/dist/cost.js CHANGED
@@ -8,6 +8,54 @@ 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 queue = [];
14
+ const push = (value) => {
15
+ if (!value)
16
+ return;
17
+ if (!aliases.includes(value)) {
18
+ aliases.push(value);
19
+ queue.push(value);
20
+ }
21
+ };
22
+ push(modelID);
23
+ for (let index = 0; index < queue.length; index++) {
24
+ const stem = queue[index];
25
+ const withoutProviderPrefix = stem
26
+ .replace(/^(?:[a-z]+\.)*anthropic\./, '')
27
+ .replace(/^anthropic[/.]/, '');
28
+ push(withoutProviderPrefix);
29
+ push(`anthropic/${withoutProviderPrefix}`);
30
+ const withoutVersionSuffix = withoutProviderPrefix.replace(/-v\d+(?::\d+)?$/, '');
31
+ push(withoutVersionSuffix);
32
+ push(`anthropic/${withoutVersionSuffix}`);
33
+ const atDate = withoutVersionSuffix.replace(/@(\d{8})$/, '-$1');
34
+ push(atDate);
35
+ push(`anthropic/${atDate}`);
36
+ const withAtDate = withoutVersionSuffix.replace(/-(\d{8})$/, '@$1');
37
+ push(withAtDate);
38
+ push(`anthropic/${withAtDate}`);
39
+ const withoutThinkingSuffix = withoutVersionSuffix.replace(/-thinking$/, '');
40
+ push(withoutThinkingSuffix);
41
+ push(`anthropic/${withoutThinkingSuffix}`);
42
+ const withoutLatestSuffix = withoutThinkingSuffix.replace(/-latest$/, '');
43
+ push(withoutLatestSuffix);
44
+ push(`anthropic/${withoutLatestSuffix}`);
45
+ const withoutDateSuffix = withoutLatestSuffix
46
+ .replace(/-\d{8}$/, '')
47
+ .replace(/@\d{8}$/, '');
48
+ push(withoutDateSuffix);
49
+ push(`anthropic/${withoutDateSuffix}`);
50
+ const dotted = withoutDateSuffix.replace(/(\d)-(\d)(?=-|$)/g, '$1.$2');
51
+ push(dotted);
52
+ push(`anthropic/${dotted}`);
53
+ const hyphenated = withoutDateSuffix.replace(/(\d)\.(\d)(?=-|$)/g, '$1-$2');
54
+ push(hyphenated);
55
+ push(`anthropic/${hyphenated}`);
56
+ }
57
+ return aliases;
58
+ }
11
59
  function normalizeKnownProviderID(providerID) {
12
60
  if (providerID.startsWith('github-copilot'))
13
61
  return 'github-copilot';
@@ -37,9 +85,14 @@ export function modelCostLookupKeys(providerID, modelID) {
37
85
  if (!keys.includes(key))
38
86
  keys.push(key);
39
87
  };
40
- push(modelCostKey(providerID, modelID));
41
- if (canonicalProviderID !== providerID) {
42
- push(modelCostKey(canonicalProviderID, modelID));
88
+ const modelIDs = canonicalProviderID === 'anthropic'
89
+ ? anthropicModelAliases(modelID)
90
+ : [modelID];
91
+ for (const candidateModelID of modelIDs) {
92
+ push(modelCostKey(providerID, candidateModelID));
93
+ if (canonicalProviderID !== providerID) {
94
+ push(modelCostKey(canonicalProviderID, candidateModelID));
95
+ }
43
96
  }
44
97
  for (const key of [...keys]) {
45
98
  for (const alias of MODEL_COST_RATE_ALIASES[key] || []) {
package/dist/format.js CHANGED
@@ -207,7 +207,7 @@ function compactProviderLabel(quota) {
207
207
  if (canonical === 'rightcode')
208
208
  return 'RC';
209
209
  if (canonical === 'xyai-vibe')
210
- return 'XY';
210
+ return 'XYAI';
211
211
  if (canonical === 'buzz')
212
212
  return 'Buzz';
213
213
  return sanitizeLine(quotaDisplayLabel(quota));
@@ -226,6 +226,78 @@ function compactWindowToken(label) {
226
226
  return 'D';
227
227
  return safe;
228
228
  }
229
+ function compactQuotaResetToken(resetLabel) {
230
+ const safe = sanitizeLine(resetLabel || '');
231
+ if (!safe || /^rst$/i.test(safe))
232
+ return 'R';
233
+ if (/^exp\+$/i.test(safe))
234
+ return 'E+';
235
+ if (/^exp$/i.test(safe))
236
+ return 'E';
237
+ return safe;
238
+ }
239
+ function compactQuotaPercentToken(label, percent) {
240
+ const rounded = percent !== undefined && Number.isFinite(percent)
241
+ ? `${Math.round(percent)}`
242
+ : '';
243
+ const safe = sanitizeLine(label || '');
244
+ if (!safe)
245
+ return rounded ? `R${rounded}` : '';
246
+ if (/^sonnet\s+7d$/i.test(safe))
247
+ return rounded ? `S7d${rounded}` : 'S7d';
248
+ const token = compactWindowToken(safe).replace(/\s+/g, '');
249
+ if (!rounded)
250
+ return token;
251
+ if (/^(?:D|W|M|\d+[hdw])$/i.test(token))
252
+ return `${token}${rounded}`;
253
+ return `${token} ${rounded}%`;
254
+ }
255
+ function compactQuotaWindowText(win) {
256
+ const reset = compactReset(win.resetAt, win.resetLabel, win.label);
257
+ const resetToken = reset
258
+ ? `${compactQuotaResetToken(win.resetLabel)}${reset}`
259
+ : undefined;
260
+ if (win.showPercent === false) {
261
+ const safe = sanitizeLine(win.label || '');
262
+ const daily = safe ? safe.replace(/^Daily\s+/i, 'D') : '';
263
+ return [daily, resetToken].filter(Boolean).join(' ');
264
+ }
265
+ const percentToken = compactQuotaPercentToken(win.label, win.remainingPercent);
266
+ return [percentToken, resetToken].filter(Boolean).join(' ');
267
+ }
268
+ function compactQuotaWindowTokens(win) {
269
+ const reset = compactReset(win.resetAt, win.resetLabel, win.label);
270
+ const resetToken = reset
271
+ ? `${compactQuotaResetToken(win.resetLabel)}${reset}`
272
+ : undefined;
273
+ if (win.showPercent === false) {
274
+ const safe = sanitizeLine(win.label || '');
275
+ const daily = safe ? safe.replace(/^Daily\s+/i, 'D') : '';
276
+ return [daily, resetToken].filter((value) => Boolean(value));
277
+ }
278
+ const percentToken = compactQuotaPercentToken(win.label, win.remainingPercent);
279
+ return [percentToken, resetToken].filter((value) => Boolean(value));
280
+ }
281
+ function compactQuotaBalanceText(balance) {
282
+ return `B${compactDesktopCurrencyValue(balance.amount, balance.currency)}`;
283
+ }
284
+ function packInlineTokens(label, tokens, width, indent = ' ') {
285
+ if (tokens.length === 0)
286
+ return [label];
287
+ const lines = [];
288
+ let current = label;
289
+ for (const token of tokens) {
290
+ const candidate = `${current} ${token}`;
291
+ if (stringCellWidth(candidate) <= width || current === label) {
292
+ current = candidate;
293
+ continue;
294
+ }
295
+ lines.push(current);
296
+ current = `${indent}${token}`;
297
+ }
298
+ lines.push(current);
299
+ return lines;
300
+ }
229
301
  function compactDesktopCurrencyValue(value, currency) {
230
302
  const rendered = formatCurrency(value, currency);
231
303
  if (currency === '$')
@@ -243,23 +315,7 @@ function compactDesktopQuotaSegment(quota) {
243
315
  let hasBalanceToken = false;
244
316
  if (quota.windows && quota.windows.length > 0) {
245
317
  for (const win of quota.windows) {
246
- const winLabel = sanitizeLine(win.label || '');
247
- if (win.showPercent === false) {
248
- const daily = winLabel.match(/^Daily\s+\$?([\d.,]+)\/\$?([\d.,]+)/i);
249
- if (daily) {
250
- parts.push(`D${daily[1]}/${daily[2]}`);
251
- continue;
252
- }
253
- if (winLabel)
254
- parts.push(winLabel.replace(/^Daily\s+/i, 'D'));
255
- continue;
256
- }
257
- const percent = win.remainingPercent !== undefined &&
258
- Number.isFinite(win.remainingPercent)
259
- ? `${compactWindowToken(winLabel)}${Math.round(win.remainingPercent)}`
260
- : compactWindowToken(winLabel);
261
- if (percent)
262
- parts.push(percent);
318
+ parts.push(...compactQuotaWindowTokens(win));
263
319
  }
264
320
  }
265
321
  else if (quota.balance) {
@@ -283,10 +339,15 @@ function renderDesktopCompactTitle(baseTitle, usage, quotas, config, _width) {
283
339
  .filter((quota) => selectedProviderIDs.has(quota.providerID))
284
340
  .map(compactDesktopQuotaSegment)
285
341
  .filter(Boolean);
286
- const segments = [
287
- ...quotaSegments,
288
- `R${shortNumber(usage.assistantMessages, 1)} I${sidebarNumber(usage.input)} O${sidebarNumber(usage.output)}`,
289
- ];
342
+ const cacheMetrics = getCacheCoverageMetrics(usage);
343
+ const usageSegments = [];
344
+ if (cacheMetrics.cachedRatio !== undefined) {
345
+ usageSegments.push(`Cd${formatPercent(cacheMetrics.cachedRatio, 0)}`);
346
+ }
347
+ if (config.sidebar.showCost && usage.apiCost > 0) {
348
+ usageSegments.push(`Est${formatApiCostValue(usage.apiCost)}`);
349
+ }
350
+ const segments = [...quotaSegments, ...usageSegments];
290
351
  const detail = segments.join(' | ');
291
352
  const safeBase = sanitizeLine(baseTitle) || 'Session';
292
353
  if (!detail)
@@ -365,17 +426,14 @@ function renderSingleLineTitle(baseTitle, usage, quotas, config, width) {
365
426
  formatRequestsLabel(usage.assistantMessages, true),
366
427
  `Input ${sidebarNumber(usage.input)} Output ${sidebarNumber(usage.output)}`,
367
428
  ];
368
- if (usage.cacheRead > 0) {
369
- segments.push(`Cache Read ${sidebarNumber(usage.cacheRead)}`);
370
- }
371
429
  if (usage.cacheWrite > 0) {
372
430
  segments.push(`Cache Write ${sidebarNumber(usage.cacheWrite)}`);
373
431
  }
374
- if (cacheMetrics.cacheCoverage !== undefined) {
375
- segments.push(`Cache Coverage ${formatPercent(cacheMetrics.cacheCoverage, 0)}`);
432
+ if (usage.cacheRead > 0) {
433
+ segments.push(`Cache Read ${sidebarNumber(usage.cacheRead)}`);
376
434
  }
377
- if (cacheMetrics.cacheReadCoverage !== undefined) {
378
- segments.push(`Cache Read Coverage ${formatPercent(cacheMetrics.cacheReadCoverage, 0)}`);
435
+ if (cacheMetrics.cachedRatio !== undefined) {
436
+ segments.push(`Cached ${formatPercent(cacheMetrics.cachedRatio, 0)}`);
379
437
  }
380
438
  if (config.sidebar.showCost && usage.apiCost > 0) {
381
439
  segments.push(formatApiCostLine(usage.apiCost));
@@ -413,34 +471,21 @@ export function renderSidebarTitle(baseTitle, usage, quotas, config) {
413
471
  lines.push(fitLine(line || 'Session', width));
414
472
  }
415
473
  lines.push('');
416
- // Input / Output line
417
- lines.push(fitLine(formatRequestsLabel(usage.assistantMessages), width));
418
- const io = `Input ${sidebarNumber(usage.input)} Output ${sidebarNumber(usage.output)}`;
419
- lines.push(fitLine(io, width));
420
- // Cache lines (provider-compatible across OpenAI/Anthropic/Gemini/Copilot)
421
- if (usage.cacheRead > 0) {
422
- lines.push(fitLine(`Cache Read ${sidebarNumber(usage.cacheRead)}`, width));
423
- }
424
- if (usage.cacheWrite > 0) {
425
- lines.push(fitLine(`Cache Write ${sidebarNumber(usage.cacheWrite)}`, width));
426
- }
427
- if (cacheMetrics.cacheCoverage !== undefined) {
428
- lines.push(fitLine(`Cache Coverage ${formatPercent(cacheMetrics.cacheCoverage, 0)}`, width));
429
- }
430
- if (cacheMetrics.cacheReadCoverage !== undefined) {
431
- lines.push(fitLine(`Cache Read Coverage ${formatPercent(cacheMetrics.cacheReadCoverage, 0)}`, width));
432
- }
433
- if (config.sidebar.showCost && usage.apiCost > 0) {
434
- lines.push(fitLine(formatApiCostLine(usage.apiCost), width));
474
+ for (const detailLine of usageDetailLines(usage, cacheMetrics, {
475
+ width,
476
+ showCost: config.sidebar.showCost,
477
+ })) {
478
+ lines.push(fitLine(detailLine, width));
435
479
  }
436
480
  // Quota lines (one provider per line for stable wrapping)
437
481
  if (config.sidebar.showQuota) {
438
482
  const visibleQuotas = collapseQuotaSnapshots(quotas).filter((q) => ['ok', 'error', 'unsupported', 'unavailable'].includes(q.status));
439
- // When multiple providers are visible, keep a consistent visual rhythm by
440
- // always rendering each provider as a header line + indented detail line(s).
441
- const forceWrappedProviders = visibleQuotas.length > 1;
483
+ const compactQuotaDetails = true;
484
+ const forceWrappedProviders = false;
442
485
  const labelWidth = visibleQuotas.reduce((max, item) => {
443
- const label = sanitizeLine(quotaDisplayLabel(item));
486
+ const label = compactQuotaDetails
487
+ ? compactProviderLabel(item)
488
+ : sanitizeLine(quotaDisplayLabel(item));
444
489
  return Math.max(max, stringCellWidth(label));
445
490
  }, 0);
446
491
  const quotaItems = visibleQuotas
@@ -448,6 +493,7 @@ export function renderSidebarTitle(baseTitle, usage, quotas, config) {
448
493
  width,
449
494
  wrapLines: config.sidebar.wrapQuotaLines,
450
495
  forceWrapped: forceWrappedProviders,
496
+ compactDetails: compactQuotaDetails,
451
497
  }))
452
498
  .filter((s) => Boolean(s));
453
499
  if (quotaItems.length > 0) {
@@ -457,6 +503,49 @@ export function renderSidebarTitle(baseTitle, usage, quotas, config) {
457
503
  lines.push(fitLine(line, width));
458
504
  }
459
505
  }
506
+ function fitsLine(value, width) {
507
+ return stringCellWidth(sanitizeLine(value)) <= width;
508
+ }
509
+ function usageDetailLines(usage, cacheMetrics, options) {
510
+ const width = options.width;
511
+ const groups = [];
512
+ groups.push([
513
+ `R${shortNumber(usage.assistantMessages, 1)}`,
514
+ `I${sidebarNumber(usage.input)}`,
515
+ `O${sidebarNumber(usage.output)}`,
516
+ ]);
517
+ const secondary = [];
518
+ if (usage.cacheWrite > 0) {
519
+ secondary.push(`CW${sidebarNumber(usage.cacheWrite)}`);
520
+ }
521
+ if (usage.cacheRead > 0) {
522
+ secondary.push(`CR${sidebarNumber(usage.cacheRead)}`);
523
+ }
524
+ if (cacheMetrics.cachedRatio !== undefined) {
525
+ secondary.push(`Cd${formatPercent(cacheMetrics.cachedRatio, 0)}`);
526
+ }
527
+ if (secondary.length > 0)
528
+ groups.push(secondary);
529
+ if (options.showCost && usage.apiCost > 0) {
530
+ groups.push([`Est${formatApiCostValue(usage.apiCost)}`]);
531
+ }
532
+ const packed = [];
533
+ for (const group of groups) {
534
+ let current = '';
535
+ for (const token of group) {
536
+ const candidate = current ? `${current} ${token}` : token;
537
+ if (!current || fitsLine(candidate, width)) {
538
+ current = candidate;
539
+ continue;
540
+ }
541
+ packed.push(current);
542
+ current = token;
543
+ }
544
+ if (current)
545
+ packed.push(current);
546
+ }
547
+ return packed;
548
+ }
460
549
  return lines.join('\n');
461
550
  }
462
551
  /**
@@ -476,7 +565,10 @@ export function renderSidebarTitle(baseTitle, usage, quotas, config) {
476
565
  * " Balance $108.88"
477
566
  */
478
567
  function compactQuotaWide(quota, labelWidth = 0, options) {
479
- const label = sanitizeLine(quotaDisplayLabel(quota));
568
+ const compactDetails = options?.compactDetails === true;
569
+ const label = compactDetails
570
+ ? compactProviderLabel(quota)
571
+ : sanitizeLine(quotaDisplayLabel(quota));
480
572
  const labelPadded = padEndCells(label, labelWidth);
481
573
  const detailIndent = ' ';
482
574
  const withLabel = (content) => `${labelPadded} ${content}`;
@@ -501,9 +593,13 @@ function compactQuotaWide(quota, labelWidth = 0, options) {
501
593
  if (quota.status !== 'ok')
502
594
  return [];
503
595
  const balanceText = quota.balance
504
- ? `Balance ${formatCurrency(quota.balance.amount, quota.balance.currency)}`
596
+ ? compactDetails
597
+ ? compactQuotaBalanceText(quota.balance)
598
+ : `Balance ${formatCurrency(quota.balance.amount, quota.balance.currency)}`
505
599
  : undefined;
506
600
  const renderWindow = (win) => {
601
+ if (compactDetails)
602
+ return compactQuotaWindowText(win);
507
603
  const showPercent = win.showPercent !== false;
508
604
  const pct = formatQuotaPercent(win.remainingPercent, { rounded: true });
509
605
  const parts = win.label
@@ -520,11 +616,20 @@ function compactQuotaWide(quota, labelWidth = 0, options) {
520
616
  // Multi-window rendering
521
617
  if (quota.windows && quota.windows.length > 0) {
522
618
  const parts = quota.windows.map(renderWindow);
619
+ const compactTokens = compactDetails
620
+ ? quota.windows.flatMap((win) => compactQuotaWindowTokens(win))
621
+ : [];
523
622
  // Build the detail lines (window texts + optional balance)
524
623
  const details = [...parts];
525
624
  if (balanceText && !parts.some((p) => p.includes('Balance '))) {
526
625
  details.push(balanceText);
527
626
  }
627
+ if (compactDetails) {
628
+ const tokens = [...compactTokens];
629
+ if (balanceText)
630
+ tokens.push(balanceText);
631
+ return packInlineTokens(label, tokens, width, ' '.repeat(stringCellWidth(label) + 1));
632
+ }
528
633
  // Keep a unified wrapped layout for providers that have multiple detail
529
634
  // lines so OpenAI/Copilot/others match the RightCode multi-line style,
530
635
  // regardless of wrapLines.
@@ -541,7 +646,11 @@ function compactQuotaWide(quota, labelWidth = 0, options) {
541
646
  // Fallback: single value from top-level remainingPercent
542
647
  const percent = formatQuotaPercent(quota.remainingPercent, { rounded: true });
543
648
  const reset = compactReset(quota.resetAt, 'Rst');
544
- const fallbackText = `Remaining ${percent}${reset ? ` Rst ${reset}` : ''}`;
649
+ const fallbackText = compactDetails
650
+ ? [`R${percent.replace(/%$/, '')}`, reset ? `R${reset}` : undefined]
651
+ .filter(Boolean)
652
+ .join(' ')
653
+ : `Remaining ${percent}${reset ? ` Rst ${reset}` : ''}`;
545
654
  return maybeBreak(fallbackText, [fallbackText]);
546
655
  }
547
656
  function isShortResetWindow(label) {
@@ -679,16 +788,10 @@ export function renderMarkdownReport(period, usage, quotas, options) {
679
788
  return '-';
680
789
  return formatApiCostValue(usage.apiCost);
681
790
  };
682
- const cacheCoverageCell = (provider) => {
683
- const metrics = getProviderCacheCoverageMetrics(provider);
684
- return metrics.cacheCoverage !== undefined
685
- ? formatPercent(metrics.cacheCoverage, 1)
686
- : '-';
687
- };
688
- const cacheReadCoverageCell = (provider) => {
791
+ const cachedCell = (provider) => {
689
792
  const metrics = getProviderCacheCoverageMetrics(provider);
690
- return metrics.cacheReadCoverage !== undefined
691
- ? formatPercent(metrics.cacheReadCoverage, 1)
793
+ return metrics.cachedRatio !== undefined
794
+ ? formatPercent(metrics.cachedRatio, 1)
692
795
  : '-';
693
796
  };
694
797
  const providerEntries = Object.values(usage.providers).sort((a, b) => b.total - a.total);
@@ -711,25 +814,15 @@ export function renderMarkdownReport(period, usage, quotas, options) {
711
814
  checkedAt: 0,
712
815
  })} (${formatUsd(topApiCost.apiCost)})`);
713
816
  }
714
- const bestCacheCoverage = providerEntries
817
+ const bestCachedRatio = providerEntries
715
818
  .map((provider) => ({
716
819
  provider,
717
- value: getProviderCacheCoverageMetrics(provider).cacheCoverage,
820
+ value: getProviderCacheCoverageMetrics(provider).cachedRatio,
718
821
  }))
719
822
  .filter((entry) => entry.value !== undefined)
720
823
  .sort((a, b) => b.value - a.value)[0];
721
- if (bestCacheCoverage) {
722
- lines.push(`- Best Cache Coverage: ${providerLabel(bestCacheCoverage.provider.providerID)} (${formatPercent(bestCacheCoverage.value, 1)})`);
723
- }
724
- const bestCacheReadCoverage = providerEntries
725
- .map((provider) => ({
726
- provider,
727
- value: getProviderCacheCoverageMetrics(provider).cacheReadCoverage,
728
- }))
729
- .filter((entry) => entry.value !== undefined)
730
- .sort((a, b) => b.value - a.value)[0];
731
- if (bestCacheReadCoverage) {
732
- lines.push(`- Best Cache Read Coverage: ${providerLabel(bestCacheReadCoverage.provider.providerID)} (${formatPercent(bestCacheReadCoverage.value, 1)})`);
824
+ if (bestCachedRatio) {
825
+ lines.push(`- Best Cached Ratio: ${providerLabel(bestCachedRatio.provider.providerID)} (${formatPercent(bestCachedRatio.value, 1)})`);
733
826
  }
734
827
  const highestMeasured = providerEntries
735
828
  .filter((provider) => measuredCostCell(provider.providerID, provider.cost) !== '-')
@@ -742,9 +835,15 @@ export function renderMarkdownReport(period, usage, quotas, options) {
742
835
  const providerRows = providerEntries.map((provider) => {
743
836
  const providerID = mdCell(provider.providerID);
744
837
  return showCost
745
- ? `| ${providerID} | ${shortNumber(provider.assistantMessages)} | ${shortNumber(provider.input)} | ${shortNumber(provider.output)} | ${shortNumber(provider.cacheRead + provider.cacheWrite)} | ${shortNumber(provider.total)} | ${cacheCoverageCell(provider)} | ${cacheReadCoverageCell(provider)} | ${measuredCostCell(provider.providerID, provider.cost)} | ${apiCostCell(provider.providerID, provider.apiCost)} |`
838
+ ? `| ${providerID} | ${shortNumber(provider.assistantMessages)} | ${shortNumber(provider.input)} | ${shortNumber(provider.output)} | ${shortNumber(provider.cacheRead + provider.cacheWrite)} | ${shortNumber(provider.total)} | ${cachedCell(provider)} | ${measuredCostCell(provider.providerID, provider.cost)} | ${apiCostCell(provider.providerID, provider.apiCost)} |`
746
839
  : `| ${providerID} | ${shortNumber(provider.assistantMessages)} | ${shortNumber(provider.input)} | ${shortNumber(provider.output)} | ${shortNumber(provider.cacheRead + provider.cacheWrite)} | ${shortNumber(provider.total)} |`;
747
840
  });
841
+ const providerHeader = showCost
842
+ ? '| Provider | Requests | Input | Output | Cache | Total | Cached | Measured Cost | API Cost |'
843
+ : '| Provider | Requests | Input | Output | Cache | Total |';
844
+ const providerDivider = showCost
845
+ ? '| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |'
846
+ : '| --- | ---: | ---: | ---: | ---: | ---: |';
748
847
  const quotaLines = collapseQuotaSnapshots(quotas).flatMap((quota) => {
749
848
  const displayLabel = quotaDisplayLabel(quota);
750
849
  // Multi-window detail
@@ -785,13 +884,8 @@ export function renderMarkdownReport(period, usage, quotas, options) {
785
884
  `- Sessions: ${usage.sessionCount}`,
786
885
  `- Requests: ${usage.assistantMessages}`,
787
886
  `- Tokens: input ${usage.input}, output ${usage.output}, cache_read ${usage.cacheRead}, cache_write ${usage.cacheWrite}, total ${usage.total}`,
788
- ...(cacheMetrics.cacheCoverage !== undefined
789
- ? [`- Cache Coverage: ${formatPercent(cacheMetrics.cacheCoverage, 1)}`]
790
- : []),
791
- ...(cacheMetrics.cacheReadCoverage !== undefined
792
- ? [
793
- `- Cache Read Coverage: ${formatPercent(cacheMetrics.cacheReadCoverage, 1)}`,
794
- ]
887
+ ...(cacheMetrics.cachedRatio !== undefined
888
+ ? [`- Cached: ${formatPercent(cacheMetrics.cachedRatio, 1)}`]
795
889
  : []),
796
890
  ...(showCost
797
891
  ? [
@@ -804,21 +898,19 @@ export function renderMarkdownReport(period, usage, quotas, options) {
804
898
  : []),
805
899
  '',
806
900
  '### Usage by Provider',
807
- showCost
808
- ? '| Provider | Requests | Input | Output | Cache | Total | Cache Coverage | Cache Read Coverage | Measured Cost | API Cost |'
809
- : '| Provider | Requests | Input | Output | Cache | Total |',
810
- showCost
811
- ? '|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|'
812
- : '|---|---:|---:|---:|---:|---:|',
901
+ '',
902
+ providerHeader,
903
+ providerDivider,
813
904
  ...(providerRows.length
814
905
  ? providerRows
815
906
  : [
816
907
  showCost
817
- ? '| - | - | - | - | - | - | - | - | - | - |'
908
+ ? '| - | - | - | - | - | - | - | - | - |'
818
909
  : '| - | - | - | - | - | - |',
819
910
  ]),
820
911
  '',
821
912
  '### Subscription Quota',
913
+ '',
822
914
  ...(quotaLines.length
823
915
  ? quotaLines
824
916
  : ['- no provider quota data available']),
@@ -837,28 +929,22 @@ export function renderToastMessage(period, usage, quotas, options) {
837
929
  { label: 'Input', value: shortNumber(usage.input) },
838
930
  { label: 'Output', value: shortNumber(usage.output) },
839
931
  ];
840
- if (usage.cacheRead > 0) {
841
- tokenPairs.push({
842
- label: 'Cache Read',
843
- value: shortNumber(usage.cacheRead),
844
- });
845
- }
846
932
  if (usage.cacheWrite > 0) {
847
933
  tokenPairs.push({
848
934
  label: 'Cache Write',
849
935
  value: shortNumber(usage.cacheWrite),
850
936
  });
851
937
  }
852
- if (cacheMetrics.cacheCoverage !== undefined) {
938
+ if (usage.cacheRead > 0) {
853
939
  tokenPairs.push({
854
- label: 'Cache Coverage',
855
- value: formatPercent(cacheMetrics.cacheCoverage, 1),
940
+ label: 'Cache Read',
941
+ value: shortNumber(usage.cacheRead),
856
942
  });
857
943
  }
858
- if (cacheMetrics.cacheReadCoverage !== undefined) {
944
+ if (cacheMetrics.cachedRatio !== undefined) {
859
945
  tokenPairs.push({
860
- label: 'Cache Read Coverage',
861
- value: formatPercent(cacheMetrics.cacheReadCoverage, 1),
946
+ label: 'Cached',
947
+ value: formatPercent(cacheMetrics.cachedRatio, 1),
862
948
  });
863
949
  }
864
950
  lines.push(...alignPairs(tokenPairs).map((line) => fitLine(line, width)));
@@ -884,18 +970,11 @@ export function renderToastMessage(period, usage, quotas, options) {
884
970
  const providerCachePairs = Object.values(usage.providers)
885
971
  .map((provider) => {
886
972
  const metrics = getProviderCacheCoverageMetrics(provider);
887
- const parts = [];
888
- if (metrics.cacheCoverage !== undefined) {
889
- parts.push(`Cov ${formatPercent(metrics.cacheCoverage, 1)}`);
890
- }
891
- if (metrics.cacheReadCoverage !== undefined) {
892
- parts.push(`Read ${formatPercent(metrics.cacheReadCoverage, 1)}`);
893
- }
894
- if (parts.length === 0)
973
+ if (metrics.cachedRatio === undefined)
895
974
  return undefined;
896
975
  return {
897
976
  label: displayShortLabel(provider.providerID),
898
- value: parts.join(' '),
977
+ value: `Cached ${formatPercent(metrics.cachedRatio, 1)}`,
899
978
  };
900
979
  })
901
980
  .filter((item) => Boolean(item));
package/dist/title.js CHANGED
@@ -6,6 +6,8 @@ function sanitizeTitleFragment(value) {
6
6
  function isCoreDecoratedDetail(line) {
7
7
  if (!line)
8
8
  return false;
9
+ // Legacy coverage/Cov/CRC/CC tokens remain recognized so old decorated titles
10
+ // can still be detected and cleaned up after format migrations.
9
11
  if (/^Requests\s+\$?\d[\d.,]*[kKmM]?$/.test(line))
10
12
  return true;
11
13
  if (/^Input\s+\$?[\d.,]+[kKmM]?(?:\s+Output(?:\s+\$?[\d.,]+[kKmM]?)?)?~?$/.test(line)) {
@@ -16,8 +18,20 @@ function isCoreDecoratedDetail(line) {
16
18
  if (/^Cache(?:\s+Read)?\s+Coverage\s+\d[\d.,]*(?:%|~)?$/.test(line)) {
17
19
  return true;
18
20
  }
21
+ if (/^Cache(?:\s+Read|\s+R)?\s+Cov\s+\d[\d.,]*(?:%|~)?$/.test(line)) {
22
+ return true;
23
+ }
24
+ if (/^Cached\s+\d[\d.,]*(?:%|~)?$/.test(line))
25
+ return true;
19
26
  if (/^\$\S+\s+as API cost$/.test(line))
20
27
  return true;
28
+ if (/^API\s+\$\S+$/.test(line))
29
+ return true;
30
+ if (/^Est\$\S+$/.test(line))
31
+ return true;
32
+ if (/^(?:R\$?[\d.,]+[kKmM]?|I\$?[\d.,]+[kKmM]?|O\$?[\d.,]+[kKmM]?|CR\$?[\d.,]+[kKmM]?|CW\$?[\d.,]+[kKmM]?|Cd\d[\d.,]*%|CC\d[\d.,]*%|CRC\d[\d.,]*%|API\$\S+|Est\$\S+)(?:\s+(?:R\$?[\d.,]+[kKmM]?|I\$?[\d.,]+[kKmM]?|O\$?[\d.,]+[kKmM]?|CR\$?[\d.,]+[kKmM]?|CW\$?[\d.,]+[kKmM]?|Cd\d[\d.,]*%|CC\d[\d.,]*%|CRC\d[\d.,]*%|API\$\S+|Est\$\S+))*$/.test(line)) {
33
+ return true;
34
+ }
21
35
  // Single-line compact mode compatibility.
22
36
  if (/^I(?:nput)?\s+\$?\d[\d.,]*[kKmM]?\s+O(?:utput)?\s+\$?\d[\d.,]*[kKmM]?$/.test(line))
23
37
  return true;
@@ -31,12 +45,19 @@ function isCoreDecoratedDetail(line) {
31
45
  if (/^C(?:ache(?:\s*R(?:ead)?)?)?\s*Coverage\s+\d[\d.,]*(?:%|~)?$/.test(line)) {
32
46
  return true;
33
47
  }
48
+ if (/^C(?:ache(?:\s*(?:R|Read))?)?\s*Cov\s+\d[\d.,]*(?:%|~)?$/.test(line)) {
49
+ return true;
50
+ }
51
+ if (/^Cached\s+\d[\d.,]*(?:%|~)?$/.test(line))
52
+ return true;
53
+ if (/^API\s+\$\S+$/.test(line))
54
+ return true;
34
55
  return false;
35
56
  }
36
57
  function isQuotaDecoratedDetail(line) {
37
58
  if (!line)
38
59
  return false;
39
- if (/^(OAI|Cop|Ant|Kimi|XY|Buzz|RC(?:-[^\s]+)?)(?:\s+(?:\?|unsupported|unavailable|error|(?:\d+h|D|W|M)\d{1,3}|D[\d.,]+\/[\d.,]+|B(?:[¥$-])?[\d.,]+))+$/i.test(line)) {
60
+ if (/^(OAI|Cop|Ant|Kimi|XYAI|Buzz|RC(?:-[^\s]+)?)(?:\s+(?:\?|unsupported|unavailable|error|(?:\d+h|D|W|M)\d{1,3}|D[\d.,]+\/[\d.,]+|B(?:[¥$-])?[\d.,]+))+$/i.test(line)) {
40
61
  return true;
41
62
  }
42
63
  if (/^(OpenAI|Copilot|Anthropic|Kimi|XYAI|Buzz|RC(?:-[^\s]+)?)\s*$/.test(line)) {
@@ -45,9 +66,21 @@ function isQuotaDecoratedDetail(line) {
45
66
  if (/^(?:(?:Daily\s+\$[\d.,]+\/\$[\d.,]+|\$[\d.,]+\/\$[\d.,]+)(?:\s+(?:Rst|Exp\+?)\s+[-:\d]+)?|(?:\d+[hdw]|Weekly|Monthly)\s+\d{1,3}%(?:\s+Rst\s+[-:\d]+)?|Balance\s+\$[\d.,]+|Remaining\s+\?|(?:error|unsupported|unavailable))$/.test(line)) {
46
67
  return true;
47
68
  }
69
+ if (/^(?:D\$?[\d.,]+\/\$?[\d.,]+|B(?:[¥$-])?[\d.,]+|(?:\d+[hdw]|[DWM])\d{1,3}|S7d\d{1,3})(?:\s+(?:R|E\+?)\d[\d:.-]*)?$/.test(line)) {
70
+ return true;
71
+ }
48
72
  if (/^(OpenAI|Copilot|Anthropic|Kimi|XYAI|Buzz|RC(?:-[^\s]+)?)(?:\s+(?:(?:Daily\s+\$[\d.,]+\/\$[\d.,]+|\$[\d.,]+\/\$[\d.,]+)(?:\s+(?:Rst|Exp\+?)\s+[-:\d]+)?|(?:\d+[hdw]|Weekly|Monthly)\s+\d{1,3}%(?:\s+Rst\s+[-:\d]+)?|(?:error|unsupported|unavailable)))$/.test(line)) {
49
73
  return true;
50
74
  }
75
+ if (/^(OpenAI|Copilot|Anthropic|Kimi|XYAI|Buzz|RC(?:-[^\s]+)?)(?:\s+(?:D\$?[\d.,]+\/\$?[\d.,]+|B(?:[¥$-])?[\d.,]+|(?:\d+[hdw]|[DWM])\d{1,3}|S7d\d{1,3})(?:\s+(?:R|E\+?)\d[\d:.-]*)?)$/.test(line)) {
76
+ return true;
77
+ }
78
+ if (/^(?:(?:D\$?[\d.,]+\/\$?[\d.,]+|B(?:[¥$-])?[\d.,]+|(?:\d+[hdw]|[DWM])\d{1,3}|S7d\d{1,3}|(?:R|E\+?)\d[\d:.-]*))(?:\s+(?:(?:D\$?[\d.,]+\/\$?[\d.,]+|B(?:[¥$-])?[\d.,]+|(?:\d+[hdw]|[DWM])\d{1,3}|S7d\d{1,3}|(?:R|E\+?)\d[\d:.-]*)))*$/.test(line)) {
79
+ return true;
80
+ }
81
+ if (/^(OpenAI|Copilot|Anthropic|Kimi|XYAI|Buzz|RC(?:-[^\s]+)?)(?:\s+(?:(?:D\$?[\d.,]+\/\$?[\d.,]+|B(?:[¥$-])?[\d.,]+|(?:\d+[hdw]|[DWM])\d{1,3}|S7d\d{1,3}|(?:R|E\+?)\d[\d:.-]*)))*$/.test(line)) {
82
+ return true;
83
+ }
51
84
  return false;
52
85
  }
53
86
  function isSingleLineDecoratedPrefix(line) {
@@ -69,8 +102,20 @@ function isSingleLineDecoratedPrefix(line) {
69
102
  if (/^Cache(?:\s+Read)?\s+Coverage\s+\d[\d.,]*(?:%|~)$/.test(line)) {
70
103
  return true;
71
104
  }
105
+ if (/^Cache(?:\s+Read|\s+R)?\s+Cov\s+\d[\d.,]*(?:%|~)$/.test(line)) {
106
+ return true;
107
+ }
108
+ if (/^Cached\s+\d[\d.,]*(?:%|~)$/.test(line))
109
+ return true;
72
110
  if (/^\$\S+\s+as API cost(?:~|$)/.test(line))
73
111
  return true;
112
+ if (/^API\s+\$\S+(?:~|$)/.test(line))
113
+ return true;
114
+ if (/^Est\$\S+(?:~|$)/.test(line))
115
+ return true;
116
+ if (/^(?:R\$?[\d.,]+[kKmM]?|I\$?[\d.,]+[kKmM]?|O\$?[\d.,]+[kKmM]?|CR\$?[\d.,]+[kKmM]?|CW\$?[\d.,]+[kKmM]?|Cd\d[\d.,]*%|CC\d[\d.,]*%|CRC\d[\d.,]*%|API\$\S+|Est\$\S+)(?:\s+(?:R\$?[\d.,]+[kKmM]?|I\$?[\d.,]+[kKmM]?|O\$?[\d.,]+[kKmM]?|CR\$?[\d.,]+[kKmM]?|CW\$?[\d.,]+[kKmM]?|Cd\d[\d.,]*%|CC\d[\d.,]*%|CRC\d[\d.,]*%|API\$\S+|Est\$\S+))*?(?:~|$)/.test(line)) {
117
+ return true;
118
+ }
74
119
  return false;
75
120
  }
76
121
  function isSingleLineDetailPrefix(line) {
package/dist/tools.js CHANGED
@@ -11,7 +11,7 @@ export function createQuotaSidebarTools(deps) {
11
11
  };
12
12
  return {
13
13
  quota_summary: tool({
14
- description: 'Show usage and quota summary for session/day/week/month.',
14
+ description: 'Show usage and quota summary for session/day/week/month. Returns the full markdown report with totals, highlights, provider table, and subscription quota so callers can present the report directly to the user.',
15
15
  args: {
16
16
  period: z.enum(['session', 'day', 'week', 'month']).optional(),
17
17
  toast: z.boolean().optional(),
@@ -42,7 +42,7 @@ export function createQuotaSidebarTools(deps) {
42
42
  },
43
43
  }),
44
44
  quota_show: tool({
45
- description: 'Toggle sidebar title display mode. When on, titles show token usage and quota; when off, titles revert to original.',
45
+ description: 'Toggle sidebar title display mode. When on, titles show token usage and quota; when off, titles revert to original. Returns a user-facing status message that callers should present directly.',
46
46
  args: {
47
47
  enabled: z
48
48
  .boolean()
package/dist/types.d.ts CHANGED
@@ -53,18 +53,15 @@ export type CacheUsageBuckets = {
53
53
  readWrite: CacheUsageBucket;
54
54
  };
55
55
  /**
56
- * Derived cache coverage metrics.
56
+ * Derived cache metrics.
57
57
  *
58
- * - `cacheCoverage`: fraction of prompt surface covered by read-write cache
59
- * (`(cacheRead + cacheWrite) / (input + cacheRead + cacheWrite)`).
60
- * Only defined when the read-write bucket has traffic.
61
- * - `cacheReadCoverage`: fraction of prompt surface served from read-only cache
62
- * (`cacheRead / (input + cacheRead)`).
63
- * Only defined when the read-only bucket has traffic.
58
+ * - `cachedRatio`: fraction of the observed input surface that was served from
59
+ * cache (`cacheRead / (input + cacheRead)`).
60
+ * This is an exact ratio over normalized message totals, not a theoretical
61
+ * cache hit rate.
64
62
  */
65
63
  export type CacheCoverageMetrics = {
66
- cacheCoverage: number | undefined;
67
- cacheReadCoverage: number | undefined;
64
+ cachedRatio: number | undefined;
68
65
  };
69
66
  export type RecentProviderEvent = {
70
67
  providerID: string;
@@ -81,7 +78,7 @@ export type CachedProviderUsage = {
81
78
  /** Equivalent API billing cost (USD) computed from model pricing. */
82
79
  apiCost: number;
83
80
  assistantMessages: number;
84
- /** Provider-level cache coverage buckets grouped by model cache behavior. */
81
+ /** Provider-level cache buckets grouped by model cache behavior. */
85
82
  cacheBuckets?: CacheUsageBuckets;
86
83
  };
87
84
  export type CachedSessionUsage = {
@@ -98,7 +95,7 @@ export type CachedSessionUsage = {
98
95
  apiCost: number;
99
96
  assistantMessages: number;
100
97
  /**
101
- * Cache coverage buckets grouped by model cache behavior.
98
+ * Cache buckets grouped by model cache behavior.
102
99
  *
103
100
  * `undefined` when no cache-capable models were used or data predates
104
101
  * billingVersion 3. The fallback in `resolvedCacheUsageBuckets()` derives
@@ -150,9 +147,9 @@ export type QuotaSidebarConfig = {
150
147
  enabled: boolean;
151
148
  width: number;
152
149
  /**
153
- * When true, render multi-line decorated session titles.
154
- * Enabled by default for clearer token/quota layout in sidebar.
155
- * Set false to keep a compact single-line title.
150
+ * Legacy switch retained for compatibility.
151
+ * TUI keeps a compact multiline sidebar layout; Desktop keeps a compact
152
+ * single-line layout.
156
153
  */
157
154
  multilineTitle?: boolean;
158
155
  showCost: boolean;
package/dist/usage.d.ts CHANGED
@@ -6,7 +6,7 @@ import type { CacheCoverageMetrics, CacheCoverageMode, CacheUsageBuckets, Cached
6
6
  * fields). This is distinct from the plugin *state* version managed by the
7
7
  * persistence layer; billing version only governs usage-cache staleness.
8
8
  */
9
- export declare const USAGE_BILLING_CACHE_VERSION = 6;
9
+ export declare const USAGE_BILLING_CACHE_VERSION = 7;
10
10
  export type ProviderUsage = {
11
11
  providerID: string;
12
12
  input: number;
package/dist/usage.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * fields). This is distinct from the plugin *state* version managed by the
5
5
  * persistence layer; billing version only governs usage-cache staleness.
6
6
  */
7
- export const USAGE_BILLING_CACHE_VERSION = 6;
7
+ export const USAGE_BILLING_CACHE_VERSION = 7;
8
8
  const MAX_RECENT_PROVIDER_EVENTS = 100;
9
9
  function emptyCacheUsageBucket() {
10
10
  return {
@@ -107,18 +107,11 @@ function resolvedCacheUsageBuckets(usage) {
107
107
  return explicit;
108
108
  }
109
109
  export function getCacheCoverageMetrics(usage) {
110
- const buckets = resolvedCacheUsageBuckets(usage);
111
- const readWritePromptSurface = buckets.readWrite.input +
112
- buckets.readWrite.cacheRead +
113
- buckets.readWrite.cacheWrite;
114
- const readOnlyPromptSurface = buckets.readOnly.input + buckets.readOnly.cacheRead;
110
+ const hasCacheActivity = usage.cacheRead > 0 || usage.cacheWrite > 0;
111
+ const cachedSurface = usage.input + usage.cacheRead;
115
112
  return {
116
- cacheCoverage: readWritePromptSurface > 0
117
- ? (buckets.readWrite.cacheRead + buckets.readWrite.cacheWrite) /
118
- readWritePromptSurface
119
- : undefined,
120
- cacheReadCoverage: readOnlyPromptSurface > 0
121
- ? buckets.readOnly.cacheRead / readOnlyPromptSurface
113
+ cachedRatio: hasCacheActivity && cachedSurface > 0
114
+ ? usage.cacheRead / cachedSurface
122
115
  : undefined,
123
116
  };
124
117
  }
@@ -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;
@@ -130,7 +131,7 @@ export function createUsageService(deps) {
130
131
  return 'read-write';
131
132
  }
132
133
  // Last resort: if the message has cache.read tokens from an unknown provider,
133
- // treat it as read-only (the safer default — avoids inflating Cache Coverage).
134
+ // treat it as read-only (the safer default — avoids overstating cached ratio).
134
135
  return 'read-only';
135
136
  };
136
137
  const isFiniteNumber = (value) => typeof value === 'number' && Number.isFinite(value);
@@ -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.11",
3
+ "version": "2.0.14",
4
4
  "description": "OpenCode plugin that shows quota and token usage in session titles",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",