@leo000001/opencode-quota-sidebar 3.0.10 → 4.0.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.
Files changed (53) hide show
  1. package/CHANGELOG.md +0 -1
  2. package/README.md +157 -42
  3. package/README.zh-CN.md +157 -42
  4. package/SECURITY.md +1 -1
  5. package/dist/cli.d.ts +18 -0
  6. package/dist/cli.js +354 -0
  7. package/dist/cli_render.d.ts +17 -0
  8. package/dist/cli_render.js +292 -0
  9. package/dist/events.d.ts +1 -1
  10. package/dist/events.js +2 -2
  11. package/dist/format.d.ts +4 -0
  12. package/dist/format.js +302 -41
  13. package/dist/history_messages.d.ts +8 -0
  14. package/dist/history_messages.js +157 -0
  15. package/dist/history_usage.d.ts +93 -0
  16. package/dist/history_usage.js +251 -0
  17. package/dist/index.js +29 -4
  18. package/dist/period.d.ts +29 -1
  19. package/dist/period.js +187 -9
  20. package/dist/provider_catalog.d.ts +8 -0
  21. package/dist/provider_catalog.js +68 -0
  22. package/dist/providers/core/anthropic.d.ts +1 -1
  23. package/dist/providers/core/anthropic.js +69 -45
  24. package/dist/providers/core/openai.js +38 -2
  25. package/dist/providers/index.d.ts +1 -2
  26. package/dist/providers/index.js +1 -3
  27. package/dist/quota.d.ts +4 -2
  28. package/dist/quota.js +18 -21
  29. package/dist/quota_render.d.ts +1 -1
  30. package/dist/quota_render.js +23 -24
  31. package/dist/quota_service.d.ts +1 -0
  32. package/dist/quota_service.js +151 -19
  33. package/dist/storage.d.ts +1 -1
  34. package/dist/storage.js +4 -4
  35. package/dist/storage_dates.d.ts +1 -1
  36. package/dist/storage_dates.js +8 -5
  37. package/dist/storage_parse.js +23 -1
  38. package/dist/supported_quota.d.ts +4 -0
  39. package/dist/supported_quota.js +36 -0
  40. package/dist/title.js +18 -8
  41. package/dist/tools.d.ts +14 -3
  42. package/dist/tools.js +54 -2
  43. package/dist/tui.tsx +17 -6
  44. package/dist/tui_helpers.js +11 -6
  45. package/dist/types.d.ts +8 -0
  46. package/dist/usage.d.ts +18 -0
  47. package/dist/usage.js +93 -9
  48. package/dist/usage_service.d.ts +4 -1
  49. package/dist/usage_service.js +193 -189
  50. package/package.json +4 -1
  51. package/quota-sidebar.config.example.json +36 -45
  52. package/dist/providers/third_party/xyai.d.ts +0 -2
  53. package/dist/providers/third_party/xyai.js +0 -348
package/dist/tui.tsx CHANGED
@@ -25,7 +25,11 @@ import {
25
25
  } from './storage.js'
26
26
  import { looksDecorated, normalizeBaseTitle } from './title.js'
27
27
  import type { QuotaSidebarConfig } from './types.js'
28
- import { fromCachedSessionUsage, summarizeMessages } from './usage.js'
28
+ import {
29
+ fromCachedSessionUsage,
30
+ mergeUsage,
31
+ summarizeMessages,
32
+ } from './usage.js'
29
33
 
30
34
  const id = 'leo.quota-sidebar'
31
35
  const INTERNAL_CONTEXT_PLUGIN_ID = 'internal:sidebar-context'
@@ -89,11 +93,18 @@ async function loadSidebarPanel(
89
93
 
90
94
  const liveUsage = summarizeMessages(liveEntries, 0, 1)
91
95
  const cachedUsage = session?.sidebarPanel?.usage || session?.usage
92
- const usage = cachedUsage
96
+ const persistedUsage = cachedUsage
93
97
  ? fromCachedSessionUsage(cachedUsage)
94
- : liveUsage.assistantMessages > 0
98
+ : undefined
99
+ const usage =
100
+ liveUsage.assistantMessages > 0 &&
101
+ (!persistedUsage ||
102
+ liveUsage.assistantMessages > persistedUsage.assistantMessages ||
103
+ (liveUsage.assistantMessages === persistedUsage.assistantMessages &&
104
+ liveUsage.total >= persistedUsage.total))
95
105
  ? liveUsage
96
- : undefined
106
+ : persistedUsage ||
107
+ (liveUsage.assistantMessages > 0 ? liveUsage : undefined)
97
108
  const compactTitle = resolveCompactTitle(sessionID, session?.lastAppliedTitle)
98
109
 
99
110
  if (!enabled) {
@@ -154,8 +165,8 @@ function useSidebarPanelData(api: TuiPluginApi, sessionID: () => string) {
154
165
  }
155
166
 
156
167
  const scheduleRefresh = () => {
157
- queueRefresh(300)
158
- queueRefresh(1_000)
168
+ queueRefresh(150)
169
+ queueRefresh(600)
159
170
  }
160
171
 
161
172
  // Bulk session sync populates messages asynchronously without emitting the
@@ -1,5 +1,6 @@
1
1
  import { fitLine, renderSidebarQuotaLineGroups } from './format.js';
2
2
  import { collapseQuotaSnapshots } from './quota_render.js';
3
+ import { isSupportedQuotaSnapshot, isSupportedQuotaTitleLabel, } from './supported_quota.js';
3
4
  const VISIBLE_QUOTA_STATUSES = new Set([
4
5
  'ok',
5
6
  'error',
@@ -70,7 +71,7 @@ function fallbackQuotaTone(detail) {
70
71
  if (/\bB-/.test(safe))
71
72
  return 'error';
72
73
  const percents = [
73
- ...safe.matchAll(/\b(?:\d+[hdw]|[DWM]|S7d|O7d|OA7d|Co7d)(\d{1,3})\b/gi),
74
+ ...safe.matchAll(/\b(?:\d+[hdw]|[DWM]|S7d|O7d|OA7d|Co7d|Sk5h|SkW)(\d{1,3})\b/gi),
74
75
  ]
75
76
  .map((match) => Number(match[1]))
76
77
  .filter((value) => Number.isFinite(value));
@@ -107,7 +108,7 @@ export function renderSidebarQuotaGroups(quotas, config) {
107
108
  });
108
109
  }
109
110
  export function sidebarPanelQuotaSnapshots(panel) {
110
- return panel?.panelQuotas || panel?.quotas || [];
111
+ return (panel?.panelQuotas || panel?.quotas || []).filter((quota) => isSupportedQuotaSnapshot(quota));
111
112
  }
112
113
  export function fallbackQuotaGroupsFromTitle(title, width) {
113
114
  const parts = (title || '')
@@ -120,18 +121,22 @@ export function fallbackQuotaGroupsFromTitle(title, width) {
120
121
  if (quotaParts.length === 0)
121
122
  return [];
122
123
  const contentWidth = quotaParts.length > 1 ? Math.max(1, width - 2) : width;
123
- return quotaParts.map((part, index) => {
124
+ const groups = [];
125
+ for (const [index, part] of quotaParts.entries()) {
124
126
  const line = fitLine(part, contentWidth);
125
127
  const parsed = parseQuotaLineParts([line]);
126
- return {
128
+ if (!isSupportedQuotaTitleLabel(parsed.shortLabel))
129
+ continue;
130
+ groups.push({
127
131
  providerID: `fallback:${index}`,
128
132
  status: 'ok',
129
133
  tone: fallbackQuotaTone(parsed.detail),
130
134
  shortLabel: parsed.shortLabel,
131
135
  detail: parsed.detail,
132
136
  continuationLines: parsed.continuationLines,
133
- };
134
- });
137
+ });
138
+ }
139
+ return groups;
135
140
  }
136
141
  export function quotaGroupsUseBullets(groups) {
137
142
  return groups.length > 1;
package/dist/types.d.ts CHANGED
@@ -12,6 +12,12 @@ export type QuotaWindow = {
12
12
  usedPercent?: number;
13
13
  resetAt?: string;
14
14
  };
15
+ export type QuotaStaleReasonKind = 'timeout' | 'network' | 'http_5xx' | 'http_transient' | 'invalid_response' | 'unknown';
16
+ export type QuotaStaleMeta = {
17
+ staleAt: number;
18
+ staleReason: string;
19
+ staleReasonKind: QuotaStaleReasonKind;
20
+ };
15
21
  export type QuotaSnapshot = {
16
22
  providerID: string;
17
23
  /** Adapter ID that produced this snapshot (e.g. openai, rightcode). */
@@ -35,6 +41,8 @@ export type QuotaSnapshot = {
35
41
  note?: string;
36
42
  /** Multi-window quota (e.g. OpenAI short-term + weekly). */
37
43
  windows?: QuotaWindow[];
44
+ /** Last successful snapshot reused during a transient quota fetch failure. */
45
+ stale?: QuotaStaleMeta;
38
46
  };
39
47
  export type QuotaProviderConfig = {
40
48
  enabled?: boolean;
package/dist/usage.d.ts CHANGED
@@ -46,12 +46,30 @@ export type UsageOptions = {
46
46
  export declare function getCacheCoverageMetrics(usage: Pick<UsageSummary, 'input' | 'cacheRead' | 'cacheWrite' | 'assistantMessages' | 'cacheBuckets'>): CacheCoverageMetrics;
47
47
  export declare function getProviderCacheCoverageMetrics(usage: Pick<ProviderUsage, 'input' | 'cacheRead' | 'cacheWrite' | 'assistantMessages' | 'cacheBuckets'>): CacheCoverageMetrics;
48
48
  export declare function emptyUsageSummary(): UsageSummary;
49
+ export declare function accumulateMessagesInCompletedRange(target: UsageSummary, entries: Array<{
50
+ info: Message;
51
+ }>, startAt?: number, endAt?: number, options?: UsageOptions): UsageSummary;
49
52
  export declare function summarizeMessages(entries: Array<{
50
53
  info: Message;
51
54
  }>, startAt?: number, sessionCount?: number, options?: UsageOptions): UsageSummary;
52
55
  export declare function summarizeMessagesInCompletedRange(entries: Array<{
53
56
  info: Message;
54
57
  }>, startAt: number, endAt: number, sessionCount?: number, options?: UsageOptions): UsageSummary;
58
+ export declare function summarizeMessagesAcrossCompletedRanges(entries: Array<{
59
+ info: Message;
60
+ }>, ranges: Array<{
61
+ startAt: number;
62
+ endAt: number;
63
+ }>, options?: UsageOptions): UsageSummary[];
64
+ export declare function accumulateMessagesAcrossCompletedRanges(summaries: UsageSummary[], entries: Array<{
65
+ info: Message;
66
+ }>, ranges: Array<{
67
+ startAt: number;
68
+ endAt: number;
69
+ }>, options?: UsageOptions): Set<number>;
70
+ export declare function mergeCursorFromEntries(cursor: IncrementalCursor | undefined, entries: Array<{
71
+ info: Message;
72
+ }>): IncrementalCursor | undefined;
55
73
  /**
56
74
  * P1: Incremental usage aggregation.
57
75
  * Only processes messages newer than the cursor. Returns updated cursor.
package/dist/usage.js CHANGED
@@ -235,27 +235,111 @@ function isCompletedAssistantInRange(message, startAt = 0, endAt = Number.POSITI
235
235
  const completed = completedTimeOf(message);
236
236
  if (completed === undefined)
237
237
  return false;
238
- return completed >= startAt && completed <= endAt;
238
+ return completed >= startAt && completed < endAt;
239
239
  }
240
- export function summarizeMessages(entries, startAt = 0, sessionCount = 1, options) {
241
- const summary = emptyUsageSummary();
242
- summary.sessionCount = sessionCount;
240
+ export function accumulateMessagesInCompletedRange(target, entries, startAt = 0, endAt = Number.POSITIVE_INFINITY, options) {
243
241
  for (const entry of entries) {
244
- if (!isCompletedAssistantInRange(entry.info, startAt))
242
+ if (!isCompletedAssistantInRange(entry.info, startAt, endAt))
245
243
  continue;
246
- addMessageUsage(summary, entry.info, options);
244
+ addMessageUsage(target, entry.info, options);
247
245
  }
246
+ return target;
247
+ }
248
+ export function summarizeMessages(entries, startAt = 0, sessionCount = 1, options) {
249
+ const summary = emptyUsageSummary();
250
+ summary.sessionCount = sessionCount;
251
+ accumulateMessagesInCompletedRange(summary, entries, startAt, Infinity, options);
248
252
  return summary;
249
253
  }
250
254
  export function summarizeMessagesInCompletedRange(entries, startAt, endAt, sessionCount = 1, options) {
251
255
  const summary = emptyUsageSummary();
252
256
  summary.sessionCount = sessionCount;
257
+ accumulateMessagesInCompletedRange(summary, entries, startAt, endAt, options);
258
+ return summary;
259
+ }
260
+ function rangeIndexForCompletedAt(ranges, completedAt) {
261
+ let low = 0;
262
+ let high = ranges.length - 1;
263
+ while (low <= high) {
264
+ const mid = Math.floor((low + high) / 2);
265
+ const range = ranges[mid];
266
+ if (completedAt < range.startAt) {
267
+ high = mid - 1;
268
+ continue;
269
+ }
270
+ if (completedAt >= range.endAt) {
271
+ low = mid + 1;
272
+ continue;
273
+ }
274
+ return mid;
275
+ }
276
+ return -1;
277
+ }
278
+ export function summarizeMessagesAcrossCompletedRanges(entries, ranges, options) {
279
+ const summaries = ranges.map(() => emptyUsageSummary());
280
+ accumulateMessagesAcrossCompletedRanges(summaries, entries, ranges, options);
281
+ return summaries;
282
+ }
283
+ export function accumulateMessagesAcrossCompletedRanges(summaries, entries, ranges, options) {
284
+ const touched = new Set();
285
+ if (ranges.length === 0)
286
+ return touched;
253
287
  for (const entry of entries) {
254
- if (!isCompletedAssistantInRange(entry.info, startAt, endAt))
288
+ if (!isAssistant(entry.info))
289
+ continue;
290
+ const completed = completedTimeOf(entry.info);
291
+ if (completed === undefined)
255
292
  continue;
256
- addMessageUsage(summary, entry.info, options);
293
+ const index = rangeIndexForCompletedAt(ranges, completed);
294
+ if (index < 0)
295
+ continue;
296
+ addMessageUsage(summaries[index], entry.info, options);
297
+ touched.add(index);
257
298
  }
258
- return summary;
299
+ for (const index of touched) {
300
+ summaries[index].sessionCount = 1;
301
+ }
302
+ return touched;
303
+ }
304
+ export function mergeCursorFromEntries(cursor, entries) {
305
+ let bestTime = typeof cursor?.lastMessageTime === 'number' &&
306
+ Number.isFinite(cursor.lastMessageTime)
307
+ ? cursor.lastMessageTime
308
+ : Number.NEGATIVE_INFINITY;
309
+ let bestID = cursor?.lastMessageId || '';
310
+ const idsAtBestTime = new Set(Array.isArray(cursor?.lastMessageIdsAtTime)
311
+ ? cursor.lastMessageIdsAtTime
312
+ : cursor?.lastMessageId && Number.isFinite(bestTime)
313
+ ? [cursor.lastMessageId]
314
+ : []);
315
+ for (const entry of entries) {
316
+ const msg = entry.info;
317
+ if (!isAssistant(msg))
318
+ continue;
319
+ const completed = completedTimeOf(msg);
320
+ if (completed === undefined)
321
+ continue;
322
+ if (completed > bestTime) {
323
+ bestTime = completed;
324
+ bestID = msg.id;
325
+ idsAtBestTime.clear();
326
+ idsAtBestTime.add(msg.id);
327
+ continue;
328
+ }
329
+ if (completed !== bestTime)
330
+ continue;
331
+ idsAtBestTime.add(msg.id);
332
+ if (msg.id.localeCompare(bestID) > 0) {
333
+ bestID = msg.id;
334
+ }
335
+ }
336
+ if (!Number.isFinite(bestTime) || !bestID)
337
+ return undefined;
338
+ return {
339
+ lastMessageId: bestID,
340
+ lastMessageTime: bestTime,
341
+ lastMessageIdsAtTime: Array.from(idsAtBestTime).sort(),
342
+ };
259
343
  }
260
344
  /**
261
345
  * P1: Incremental usage aggregation.
@@ -1,5 +1,8 @@
1
1
  import type { PluginInput } from '@opencode-ai/plugin';
2
+ import { type HistoryPeriod } from './period.js';
2
3
  import { type UsageSummary } from './usage.js';
4
+ export type { HistoryUsageRow, HistoryUsageResult } from './history_usage.js';
5
+ import type { HistoryUsageResult } from './history_usage.js';
3
6
  import type { QuotaSidebarConfig, QuotaSidebarState } from './types.js';
4
7
  type DescendantsResolver = {
5
8
  listDescendantSessionIDs: (sessionID: string, opts: {
@@ -24,8 +27,8 @@ export declare function createUsageService(deps: {
24
27
  }): {
25
28
  summarizeSessionUsageForDisplay: (sessionID: string, includeChildren: boolean) => Promise<UsageSummary>;
26
29
  summarizeForTool: (period: "session" | "day" | "week" | "month", sessionID: string, includeChildren: boolean) => Promise<UsageSummary>;
30
+ summarizeHistoryUsage: (period: HistoryPeriod, rawSince: string) => Promise<HistoryUsageResult>;
27
31
  markSessionDirty: (sessionID: string) => void;
28
32
  markForceRescan: (sessionID: string) => void;
29
33
  forgetSession: (sessionID: string) => void;
30
34
  };
31
- export {};